textmachine/platform/docs/DEFECT_REGISTER.md

818 KiB
Raw Blame History

Регистр дефектов и уязвимостей платформы

Заведён по решению владельца 04.08 («отдельно ведётся колонка багов и уязвимостей — тут опасно всё»). ⚠ Токен ОСПОРЕНО(PD-N) (введён паком P8-REVIEW 24.08; правило ратифицировано D39.159, оговорка к PD-1 дописана 27.08 по вычитке старшего): строка стоит fixed, а пак доказал, что её пин доказывает свойство СЛАБЕЕ, чем строка гласит. Статус при этом НЕ меняется — это акт лендинга — и строка не пере-открывается, пока описанный ею дефект из кода ушёл: иначе регистр объявляет вернувшимся баг, который не воспроизводится, и считает один пробел двумя открытыми. Живой пробел несёт открытая строка-преемник с ДВУСТОРОННЕЙ ссылкой, её номер стоит в скобках токена; пере-открытие — только когда дефект ВОСПРОИЗВОДИТСЯ. Грепается: grep -n 'ОСПОРЕНО(' ….

Правила: каждая находка любой сессии/приёмки/аудита — строкой сюда ДО закрытия; ID стабилен навсегда; закрытие — только с коммитом фикса и тестом, пинящим свойство (урок PD-1: свойство без пинящего теста считается НЕ закрытым; оговорка к нему — токен ОСПОРЕНО выше). Класс: vuln — эксплуатируемо или ослабляет защиту · bug — неверное поведение · hardening — защита в глубину / латентное · doc — док лжёт о коде · standards — расхождение с объявленной нормой зоны (введены приёмкой P2; словарь отставал от строк — испр. оркестратором №15). Статус: open · fixed(<commit>) · accepted-risk(<кем, когда>).

Форма файла (пинг оркестратора №16, 09.08). Таблица разложена на секции — открытые по весу, принятый риск, закрытые по эрам паков, — а построчная форма | PD-N | … | сохранена: docs/scripts/counts.py (путь от КОРНЯ репозитория — скрипт зоны docs/, зовётся python3 docs/scripts/counts.py --check оттуда же) ключуется формой строки, а не позицией, и секции его не ломают. ID остаётся стабильным навсегда, поэтому строка не переезжает между секциями иначе как при смене статуса, и внутри секции строки идут по номеру.

Открытые — major

Несущий путь или контрактно видимое поведение. Каждая строка здесь — то, что решается до следующего пака, а не «когда-нибудь».

ID Класс Серьёзность Где Суть Статус Источник
PD-410 bug major internal/pgstore/readmodel.go, греп draftWork (⚠ адрес пере-нацелен 06.09: строка уехала на ~130 позиций правками пака формы заказа); ⚠ контраст ряда «движку передаётся ТОЛЬКО --ceiling-usd» БОЛЬШЕ НЕ ВЕРЕН--max-units едет в argv с 05.09 (internal/runs/spawn.go, греп maxUnitsFor), и draft-волна по ВСЕМ чанкам книги больше не фанится: её гейтит volumeScope.allows (backend/internal/pipeline/volume.go) Полоса и платёжная модель считают, что прогон работает над диапазоном «C глав от edit-фронта» в обеих волнах, а движок гонит волны последовательно от СВОИХ фронтов до денег — и на continuation над недочерновленной книгой (draft_before ≥ chapters_before + C) формула даёт draftWork=0: движок тратит весь потолок на черновики, полоса стоит 0/C весь прогон со stage editing на прогоне, который только черновит. Зеркало дефекта, который draftWork чинил (тот закрывал «ready на 50%», этот открывает «0% при сделанной работе»). Это НЕ локальная ошибка формулы: любая полоса из (баз, C, D, E) где-то соврёт, пока платёжная модель («довести до конца C глав от e0») и движковый план работ («деньги в порядке волн от фронтов») не согласованы — лечение требует либо передавать движку план (диапазон/волны), либо выводить total полосы из движкового плана; ceiling_chapters сегодня до движка не доезжает вовсе. Архитектурное, ОТДЕЛЬНЫМ паком по слову оркестратора 28.08 («остановись и скажи»); подтверждено воркфлоу арифметикой на существующем зелёном пине (TestARunOverADraftedBacklogOwesOnlyTheLastPass держит draftWork=0 в родственной точке) ⚠ ПЕРЕ-ДИСПОЗИЦИОНИРОВАНА паком P12 (30.08): направление ЕСТЬ — D39.165 §1, четыре части (цена от объёма исходника · стоп объёма в движке · хвост качества в тариф). (г) константу по сегодняшним числам НЕ калибровать — гейт строка бэклога 202. Что остаётся открытым: не решение, а МЕХАНИКА, и она — отдельный пак (слово оркестратора 28.08), паком P12 не тронута. ⚠ ПРИЧИНА СНЯТА ПАКОМ «ФОРМА ЗАКАЗА» (05.09), статус флипает ЛЕНДИНГ; и снята она НЕ так, как обещала строка 280 — расхождение названо. Строка велела УДАЛИТЬ draftWork; зона этого не сделала и объясняет почему: дефект draftWork в том, что draft-волна фанилась по ВСЕЙ книге, а формула считала купленный диапазон. С проводкой translate --max-units волна больше по всей книге не фанится (backend/internal/pipeline/volume.go, volumeScope.allows гейтит и draft-волну), то есть посылка формулы стала истинной, а сама формула — нужной: непочерновленный остаток заказа это реальная работа прогона. Удалить её значило бы сломать полосу по-новому. ⇒ снята ПРИЧИНА, носитель оставлен. Испр. 06.09: здесь стояло «полоса при этом осталась в ГЛАВАХ, перевод её в юниты в этот минор не входит» — НЕВЕРНО, и это был третий носитель одной моей ошибки (отчёт зоны, состав минора, этот ряд). runDone/runTotal первой ветвью считают в ЮНИТАХ, когда runs.ordered_units не null (internal/pgstore/readmodel.go, греп ordered_units is not null), и это пиньнуто runs.TestACharacterOrdersBarIsCountedInWhatItActuallyBought. draftWork при этом остаётся носителем ГЛАВНОЙ ветви — той, что считает заказ в главах, — и довод выше её и касается. ⇒ настоящее следствие: канон 0.11.0 описывает Progress как «in chapters» для ВСЕХ трёх форм прогона, а сборка для символьного заказа шлёт юниты; недостающий случай внесён в состав минора и отдан оркестратору 06.09. fixed(628cc56) воркфлоу-ревью P9 28.08 (линза bar:double-count), стоп-решение сессии P9 28.08: тянет архитектуру, не чинится в паке
PD-424 bug major internal/runs/reconcile.go reopen (вердикт deferred) и reconcileOne, internal/pgstore/runs.go StalledRuns, internal/pgstore/observe.go ЖИВОЙ прогон с намертво заблокированной расплатой невидим на ВСЕХ поверхностях PD-385 и не поддаётся run abandon — счётчик неудач не растёт НИКОГДА. Цепь, воспроизведённая охотником приёмки на десяти проходах: юнит исчез без маркера → restartsettle возвращает settlementBlocked, nil (не ошибку) → reopen отказывается открыть новую попытку поверх незакрытого холда и возвращает вердикт deferredcase deferred: return nilreconcileOne считает проход УСПЕШНЫМ и вдобавок зовёт ClearRunDeferral. Итог: reconcile_failures 0, reconcile_after NULL, StalledRuns(5) пуст, гейдж 0, холд заморожен, движок дёргается каждым проходом. Вывести из состояния может только пользователь, нажав Stop, — и никто ему об этом не говорит. ⚠ Это ТА ЖЕ болезнь, что PD-384/PD-385, но в ЖИВОЙ фазе, куда пак P11 не дошёл: он лечил вторую фазу (расплату) и её поверхности. ⚠ СУЖЕНА пак P12 (3031.08), НЕ ЗАКРЫТА — половина «невидим» вылечена, половина «ручки нет» стоит. Приор зоны принят и исполнен: блокированная расплата — НЕУДАЧА владеющей фазы, и она считается. restart больше не выбрасывает вердикт settle: на settlementBlocked он возвращает errSettlementBlocked ДО reopen (прежний путь платил за запрос, чтобы узнать то, что settle только что сказал, писал WARN, винящий рестарт в блокировке расплаты, и отвечал nil, который вызывающий считал успешным проходом). reconcileOne ключует на этом сентинеле причину — ОДНА фраза на оба фазовых пути — и ЗАДЕРЖКУ: расписание РАСПЛАТЫ, а не живой фазы, потому что открытая резервация и есть resume-гейт пользователя, и получасовой живой бэкофф держал бы его resume за блокировку, с которой он ничего сделать не может. Вердикт deferred у reopen оставлен как был: после правки он достижим только на settlementRaced с моментально открытой резервацией — самоисправляется следующим проходом, и фаза расплаты прямо аргументирует, что гонка не должна попадать на счётчик оператора. Гард выключения (ctx.Err()) стоит: счёт, придуманный ОСТАНОВКОЙ демона, — тот самый дефект, который соседняя фаза уже нашла. Итог, проверенный пином runs.TestALiveRunWhoseSettlementIsBlockedIsCountedAndReachesTheOperator: reconcile_failures доходит до порога, строка появляется в runs --stalled (половина live, не settling), гейдж tm_platform_runs_stalled = 1, отсрочка не превышает settlementBackoffCap. Посадка M13 КРАСНАЯ адресно. ⚠ ЧТО ОСТАЁТСЯ ОТКРЫТЫМ и почему не взято этим паком: терминальная РУЧКА. run abandon ветвится по runs.finished_at и на живом прогоне уходит в живую ветку, а AbandonRun отказывает над попыткой, ещё называющей юнит. Ручка «процесса нет, закрой деньги» — НОВАЯ разрушительная операторская поверхность над деньгами, и её дизайн (кто вправе звать, чем доказывается отсутствие процесса, что будет, если процесс вернётся) — решение своего размера, а не хвост этой правки. Сегодня пользователь выводит прогон из состояния кнопкой Stop, и теперь об этом хотя бы говорят все три поверхности PD-385. ⚠ РУЧКА ПОСТРОЕНА паком «закрыть цикл» (04.09) — строка СУЖЕНА до своего остатка, не закрыта. Форма: та же команда tmplatformctl run abandon, потому что рантбук уже посылает оператора именно к ней; новой команды нет. Кто вправе звать — оператор из CLI и только оттуда: терминальный вердикт над деньгами на HTTP-поверхность не выходит, как и run unquarantine. Чем доказывается отсутствие процесса — систему СПРАШИВАЮТ, а не верят ей на слово: команда сама зовёт runner.Alive по имени юнита стоящей попытки, и ответов ЧЕТЫРЕ, а не два — «нет» пропускает, «есть» отказывает, недостижимая шина отказывает тоже (отсутствие ответа не есть отсутствие процесса), и — четвёртый, найденный адверсариальным проходом уже по готовой работе — «это НЕ ТОТ systemd» отказывает тоже: systemctl --user отвечает про менеджер СПРАШИВАЮЩЕГО, а прогоны живут у пользователя демона, и там юнит, о котором чужой менеджер не слышал, неотличим от кончившегося (замерено: ActiveState=inactive, выход 0). Различитель — владелец TM_PLATFORM_STATE_DIR, ⚠ поэтому переменная деплоя обязана быть экспортирована в оболочке оператора, иначе команда ОТКАЖЕТ (рантбук об этом теперь говорит). Пины: cmd/tmplatformctl/abandon_proof_test.go — четыре ответа различителя, три отказа и путь «gone» через саму команду, плюс отказ над ЖИВЫМ транзиентным юнитом под systemd-гейтом. Второе условие проверяет хранилище ВНУТРИ транзакции: reconcile_failures >= abandonAfter, тот же пол, что у settling-ветви, — видеть и иметь право уничтожить это разные разрешения. Деньги: попытка с базовой линией и отчитанной цифрой рассчитывается по РАЗНИЦЕ (та же арифметика, что печатает StalledRuns), холд закрывается как settled с ценой в леджере; попытку без базовой линии ценить нечем — холд возвращается ЦЕЛЫМ, довод settling-ветви, доехавший до живой половины. Что будет, если процесс вернётся — названо прямо, потому что это цена ручки: живой движок продолжит тратить против СВОЕГО книжного потолка, а холд аккаунта уже закрыт, значит эту трату не оплатит никто и провайдеру платит ДЕПЛОЙ. Она ограничена (потолком этого же прогона) и не эксплуатируема аккаунтом: попасть в состояние можно только через прогон, который собственный реконсилятор деплоя не смог закончить. Тем же касанием закрыта долговечная половина PD-418. Пины: internal/runs/abandon_orphan_test.go — отказ без доказательства · отказ ниже пола · вердикт orphan со списанием по отчёту · целый холд там, где цены нет · осиротевшая попытка ЖИВОГО прогона. ⚠ ОСТАТОК, ради которого строка остаётся открытой: отсутствие процесса доказывается ОДИН раз, в момент команды, и между этим ответом и коммитом транзакции есть окно, в которое юнит может подняться заново. Окно узкое, его цена названа выше, но оно есть, и закрыть его может только фактически транзакционная проба — арбитр в хранилище, а не ещё одна проверка в CLI. open приёмка оркестратора №19 по паку P11 (охотник вне карты, воспроизведено 10 проходами)
PD-440 bug major два из трёх адресов УМЕРЛИ вместе с лечением, и это пере-написано, а не пере-нацелено: DefaultPerChapter и BookRunContext.ChaptersLeft удалены паком формы заказа. Живой носитель лечения — internal/pricing/pricing.go, греп func (m Model) Hold; аргумент потолка по-прежнему internal/runs/spawn.go, греп func (m meter) bookCap; движковая половина — backend/internal/pipeline/stagerun.go (резерв под шаг при waves.workers параллельных вызовах) КНИГУ ЧЕРЕЗ API НЕЛЬЗЯ ДОЧИТАТЬ НИКАКИМИ ДЕНЬГАМИ — покупка перестаёт покупать. Прирост книжного потолка за прогон равен chaptersLeft × DefaultPerChapter, то есть максимум chaptersLeft × $0.03. Движок перед редакторским шагом РЕЗЕРВИРУЕТ оценку на каждый параллельный вызов (waves.workers, на стенде 4 × ≈$0.069 = ≈$0.28) и останавливается на ПЕРВОЙ отказанной резервации, не дожидаясь уже летящих. Как только прирост становится меньше стоимости одного шага волны, каждый следующий прогон встаёт МГНОВЕННО и не продвигает книгу ни на юнит — а прирост только УБЫВАЕТ, потому что chaptersLeft уменьшается с каждой дочитанной главой. ⚠ Воспроизведено живьём дважды подряд, платным прогоном 04.09 (книга bk_SS5VES2JELESJSTR, 10 глав, после первой готовой главы chaptersLeft = 9, прирост $0.27): прогоны run_CR2RN76NAKYKAJE5 и run_VSXPMJE53KJQQAOSоба paused/credit_exhausted за 1015 секунд, committed не сдвинулся ни на микро-доллар (278319 до и после), холд вернулся целиком. Движковая строка обоих: book USD ceiling reached ($0.548319) (committed=$0.278319 reserved=$0.207805, denied estimate=$0.069828) … reserve ceiling reached. Это ПРОДУКТОВЫЙ СТОП, а не «шкала короче»: занижение ставки ×4.47 (D39.179 п.1) до сих пор читалось как неудобство, и вот порог, за которым оно становится недостижимостью результата. ⚠ Денег сам стоп НЕ жжёт — отменённые вызовы этих двух прогонов не успели уйти (латентность 20 и 31 мс, model_actual пуст, 0 токенов); неучтённый расход — отдельная строка PD-441. ⚠ Чинится СОГЛАСОВАННО, одной стороной нельзя: поднять ставку — платформенная половина и она меняет всю шкалу покупки; не бросать летящие вызовы и/или считать резерв по фактической конкуренции — движковая. Проверка без Go: sqlite3 -header -column "file:<project.db>?mode=ro" "select trace_id, count(*), round(sum(cost_usd),6) from request_log group by trace_id;"РАДИУС ПЕРЕ-СНЯТ 04.09 ЗАМЕРОМ ЗА $0 (заказ оркестратора №22): дефект НЕ про хвост длинной книги, а про ПОРОГ, ниже которого книга не переводится ВООБЩЕ. Порог считается из измеренного, а не из оценки: движок в собственном отказе назвал резерв ОДНОГО редакторского вызова до шестого знака — committed=$0.278319 reserved=$0.207805, denied estimate=$0.069828, ch5/chunk0/edit при потолке $0.548319. Отсюда волна из четырёх стоит $0.277633 — наблюдённая величина: три реально стоявших резерва ($0.207805) плюс отказанный ($0.069828). ⚠ Не 4 × $0.069828 = $0.279312: первая редакция ряда взяла именно это произведение, а оно СКОНСТРУИРОВАНО — вызовы волны не равны между собой, промпты чанков разной длины, и 3 × $0.069828 = $0.209484 расходится с наблюдёнными $0.207805 на $0.001679 (средний реальный резерв $0.069268 против отказанного $0.069828). Произведение оставлено рядом как ВЕРХНЯЯ оценка, и держать оба стоит: порог считается по обоим одинаково — $0.277633 / $0.03 = 9.25 и $0.279312 / $0.03 = 9.31, — то есть вывод не зависит от выбора числа. ⚠ Счёт «три» выведен, а не напечатан: он следует из waves.workers: 4 минус отказанный, и подтверждается отношением $0.207805 / $0.069828 = 2.976. Порог — chaptersLeft × $0.03 ≥ стоимости волны, то есть книга проходит только с 10 непереведённых глав и больше. Поправка числа — оркестратора №22, 04.09. Следствия: последние ДЕВЯТЬ глав любой книги недостижимы, и книга из ≤9 глав не переводится никогда — ни первой покупкой, ни повторными, потому что потолок КУМУЛЯТИВНЫЙ (bookCap = committed + increment, internal/runs/spawn.go:212-214) и запас каждого прогона равен ровно инкременту, сколько бы книга ни потратила раньше. ⚠ Предъявлено живьём и бесплатно: пятиглавая книга bk_P5UCXKHDO4HFWSH3 заведена через настоящий интейк ($0 — manifest без ключей), и GET /v0/books/{id}/run-options вернул max_chapters: 5, то есть максимум, который пользователь вообще может купить этой книге, — $0.15 против $0.279312 за волну; для книги пака после первой дочитанной главы тот же вызов вернул max_chapters: 9 ($0.27). Разрезка короткой книги ПОБАЙТНО та же (юниты по главам 2·1·1·2·1 против тех же 2·1·1·2·1 у первых пяти глав длинной), то есть короткая книга содержит ровно тот кусок ch5/chunk0, чей резерв измерен. ⚠ ЧЕГО В ЗАМЕРЕ НЕТ, называю прямо: сама короткая книга НЕ запускалась — её прогон способен потратить до $0.15 на черновую волну, а санкция владельца на платные прогоны закрылась вместе с паком; арифметика замкнута, живой прогон короткой книги остаётся неснятым. ⚠ ЛЕЧЕНИЕ В ДЕРЕВЕ ПАКА «ФОРМА ЗАКАЗА» (05.09), статус флипает ЛЕНДИНГ. Константа DefaultPerChapter удалена целиком: цена берётся из проекции движка (manifest --json: expected_usd по главам, step_max_usd), а холд считается k × ожидаемое(заказ) + step_maxаддитивно, не max(...), потому что последнему вызову заказа нужен запас под ЦЕЛУЮ резервацию сверх уже списанного. Пины: pricing.TestTheLastTwoChaptersOfABookAreBuyable (последние две главы и последняя глава покупаются), pricing.TestTheHoldIsTheCushionedBillPlusOneWholeReservation. ⚠ И вторая половина той же стены, найденная этим же паком: книжный expected_usd ВКЛЮЧАЕТ book_once_usd — плоские $2.00 на боевом pipeline-c1 независимо от длины книги, — поэтому в основу холда он НЕ кладётся (иначе пятиглавая книга за $0.25 снова непокупаема); пин pricing.TestAFlatBookLevelBoundDoesNotPutATwoDollarThresholdUnderEveryPurchase. fixed(628cc56) платный сквозной прогон через API, пак «закрыть цикл» 04.09 (воспроизведено дважды)
PD-441 bug major, деньги движковая половина — backend/internal/pipeline/stagerun.go, ветвь с комментарием No 2xx ever arrived: nothing was billed (releaseReservation + строка request_log с нулевой ценой); платформенная — internal/runs/reconcile.go settleOne, который берёт цифру движка как истину ЛЕДЖЕР ДЕНЕГ — НИЖНЯЯ ГРАНИЦА, и потолок сам её создаёт: halt отменяет вызовы, которые УЖЕ УШЛИ к провайдеру, и их цена не записывается никуда. Замерено 04.09 на run_FS3O4UO5EDTKVF42: волна пустила четыре редакторских вызова, один дошёл (6846+13729 токенов, $0.063404, 156444 мс) и своим коммитом сорвал книжный потолок, а три оставшихся были отменены В ТОТ ЖЕ МИГ, отработав 120756, 156469 и 156470 мсс cost_usd 0, нулевыми токенами и пустым model_actual. Вызов, проживший 22.6 минуты, до провайдера дошёл почти наверняка. Порядок величин: соседние ЗАВЕРШЁННЫЕ вызовы того же прогона стоили $0.017409 и $0.063404 ⇒ незаписанное — порядка $0.050.19 на ОДНО срабатывание потолка против $0.278319 за всю книгу. То есть механизм, который защищает от перерасхода, сам создаёт неучтённый расход до двух третей стоимости книги за одно срабатывание. ⚠ Дыра не в принципе, а в ПОКРЫТИИ, и это делает строку заказом, а не наблюдением. Консервативная оценка в движке уже построена и ратифицирована (stagerun.go, «paid 2xx with zero usage; settling the reservation estimate to keep the ceiling honest», pack-13 point-9): проект уже принял правило «нельзя ослеплять потолок нулём там, где вызов был платным». Но условие требует respОТВЕТА. Вызов, отменённый в полёте, ответа не получает, уходит другой веткой, и та честно пишет «nothing was billed» — что верно для транспортного отказа, не выпустившего запрос, и НЕВЕРНО для отмены на 156-й секунде. Направление лечения (именно направление): сеттлить оценку и при отмене вызова, который успел уйти, различая по ФАКТУ ухода; латентность — кандидат-различитель (20 мс против 156 000 мс разводит два случая без всякого рассуждения), но порог обязан быть обоснован замером, а не назначен. ⚠ Списание провайдером НЕ ДОКАЗАНО: биллинга провайдера у зоны нет, утверждается ровно наблюдаемое — вызовы шли минутами и их цены в леджере нет. ⚠ Правка stagerun.go — ЧУЖАЯ зона, отсюда только строка. Смежная строка единого бэклога — 78. Воспроизведение: sqlite3 -header -column "file:<project.db>?mode=ro" "select id, ts, stage, role, latency_ms, cost_usd, err from request_log where err='context canceled' and latency_ms > 1000;" open платный сквозной прогон через API, пак «закрыть цикл» 04.09 (усилено разбором оркестратора №22 по коду движка)
PD-438 bug major internal/ingest/tail.go apply (греп ErrForeignStreamAhead), internal/ingest/tail.go tailFrom (греп mine := want), internal/runs/reconcile.go drainJournal Владение потоком не переживало проход свипа, и чужие события ложились на оплаченную попытку. mine — возвращаемое значение, а не колонка: следующий проход выводит его заново из pos.LastSeq > 0. Пока тейлер ПРОХОДИЛ мимо чужого handshake'а, двигая байтовый хинт, следующий проход читал чужую область с курсором, который говорит «это наше»: чужой ceiling ставил paused живому оплаченному прогону, unit_done писал главы, которых никто не покупал, а чужой seq, попавший на наш, давал ErrPayloadConflict и карантинил здоровую проекцию. ⚠ Воспроизведено в двух вариантах, до и после правки P13 (копия дерева, Tail дважды — форма, в которой его зовёт drainJournal): на HEAD 494c4ef законный чужой hello травит проекцию (pass 2 applied a FOREIGN event (seq [2])), а чужой мажор карантинит; после пере-упорядочивания пункта 3 P13 ОБА травят — то есть заказанная правка расширила старый класс с законных чужих строк на любые. Конфликт payload воспроизводится одинаково на обеих версиях. ⚠ Лечение в дереве пака P13 (03.09): чтение ОСТАНАВЛИВАЕТСЯ на чужом handshake'е, если платформа именовала поток попытки и наши строки уже применены (pos.LastSeq > 0), и курсор на нём остаётся — владение выводится заново каждый проход, ценой одного пере-чтения строки. Прежняя семантика «пройти мимо» сохранена там, где она безопасна: пока LastSeq == 0, наш handshake ещё может лежать ниже по файлу (пин TestAForeignStreamBeforeOursIsWalkedPastRatherThanStoppedOn), и у безымянной легаси-попытки (want == "") — пин TestAnotherAttemptsStreamInTheSameJournalIsSkipped зелёный. Пины: TestAForeignStreamDoesNotOwnOurAttemptOnTheNextPass (три формы чужого hello × три следующих строки). ⚠ ДОФИКС 03.09: обоснование «вред ограничен по времени» ОПРОВЕРГНУТО приёмкой исполнением, и оно было моё. Довод «чужой handshake ⇒ нашего процесса в книге уже нет» неверен: платформа отдаёт КАЖДОМУ спавну одной и той же попытки один и тот же id потока (internal/runs/spawn.go, греп engineStreamID(l.RunID, l.AttemptNo)), а движок, найдя, что этот id уже писал события книги, минтит СВЕЖИЙ и продолжает (backend/internal/pipeline/events.go, греп fresh := obs.NewTraceID()). Значит «чужой» hello может писать ЖИВОЙ процесс нашей же попытки, и прогон способен простоять припаркованным весь свой срок. Радиус сужен опровергателем приёмки: путь — конъюнкция двух признанных сбоев (претензия на юнит, отчитавшаяся ошибкой, плюс исчезновение юнита без exit-marker), а не рядовой рестарт. Наблюдаемость дана в том же дофиксе: тейлер сообщает о парковке отдельным сигналом (internal/ingest/tail.go, греп ErrForeignStreamAhead) вместо тихого nil; свип пишет WARN с прогоном, попыткой и смещением; и парковка теперь СТОИТ РЯДОМ с карантином в условии канала починки (maybeResync, греп !l.Quarantined && !parked), то есть у припаркованной попытки снова есть свежесть через tmctl status. Пины: TestTheParkIsReportedSoTheCallerCanTellItFromBeingCaughtUp (без DSN), TestAParkedAttemptStillGetsTheRepairChannel (DSN). ⚠ Чего по-прежнему НЕТ: строки в runs, гейджа и колонки — парковка живёт длину одного прохода и в БД не пишется; дать ей строку значит завести колонку, то есть миграцию, а она этим нарядом не заказана ⚠ ОСТАТОК НАЗВАН 04.09 приёмкой: лечение в дереве и заленджено (6ae3e76), но ПРИПАРКОВАННАЯ попытка не видна ни в runs, ни гейджем, ни колонкой — у этого класса нет ни одного из трёх сигналов, которые были у карантина, а maybeResync до дофикса такую попытку пропускал. Дофикс дал ей имя (ErrForeignStreamAhead), WARN с троттлингом и вход в ресинк; поверхностная видимость для ОПЕРАТОРА остаётся открытой и идёт райдером платформенного пака. ⚠ ДИСПОЗИЦИЯ ПАКА «ЗАКРЫТЬ ЦИКЛ» (04.09): ОТЛОЖЕНО СОЗНАТЕЛЬНО, и вот довод, а не отсутствие времени. Все три недостающих сигнала — строка в runs, гейдж, колонка — читают ХРАНИЛИЩЕ, а парковка в хранилище не пишется: она живёт длину одного прохода и выводится заново каждым свипом. Дать ей любой из трёх значит завести колонку на run_attempts, то есть миграцию и новое состояние, которое надо СНИМАТЬ, — а снимать его некому, кроме того же свипа, который его поставил. Это не райдер, а отдельная работа со своим дизайном («парковка как СОСТОЯНИЕ, а не как исход прохода»), и сделать её заодно с дверью выдачи значило бы решить её мимоходом. Что у класса ЕСТЬ сегодня: имя (ErrForeignStreamAhead), WARN с троттлингом и вход в канал починки — оператор, который смотрит в ЖУРНАЛ, парковку видит; невидима она тому, кто смотрит только в таблицу. Строка остаётся открытой ровно на этом. open самопроверка пака P13 (веер, линза шва; воспроизведено сессией на копии)

Открытые — minor

ID Класс Серьёзность Где Суть Статус Источник
PD-452 bug info backend/cmd/tmctl/backup.go:29=func backupDirFor(dbPath string) string, backend/cmd/tmctl/backup.go preflightBackup, контраст: удаляющего кода нет НИ В ОДНОЙ зоне ПРЕДРЕЙСОВЫЕ ТОЧКИ ДВИЖКА В КАТАЛОГЕ КНИГИ РАСТУТ БЕЗ ПРЕДЕЛА: их не чистит никто. Гард движка снимает ПОЛНУЮ копию проектной базы (VACUUM INTO) перед КАЖДЫМ платным прогоном и перед каждой tmctl migrate, в <каталог книги>/backups/. Проверено грепом по обеим зонам: писатели — backupCmd, preflightBackup и migrate, удаляющего вхождения нет (grep -rn backupDirFor backend/ platform/ --include=*.go и grep -rn '"backups"' backend/ platform/ --include=*.go — только записи). ⇒ книга, купленная главами, накапливает по копии базы на главу, и на большой библиотеке это кончается диском — тем самым, потерю которого точки восстановления и должны переживать. ⚠ Точки платформы это НЕ лечат и не заменяют: они снимаются по расписанию и отвечают на «диск пропал», а предрейсовые — на «откатить вот этот прогон»; поэтому платформа их не копирует (иначе рост квадратичный) и не удаляет (чужой каталог по замыслу движка — «the durable backup directory for a book»). Чья половина: политика удержания предрейсовых точек не принята НИ ОДНОЙ зоной; операторский обход назван в рантбуке (find … -mtime +30), но обход в порядок работы записывать нельзя. Ряд заведён, чтобы решение было принято, а не унаследовано open пак операций 05.09, самопроверка построенного (первая редакция моего же доккомментария утверждала, что копии платформы предрейсовые «замещают» — неверно)
PD-453 bug info internal/backup/backup.go isLiveDatabase (правило по суффиксу), контраст: backend/internal/config/book.go (греп b.ProjectDB =) — project_db свободная строка, а не конвенция ТОЧКА ВОССТАНОВЛЕНИЯ УЗНАЁТ ЖИВУЮ БАЗУ ПО СУФФИКСУ .db, А НЕ ПО ИМЕНИ, КОТОРОЕ ЕЙ ДАЛ ОПЕРАТОР. Платформе запрещено выводить путь проектной базы движка (закон шва п.1), поэтому копир формулирует правило «чего НЕ копировать». Два следствия для книги, чей project_db назван иначе: (а) имя без .db (project.sqlite) — живой, возможно рваный файл ложится в точку РЯДОМ с движковым снимком project.db, и у восстанавливающего оператора два кандидата вместо одного (потери нет, есть неоднозначность; шаг 4 рантбука велит смотреть в book.yaml, но именно в этом случае в каталоге лежит файл с «правильным» именем и соблазн mv пропустить); (б) путь ВНЕ каталога книги — runner.backupPathIn откажет по гарду, книга станет ДЫРОЙ (complete: false, громко, с 05.09), но копия движка при этом уже написана в свой backups/ и никем не удаляется. ⚠ Законный канал закрыть это ЕСТЬ и он не использован: движок публикует project_db в конверте StatusArtifacts (status --json/manifest --json; закон шва перечисляет его прямо), платформа декодирует из конверта только bank_export (internal/ingest/manifest.go). Не взято в паке операций сознательно: чтение конверта — это лишний status/manifest НА КНИГУ ЗА ПРОХОД, а он по построению пере-ингестит книгу («status дорог по построению»), то есть цена — удвоение самой дорогой части бэкапа ради случая, которого сегодняшний шаблон деплоя не создаёт. Решать паку, который либо удешевит конверт, либо примет цену open пак операций 05.09, самопроверка построенного
PD-450 bug minor internal/httpapi/exports.go:212=func (h *v0) exportName (имя файла берётся из титула КНИГИ платформы) против backend/internal/bookfile/epub.go:68=xmlText(strings.TrimSpace(b.Title)) (титул берётся из book.yaml); ручка — internal/httpapi/v0.go updateBook ПОСЛЕ ПЕРЕИМЕНОВАНИЯ ИМЯ ФАЙЛА И ИМЯ ВНУТРИ ФАЙЛА РАСХОДЯТСЯ: пользователь скачивает Мастер Гу.epub, открывает — и читалка показывает старое имя. Замерено живым прогоном 05.09 в прод-конфигурации: PATCH /v0/books/{id} {"title":"Мастер Гу"}Content-Disposition: … filename*=UTF-8''%D0%9C…%D0%93%D1%83.epub, а dc:title в скачанном архиве = P12 verify-bank probe. ⚠ Половина платформы сделана и это ВСЁ, что она может сделать законно: канон §updateBook объявляет титул DISPLAY-полем, которое «reaches nothing else — not the translation, whose configuration is written once at intake and never rewritten», а движок сворачивает title в BriefHash (backend/internal/config/book.go, греп Title is part of the brief) → в снапшот → в каждый request-hash, поэтому запись титула в book.yaml отказала бы следующему прогону недопереведённой книги и стоила бы ПЕРЕ-ОПЛАТЫ всей книги. Лечение — в зоне движка и уже заказано решением владельца 05.09: строка бэклога 284 (титул и заголовки глав переводятся отдельной стадией с подписью владельца). Пока она открыта — ряд открыт open пак операций 05.09, замер живым прогоном
PD-449 doc minor internal/runner/backup.go:126=func backupPathIn (разбор строки), backend/cmd/tmctl/backup.go backupCmd (backup OK: %s (integrity_check green, VACUUM INTO)), пин — internal/runner/backup_live_test.go TestTheRealEngineNamesItsRestorePointInTheLineThisPlatformParses ЕДИНСТВЕННЫЙ КАНАЛ ШВА БЕЗ ВЕРСИИ: путь точки восстановления читается из ЧЕЛОВЕЧЕСКОЙ строки stdout движка. У tmctl backup нет ни --out, ни --json, а каталог он выбирает сам (<project_db>/../backups/), поэтому законных вариантов ровно два — разобрать его прозу или ВЫВЕСТИ путь самой, что запрещено законом шва п.1 («никогда не выводит путь сама») и что зона уже однажды откатывала (internal/runner/artifacts.go, «The path is taken rather than derived»). Выбран разбор, СТРОГИЙ (незнакомая строка = ошибка, не догадка) и с гардом «путь внутри каталога книги»; закон шва п.3 при этом требует версии у каждого документа глагола, и её тут нет. Лечение — движковое: --out (тогда путь выбирает платформа и разбор не нужен вовсе) либо версионированный JSON-выход. Цена бездействия сегодня НУЛЕВАЯ: живой пин ловит смену формы на первой же батарее с движковым гейтом open пак операций 05.09, самопроверка построенного
PD-451 hardening info cmd/tmplatformd/runner.go (греп a PostgreSQL tool the backup needs), internal/backup/backup.go dumpPostgres Бут НАЗЫВАЕТ правило «мажор pg_dump = мажор сервера», но не СВЕРЯЕТ иху демона есть соединение с базой и он мог бы. Сегодня несовпадение ловит первый же пасс (он идёт на буте) строкой pg_dump: error: aborting because of server version mismatch, то есть громко и сразу, плюс гейдж tm_platform_backup_age_seconds остаётся +Inf; но диагноз оператор ставит по чужому тексту, а не по нашему. Замерено исполнением рецепта в чистом контейнере 05.09: postgresql-client дистрибутива дал мажор 15 против сервера 18. Лечение — сверка server_version_num с pg_dump --version на буте open пак операций 05.09, исполнение рантбука в контейнере
PD-448 bug minor internal/runs/runs.go:170=ErrNotResumable (отказ, который получает проигравший) и internal/runs/control_test.go:846 TestTwoResumesOfOneRunTakeOneHoldAndBothAnswer (пин гарантии) ДВОЙНОЙ КЛИК ПО «ПРОДОЛЖИТЬ»: проигравший гонку получает ОТКАЗ вместо прогона, когда два вызова не успевают пройти проверку состояния в одном окне. Гарантия зоны сформулирована в шапке самого теста: «Both calls pass the state check together and race for attempt N+1 on the unique index; the loser must answer with the run the winner re-opened, not with a not-found». Замер 05.09: под нагрузкой машины (load average 4248 на 8 ядрах, тринадцать чужих tmmutate) вызовы СЕРИАЛИЗУЮТСЯ — победитель успевает перевести прогон в translating до того, как проигравший дойдёт до своей проверки, — и проигравший получает runs: the run cannot be continued: it is translating (control_test.go:863). ⚠ Пользовательский смысл: человек, дважды нажавший «продолжить», видит ошибку, хотя прогон в этот момент СТАРТОВАЛ.Деньги не затронуты: отказ приходит ДО взятия холда, так что второй холд не берётся — та половина гарантии («ровно один холд») держится и на отказном пути. ⚠ Это НЕ PD-420: тот ряд про другой тест (internal/pgstore/runs_test.go TestAClaimThatLostARaceToAReleaseIsRetriedAndNotAnError); проверено грепом, имени этого теста в реестре не было ни в одном ряду. Воспроизведение: полный пакет go test ./internal/runs/ при загруженной машине — упал 2 раза из 2; ИЗОЛИРОВАННО (-run TestTwoResumesOfOneRunTakeOneHoldAndBothAnswer -count=5) — 5/5 ok за 10 с. То есть окно существует и открывается голоданием по процессору, а не правкой кода. Направление лечения (не решение): проигравшему различать «прогон уже идёт, потому что его только что открыл параллельный резюм» от «прогон идёт сам по себе» — первое обязано вернуть прогон, второе законно отказывает. open замер зоны 05.09 при ревизии документации: батарея краснела дважды, разобрано до конкретного теста
PD-443 bug minor internal/exports/exports.go Build/Sweep, internal/pgstore/queries/exports.sql, миграция 00032_exports.sql (on delete cascade) Артефакт экспорта может пережить СТРОКУ, которая одна умеет его найти, и тогда его не удалит никто. Путь артефакта детерминирован (<Dir>/<bookID>/<exportID>.<format>), но каталог со строками не сверяет ничто: GC удаляет только те пути, которые НЕСУТ строки. Три живых пути, найдены адверсариальным проходом по готовой работе: (а) демон убит между rename движка и FinishExport — путь в строку так и не попал, свип пере-водит её в failed, файл остаётся навсегда (не экзотика: KillMode=mixed в юните и дренаж очереди на 10 с делают это штатным исходом рестарта под нагрузкой); (б) книга удалена — on delete cascade сносит строки, файлы и каталог книги под Dir не трогает никто (сегодня у DeleteBook вызывающих нет, но внешний ключ уже стоит и ловушка взведена); (в) артефакт, чей unlink не прошёл, — ⚠ ЭТОТ ПУТЬ ЗАКРЫТ ТЕМ ЖЕ ПАКОМ: path теперь переживает смену состояния и снимается только после реального удаления (UnlinkedExports/ForgetExportPath), то есть unlink стал ПОВТОРЯЕМЫМ; пин TestAFileTheSweepCouldNotRemoveKeepsItsRowPointingAtIt. Четвёртый путь — временные файлы движка (.<имя>.tmp-*) после SIGKILL — тоже закрыт (exports.Service.discard), потому что tmctl build контекста не читает и его всегда добивает SIGKILL. ⚠ Лечение (а) и (б) — одно и то же и это НЕ райдер: сверка каталога со строками, то есть реконсилятор файловой системы со своим дизайном (что считать сиротой, как отличить чужой файл от своего, что делать с каталогом книги, которой нет). Делать его заодно с дверью значило бы решить мимоходом. Радиус сегодня ограничен: TTL двери — сутки по умолчанию, файлы лежат под одним каталогом деплоя, и оператор видит рост диска раньше, чем что-либо ломается. ⚠ ДВА СИБЛИНГА, НАЗВАННЫЕ ПРИЁМКОЙ 04.09 — один закрыт, один остаётся здесь. (I) STAGING-файл движка (.<имя>.tmp-*) переживает смерть демона ПОСРЕДИ сборки: discard живёт в том же процессе, поэтому при kill -9 убирать его некому, а следующая сборка того же экспорта не случится — строка уже не pending. Это тот же класс, что (а), и лечится тем же реконсилятором каталога. (II) ⚠ ВТОРОЙ ВОРКЕР НА ОДНОЙ СБОРКЕ — ЗАКРЫТО ТЕМ ЖЕ ДОФИКСОМ, а не только названо: оба писали бы по ОДНОМУ пути (он выводится из id экспорта), и уборка проигравшего снесла бы файл, только что опубликованный выигравшим. Клейм сделан ИСКЛЮЧАЮЩИМ — where … and state = 'pending' and started_at is null, — поэтому второй воркер получает ErrExportSettled и до сборки не доходит. Сегодня недостижимо (River ведёт одно задание рода за раз), но именно это превращает вторую реплику из потерянного артефакта в дубль сборки. Пин — в TestAQueuedBuildAndAClaimedOneAreJudgedOnDifferentClocks. Воспроизведение (а): kill -9 демона между строкой лога export built и следующей записью в БД. open адверсариальный проход пака «закрыть цикл» по своей же работе, 04.09
PD-444 bug minor internal/httpapi/bank.go:142=Invalid(w, r) (ветка строгого разбора) против internal/httpapi/bank.go:146=Invalid(w, r, items...) (ветка валидации); механизм — internal/httpapi/problem.go:173=Errors []Item Дверь правок банка отвечает 400 invalid_request БЕЗ единого указателя на то, ЧТО не так. Замерено живым прогоном 04.09: две попытки с чужими именами членов (decisions вместо corrections) вернули голое тело, оба ответа Content-Length: 119 — ни errors[], ни имени члена, ни позиции. ⚠ Сам отказ ВЕРЕН и оспаривать его нечего: схема канона объявляет additionalProperties: false на обоих уровнях, и незнакомый член — это клиент, уверенный, что он что-то задал; молчаливая версия этого — правка, применённая наполовину. Дефект в другом: канон нигде не требует МОЛЧАТЬ о том, какой член виноват, а механизм у зоны уже построен и на соседней ветке применяется — validateCorrections возвращает []Item и отдаёт его в Invalid. Ветка строгого разбора теряет даже то, что у неё в руках: encoding/json называет поле в тексте ошибки (json: unknown field "decisions"), а обработчик его не только не отдаёт, но и не логирует — соседняя ветка «тело не доехало» логирует. Цена: клиент двери — редактор пользователя, и 400 без адреса отлаживается перебором. ⚠ Лечение НЕ трогает канон: errors[] в нём уже объявлен, довести до ответа нужно ветку разбора. open живой платный прогон пака «закрыть цикл», наблюдение H12, 04.09
PD-446 doc minor канон docs/architecture/14-api-contract/ §PausedReason; носители в зоне — internal/pgstore/books.go:1196=PausedCreditExhausted = ingest.PausedCreditExhausted и internal/httpapi/v0.go:1008=ContractHaltReason paused_reason: credit_exhausted при 34% НЕТРОНУТОГО баланса — слово называет кредит там, где исчерпан потолок ПРОГОНА. Замерено 04.09: в один и тот же момент прогон стоял с paused_reason: credit_exhausted, а GET /v0/usage отвечал state: ok, halt_reason: null при остатке 0.102494 из 0.30. ⚠ Оба ответа ВЕРНЫ, и счётная половина этого дефекта уже вылечена — пере-открывать её нельзя: halt читается с АККАУНТА, а не с паузы последнего прогона (ReadUsage, разбор в books.go над телом), и именно поэтому halt_reason здесь честно пуст. Остаётся ОДНО: у PausedReason и AccountHaltReason разные словари с одним и тем же единственным значением, и это слово — credit_exhausted. Пользователь, читающий «кредит исчерпан» рядом с «остаток 34%», получает противоречие, которого в фактах нет. ⚠ Зона правку канона своей рукой не делает: кандидат в состав минора — значение PausedReason обязано называть ПРОГОН (run_ceiling_reached), словарь аккаунта не трогается. Пока минор не принят, ряд держит вопрос открытым. ⚠ ЗАКРЫТО ДЕРЕВОМ ПАКА «ФОРМА ЗАКАЗА» (05.09), статус флипает ЛЕНДИНГ. У паузы прогона появилось СВОЁ слово: run_limit_reached (ingest.PausedRunLimitReached), и CeilingPause(ScopeBook) отдаёт теперь его, а не credit_exhausted. Аккаунтное слово осталось за аккаунтом (ReadUsage, balance <= 0). ⚠ Разделены и ДВА вердикта реконсилятора, которые делили одно слово: ceilingSpentrun_limit_reached, creditUnavailablecredit_exhausted (internal/runs/reconcile.go, греп if v == creditUnavailable) — лечения противоположны (купить снова против пополнить), и пользователь, которому сказали не то, идёт делать не то. Миграция 00033 пере-называет и старые строки. Пины: pgstore.TestTheCeilingScopeDecidesWhichPauseTheRunGets, runs.TestAnInterruptedRunWithNothingLeftIsPausedAndStaysPausedThroughAResume, runs.TestAnInterruptedRunThatTheBalanceCannotCarryIsPaused. fixed(628cc56) живой платный прогон пака «закрыть цикл», наблюдение H14, 04.09
PD-435 bug minor internal/pgstore/readmodel.go runDone/runTotal/runStage (читают монотонный editWave), internal/pgstore/runs.go StartRun (снятие базлайнов) Полоса прогона на деплое, где редактора УБРАЛИ, не доходит до единицы: знаменатель тарифицирует edit-волну, которой не будет. Вторая половина PD-403; первая (счёт книги) закрыта паком P12 эпохой формы конвейера, эта — нет, и попытка закрыть её тем же носителем была ОТКАЧЕНА тем же паком: полоса, переведённая на ПРИСВАИВАЕМУЮ эпоху, перестаёт быть монотонной — замерено, один прогон читал 4/4, затем 2/2 на своих же двух попытках при неизменной structure_version, что канон запрещает прямо (строка 200, «одна монотонная дробь на всю работу прогона»). ⚠ Почему это не однострочник: правильный носитель — форма, под которой работает ЭТОТ прогон, записанная на самом прогоне; а в StartRun она ещё НЕ ИЗВЕСТНА — движок объявляет её первым progress-событием прогона, и попытка снять её раньше это ровно PD-401, закрытый пином. Значит запись должна происходить на ПЕРВОМ объявлении прогона, и тогда нужен разбор, что делать со второй попыткой того же прогона, объявившей другую форму (сегодняшний ответ — ничего, потому что флаг монотонен). Лечение: колонка формы на runs, заполняемая первым объявлением, плюс решение о смене формы между попытками одного прогона. Пин обратной стороны уже стоит и покраснеет, если кто-то снова переведёт полосу на эпоху: pgstore.TestTheRunsBarIsMonotoneAcrossAShapeBoundaryItSpans. ⚠ Почему minor, когда родительская PD-403 была major — вопрос приёмки, отвечаю доводом, а не весом. У PD-403 мажорной её делали ДЕНЬГИ: шкала покупки продавала уже переведённые главы повторно. Эта половина ЗАКРЫТА — ChaptersLeft считается через эпоху, дважды не продаётся ничего, и PD-410 (тоже про деньги, ручка «купить N глав» ограничивает не главы, а доллары) остаётся major именно поэтому. Здесь не двигается ни один микро-доллар: прогон делает всю купленную работу, закрывается ready, леджер сходится, следующая покупка предлагает правильный остаток. Врёт ДРОБЬ и подпись (editing на деплое без редактора) — контрактно видимо и потому не info, но это отчёт о работе, а не её оплата. Плюс достижимость: нужна смена ФОРМЫ деплоя под книгой, которая уже прошла редактирующий пайплайн, — не обычный путь, в отличие от PD-410, который кусает на КАЖДОЙ покупке. Если приёмка сочтёт довод слабым — поднять до major дешевле, чем спорить: работа от веса не меняется open адверсариальный проход пака P12 (3031.08), замерено на живом Postgres через продовые пути записи
PD-433 hardening minor internal/pgstore/identity.go:106=if !errors.Is(err, errIdentityRace) (ретрай), :118=var errIdentityRace, ветвь on conflict … do nothing в upsertIdentityOnce Ветвь, обслуживающая ЛЕГИТИМНУЮ гонку двух первых логинов одной новой личности, не исполняется НИ ОДНИМ тестом. Форма та же, что у PD-380 и PD-86: единственная точка принуждения объявленного свойства, мутация переживает полную батарею, транзитивной страховки нет. Механика: под READ COMMITTED for update в LockIdentity не блокирует строку, которой ещё нет, поэтому оба гонщика проходят; проигравший видит created == 0, получает errIdentityRace и ретраит — и именно ретрай спасает его от 500 на пути логина. Снос ветви — только с доказательством недостижимости: она обслуживает легитимную гонку, и её снос = 500 на легитимном пути (класс PD-369, который этот же пак и закрывал). Диспозиция: запинить конкурентным тестом ЛИБО доказать недостижимость. Форма теста, если пинить: в пакете pgstore, ~40 раундов, в каждом СВЕЖАЯ личность и 4 горутины на UpsertIdentity; утверждать, что все четыре вернули nil, что userID у всех ОДИН, что identities держит одну строку, что грант посева записан ОДИН раз и что число строк в users равно числу раундов — последнее ловит настоящую утечку, осиротевшего пользователя от проигравшего, который успел CreateUser до проигрыша. Проверять посадкой (снять арм errIdentityRace — тест обязан покраснеть), потому что вероятностный тест без посадки не доказывает, что он вообще кусает open пак P12 (30.08), греп непокрытых ветвей по своим путям
PD-434 bug minor internal/pgstore/sink.go ApplyStatus, internal/runs/reconcile.go maybeResync Канал ПОЧИНКИ прогресса не чинит прогресс: ресинк не материализует ничего, что читает экран. maybeResync существует ровно для прогона, чей поток в карантине или чей курсор не двигался — «это теперь единственный источник», говорит его собственный комментарий. Но ApplyStatus писал пер-волновые цифры отчёта ТОЛЬКО в runs.draft_done/draft_total/edit_done/edit_total, у которых не было ни одного читателя (PD-411), и не трогает ни unit_resolutions, ни chaptersа вся полоса выведена из chapters. Значит полоса такого прогона стоит всю его жизнь, как бы исправно ни отвечал tmctl status. ⚠ Строка заведена паком P12 при сносе PD-411, и это главное в ней: мёртвые колонки ПРЯТАЛИ гап — код выглядел так, будто канал починки чинит, и комментарий maybeResync прямо утверждал «ApplyStatus now materializes the same four counters as the stream». Обе лгущие фразы исправлены, сам гап НЕ лечится сносом и вынесен сюда, чтобы снос не выдал себя за лечение. Лечение: либо ресинк складывает отчёт в chapters (и тогда нужен разбор, как он не спорит с потоком, который те же строки пишет из unit_resolutions), либо зона признаёт, что карантинный прогон полосы не показывает, и говорит об этом на поверхности. Пин формы «ресинк НЕ двигает счётчики глав» уже стоит — pgstore.TestTheResyncRecordsFreshnessAndShapeAndNoProgress — и он покраснеет, если канал научится материализовать: это указатель на строку, а не её лечение open пак P12 (30.08), вскрыто сносом PD-411
PD-375 bug minor internal/runs/runs.go, греп quote.Hold > acct.Balance, internal/httpapi/v0_test.go:359, internal/runs/control_test.go Верхнюю половину проверки ceiling_chapters не исполняет НИ ОДИН тест: снятие второго операнда in.CeilingChapters > bounds.Max проходит ВСЮ батарею. Собственный доккомментарий называет проверку несущей — «число, решающее СКОЛЬКО ДЕНЕГ резервируется, не может быть выбрано вызывающим односторонне», — и канон требует того же от сервера (max_chapters уже прижат к остатку, «a client MUST NOT clamp it again»). Единственный тест, называющий ErrCeilingOutOfBounds, это таблица соответствия ошибки коду 409 в httpapi/v0_test.go, которая Start не зовёт, а самая тесная фикстура просит 10 глав у книги, где их 100. Код сегодня ВЕРЕН; дефект в том, что править эту строку можно безнаказанно. Замерено на живом стенде: чистая сборка отвечает 409 ceiling_unavailable/bounds_moved, сборка с мутацией отдаёт 202 и открывает резервацию 24000000 микро на книге из ТРЁХ глав, а движку уходит --ceiling-usd 24.200000. Вес: рефутер сузил major → minor, потому что код верен и ни один сегодняшний клиент до вреда не доходит. Воспроизведение: docs/p8-review/axis1-money/a1-mut5-ceiling-bound.sh, готовый пин docs/p8-review/axis1-money/r1_ceiling_bound_test.go.txtНОСИТЕЛЬ УМЕР ВМЕСТЕ С ФОРМОЙ ЗАКАЗА (05.09), строка пере-написана ПО СУЩЕСТВУ, а не пере-нацелена. Проверки `in.CeilingChapters < bounds.Min > bounds.Maxбольше НЕТ: шкала в главах иpricing.Boundsудалены вместе с per-chapter константой (строка бэклога 280), заказ судится ценой —Quoteклампит объём остатком книги, аquote.Hold > acct.Balanceотбивает то, что баланс не несёт. **Класс дефекта — «несущую половину не исполняет ни один тест» — закрыт НОВЫМ пином, а не исчезновением кода:**TestAnOrderTheBalanceCannotCarryIsRefusedBeforeTheHold` держит обе половины (неподъёмный заказ отбит ДО холда и деньги не двинулись; заказ больше книги = вся книга, а не бОльшая резервация). Проверено посадкой: снятие отказа краснит этот тест ИМЕНЕМ.
PD-377 doc minor internal/pgstore/credits.go:162=The ceiling to hand the engine is the amount held, internal/runs/spawn.go bookCap, cmd/tmplatformctl/main.go balance Доккомментарий Hold на входе в денежный путь неверен ОБЕИМИ половинами с миграции 00011. Он обещает «the ceiling to hand the engine is the amount held; read it back with OpenReservations». Движку передаётся не сумма холда, а run_attempts.ceiling_arg_micro_usd = committed книги плюс прирост (spawn.go bookCap), и сама 00011 говорит это прямым текстом («It is NOT ceiling_micro_usd»); начиная со ВТОРОГО прогона книги числа расходятся тем сильнее, чем дороже книга — на стенде до 17 раз (60000 холда против 1060000 в аргументе). Вторая половина не работает даже механически: Reservation.Ceiling — поле, которое только сканируется и не читается никем, единственный потребитель OpenReservations печатает Amount/BookID/OpenedAt/EngineRunID. ⚠ Рефутер снял два из трёх исходных якорей: доккомментарии в применённых миграциях 00007/00009 зона не правит после лендинга (у всех 25 файлов миграций ровно по одному коммиту), а исправление уже лежит в следующем файле того же каталога. Остаётся Go-доккомментарий. Воспроизведение: docs/p8-review/axis1-money/a1-ceiling-column-doc.sql open ревью-пак P8-REVIEW, ось 1 (живой стенд, сужено рефутером до одного якоря)
PD-386 bug minor cmd/tmplatformd/runner.go:474=out := []sweepPass{{"runs", sweepBudget, s.runs.Sweep}} и четыре следующих прохода того же one(), cmd/tmplatformd/runner_test.go Такт свипа последователен, и бюджеты пяти проходов СКЛАДЫВАЮТСЯ: сумма объявленных — 37 минут при TM_PLATFORM_SWEEP_EVERY 15 секунд. Пак P8-FIX дал каждому проходу свой бюджет и закрыл «один проход съедает дедлайн другого», но проходы по-прежнему идут подряд в одной горутине: runs 2м, readmodel 10м, intake 21м, idempotency 2м, observe 2м. Ничто эту сумму не ограничивает и ничто её не пинит — у функции sweep нет ни одного теста (в пакете два теста, оба про другое), и посадка «фаза runs уходит из головы такта в хвост» пережила ПОЛНУЮ батарею. Замерено пробой на реальной функции: за 4 секунды при такте 100 мс фаза прогонов отработала 40 раз сама по себе и 9 раз позади прохода материализации. Следствия по оси: калибровки самого лечения заданы в проходах и минутах и молча растягиваются (StalledAfter 5 «неудач подряд» и бэкофф, про который комментарий обещает «about a quarter of an hour», превращаются в часы); обещание Stop «the reconciler re-issues the stop on its next pass» задерживается на ту же величину; телеметрия стоит ПОСЛЕДНЕЙ. ⚠ Рефутер сузил: 37 минут — сумма ОБЪЯВЛЕННЫХ бюджетов, а не достижимая длительность (idempotency это один индексированный DELETE, а проход интейка сам себя режет). Воспроизведение: docs/p8-review/axis3-queue/probe_sweep_serialisation_test.go.txt open ревью-пак P8-REVIEW, ось 3 (проба на реальной функции + посадка мутации, сужено рефутером)
PD-387 doc minor deploy/README.md:592=сколько всего может занять один проход свипа, cmd/tmplatformd/runner.go one, internal/config/config.go SweepBudget Рантбук называет TM_PLATFORM_SWEEP_BUDGET ручкой «одного прохода свипа» и не говорит, КАКОГО из пяти, — а два бюджета из пяти оператору недоступны вовсе. Один такт прогоняет ПЯТЬ проходов подряд, и только три берут sweepBudget; проход материализации берёт refreshSweepBudget 10 минут, проход интейка — intakeSweepBudget = jobs.JobTimeout + readmodel.MaterializeBudget + минута = 21 минута, и обе константы оператору недоступны вовсе. Собственный доккомментарий кода при этом ТОЧЕН («SweepBudget is what ONE pass of the RUN sweep may take») — расходится именно операторская проза, и расходится в разделе «Застрявшая работа: что оператор делает, когда свип не справляется», то есть там, где по ней и будут действовать. Родня PD-368: та про то, что поднимать надо пару, эта про то, что ручка не покрывает такт ⚠ Арифметика 37 минут пере-проверена и держится: 3×2 + 10 + 21. Воспроизведение — docs/p8-review/axis3-queue/probe_sweep_serialisation_test.go.txt (последовательность тактов) плюс sed -n '264,272p' deploy/README.md open ревью-пак P8-REVIEW, ось 3 (находка рефутера)
PD-389 hardening minor cmd/tmplatformd/runner.go:562=s.metrics.ObserveRunner(metrics.Runner{, cmd/tmplatformd/runner.go pass, internal/metrics/metrics_test.go TestTheRunnersStateIsExposedWithItsUnits Шов телеметрии не покрыт НИЧЕМ, и это доказуемо без прогона батареи: sweep и observe — неэкспортируемые функции пакета main, то есть из другого пакета их не может вызвать ни один тест в принципе, а единственный тест-файл каталога несёт два теста, оба про другое. Проверено тремя посадками, пережившими полную батарею: StalledRuns: o.StalledRuns в ноль (наблюдаемая половина закрытого BLOCKER PD-346), errors.Is(err, context.DeadlineExceeded) в false (sweep_unfinished_total больше не может вырасти — PD-351 со стороны ВЫЗЫВАЮЩЕГО, куда пин TestAPassThatRanOutOfTimeSaysSo по построению не достаёт), и перестановка queue_depth с live_runs. Дыра шире шва: пин формы, на который ссылается STACK_DECISIONS §24, задаёт литерал metrics.Runner из ШЕСТИ полей из восьми — StalledRuns и AbandonedSurfaces в него не входят, поэтому мутация внутри самого ObserveRunner тоже выживает. Пере-проверено координатором пака независимо: снятие m.stalledRuns.Set(...) и снятие m.abandonedSurfaces.Set(...) по отдельности проходят ПОЛНУЮ батарею (18 пакетов), при том что снятие соседнего инкремента sweepUnfinished тем же пином ловится. То есть операторская ручка, построенная паком P8-FIX в ответ на PD-169, не пиньётся ничем. Воспроизведение: docs/p8-review/mutations-full.log и docs/p8-review/axis4-metrics/60-mutations.sh open ревью-пак P8-REVIEW, ось 4 (посадки финдера, рефутера и координатора)
PD-390 hardening minor cmd/tmplatformd/runner.go:559=log.Warn("the control plane's own state could not be read", "err", err), internal/pgstore/observe.go Observe, internal/metrics/metrics.go ObserveRunner Гейджи замирают при отказе телеметрического чтения, и признака устаревания в экспозиции нет. STACK_DECISIONS §24 объявляет «значения снимает СВИП, а не скрейп», но у снятого значения нет ни отметки свежести, ни счётчика неудач: observe() при ошибке пишет один WARN и возвращается, НЕ тронув ни одного гейджа, а Prometheus такой ряд устаревшим не помечает — цель жива, ряд на месте, значение старое. Живой замер: при сломанном чтении и одновременно вылеченном мире экспозиция продолжала утверждать «1 застрявший прогон, 1 карантин, 1 живой прогон, холд возрастом 1201 с», тогда как в базе застрявших было 0; при этом sweep_duration_seconds_count рос по всем четырём проходам, то есть все «жив ли свип» сигналы оставались зелёными. Observe — ОДИН стейтмент на все восемь чисел, поэтому любая его поломка гасит все гейджи разом, а сам observe() вызывается ВНЕ pass(), поэтому своего ряда в sweep_duration_seconds у него нет и его отказ там не виден. Обратное направление хуже: процесс, у которого чтение не удалось НИ РАЗУ, отдаёт нули как здоровье, и /readyz с /healthz при этом зелёные. ⚠ Вторая половина того же корня, найденная рефутером: инстанс-ЧИТАТЕЛЬ (пустой TM_PLATFORM_ENGINE_BIN — объявленная форма деплоя) не запускает свип вовсе, observe() не зовётся ни разу, а метрики созданы раньше и регистрируют все восемь гейджей безусловно, поэтому реплика уверенно отвечает runs_stalled 0, queue_depth 0, oldest_open_hold_seconds 0 про контрол-плейн, который она не измеряет — и тут нет даже WARN-строки. Лечится дёшево: *_last_success_timestamp_seconds либо счётчик неудач наблюдения плюс проведение observe через тот же pass. Воспроизведение: docs/p8-review/axis4-metrics/30-stale-gauges.sh, r1-boot-with-blind-telemetry.sh, r6-read-replica-zeroes.sh open ревью-пак P8-REVIEW, ось 4 (живой замер, расширено рефутером)
PD-392 hardening minor internal/metrics/metrics.go:91=Namespace: namespace, Name: "quarantined_attempts", internal/metrics/metrics.go oldest_open_hold_seconds, cmd/tmplatformctl/runs.go listRuns Две метрики без ручки — тот самый класс, за который PD-169 стоял BLOCKER'ом, в двух других местах. Help гейджа карантина сам называет цену («Such a run keeps going and keeps spending»), но команды, называющей строку за этим числом, нет: grep -rn quarantine cmd/ не даёт ни одного хита, и в таблице runs колонки карантина тоже нет. У возраста холда ручка формально есть — balance --user, — но она требует идентификатор аккаунта, которого гейдж не даёт, а документированный случай самого гейджа («A hold outlives its run only when a settlement could not be made») — это холд ЗАКОНЧЕННОГО прогона, которого список не показывает по построению. Живая проба на состоянии, произведённом ШТАТНЫМ операторским сценарием (abandon застрявшего прогона): при tm_platform_oldest_open_hold_seconds 10813 команды отвечают «no run is live», «no run is failing to reconcile» и «no book has been given up on», а единственный путь к строке — psql, то есть ровно то, что эти числа заводились заменить. Глобального списка открытых холдов в CLI нет. Воспроизведение: docs/p8-review/axis4-metrics/50-gauges-without-a-handle.sh и r5-hold-without-a-handle.shОбщий корень с PD-385, и там же он взвешен: сужение ЭТОЙ строки опирается на операторскую поверхность, несостоятельность которой доказывает соседняя строка того же пака — круговое сужение разобрано в PD-385, поднятой до major ⚠ ПАК P13 03.09: половина про КАРАНТИН закрыта в дереве, строка сужается до возраста холда. grep -rn quarantine cmd/ теперь даёт хиты: tmplatformctl runs печатает колонку QUARANTINE с причиной, tmplatformctl run unquarantine --run <id> снимает её (PD-426). Ручки для oldest_open_hold_seconds по-прежнему нет — этот остаток и держит строку открытой. Статус — акт лендинга open ревью-пак P8-REVIEW, ось 4 (живая проба, подтверждено рефутером)
PD-101 bug minor internal/login/login.go:507 login_events.ip_prefix берётся из r.RemoteAddr, а в задуманном деплое перед сервисом стоит edge-прокси ⇒ префикс всегда сеть прокси. Журнал входов заведён как ответ на «откуда примерно я входил» — в шипуемой форме он систематически отвечает неверно. X-Forwarded-For/Forwarded нигде не читаются и доверенного прокси в конфиге нет (это правильный дефолт: доверять заголовку без edge нельзя) — значит решение про edge и про этот столбец принимается вместе ⚠ ПАК P8-REVIEW 24.08: якорь дрейфанул и носителей ДВА. internal/login/login.go:507 сегодня это h.mu.Lock() внутри checkIssuer; чтение адреса живёт на :527=ev.IPPrefix = ipPrefix(r.RemoteAddr), и второй, строкой не названный, — internal/login/dev.go:195 с тем же выражением. Суть верна open приёмка P2 (панель)
PD-102 doc minor internal/httpapi/serve.go:36-38 Доккоммент DefaultTimeouts утверждает, что «an upload extends its own deadline as it makes progress» — это НЕВЕРНО: ReadTimeout в net/http (Go 1.26.5, server.go:990 wholeReqDeadline = t0.Add(ReadTimeout)) выставляется один раз и по мере прихода байтов не продлевается. Комментарий несущий: он объясняет, почему Read короткий, и на нём будущая ручка загрузки книги (23 МБ по контракту) построит неверное ожидание — ей понадобится собственный дедлайн через ResponseController, а не «прогресс продлевает» open приёмка P2 (панель, сверено с исходником Go)
PD-103 hardening minor internal/auth/middleware.go:43,66 У обращений к БД на аутентифицированном пути (Lookup/Touch) нет собственного дедлайна — только голый r.Context(), а WriteTimeout у сервера отсутствует по проекту (SSE) и TimeoutHandler в цепочке нет. Зависший Postgres паркует хендлеры и ждущих в пуле, пока клиент сам не уйдёт. readyz свой таймаут получил (PD-14) — горячий путь нет open приёмка P2 (панель)
PD-115 standards minor docs/ENGINEERING_STANDARDS.md §2 Внешняя версионированная базовая линия объявлена ровно для ОДНОЙ оси — безопасности (ASVS 5.0 L2 + OWASP API Top-10 2023, с указанием глав). Отказоустойчивость, наблюдаемость и контракт-первичность описаны собственной прозой зоны без внешнего эталона, а конфигурация, релиз/откат, ёмкость и восстановление не описаны вовсе. Разница не теоретическая: PD-57 и PD-58 нашлись ИМЕННО сверкой кода с RFC 9700/9207 и NIST SP 800-63B — механизм работает там, где эталон есть, и не может сработать там, где его нет. Грепнуто на 08.08: метрик и трейсинга ноль (ни prometheus, ни otel, ни expvar, ни pprof), процедуры бэкапа/восстановления в deploy/README.md нет, SLO не заданы. Предложение зоны: §2 получает по эталону на ось (наблюдаемость, ops, конфигурация) — направление РАТИФИЦИРОВАНО 09.08 (оркестратор №15 по делегации владельца); носитель работы — эта строка, исполнение — своими паками ⚠ Уточнено паком P5: ось НАБЛЮДАЕМОСТИ эталон получила — практики именования Prometheus (базовые единицы, _total у счётчиков, единица не в лейбле) плюс «четыре золотых сигнала» на вопрос «что мерить», записано в STACK_DECISIONS §24 и пинится metrics.TestTheRunnersStateIsExposedWithItsUnits. Оси ops/конфигурация/восстановление эталона по-прежнему не имеют — строка открыта ими open (наблюдаемость закрыта P5; ops и конфигурация — нет) абстрактный вопрос владельца 08.08 + сессия P4
PD-157 bug minor internal/runs/spawn.go, cmd/tmplatformctl/runs.go book add ДНЕВНОЙ потолок книги --ceiling-usd не перекрывает, а платформа его не видит и не задаёт. Движок требует хотя бы один из book_usd/day_usd (backend/internal/config/book.go:332=book_usd/day_usd (⚠ пере-нацелен ВТОРОЙ раз, 04.09: цель уехала с :321; первая попытка того же дня промахнулась на строку — условие на Go-поля стоит на :331, а процитированные литералы на :332), Р7 — ⚠ якорь пере-нацелен оркестратором №19 при лендинге 27.08 с :250: требование уехало на 321 из-за лендинга бэкенда d1eb8a9, не из-за правки платформы), флаг переопределяет только книжный (D39.122 прямо: «День-потолок не перекрывается»), а book.yaml пишет ОПЕРАТОР — платформа его не правит (D39.110 §2b) и в book add только проверяет наличие файла. Значит книга с низким day_usd останавливает прогон на лимите, которого платформа не выбирала: движок выходит кодом 1 (тот же путь, что у PD-113), прогон приезжает failed, а деньги пользователя целы и он не понимает, почему. В status --json дневной фигуры нет вовсе (есть book_ceiling_usd/ceiling_pct), поэтому даже диагностировать это платформа сегодня не может. Заведено, не построено: закрывать — либо проверкой day_usd при заведении книги, либо словом контракта о том, кто владеет потолками book.yaml у книг под платформой ⚠ ПОЛОВИНА ЗАКРЫТА (P6): дневной потолок стал РАЗЛИЧИМ. Поток несёт ceiling.scope со значениями book и day (D39.131), платформа хранит внутреннюю причину daily_ceiling (миграция 00015) и НЕ проецирует её как credit_exhausted — иначе экран сказал бы «кончились деньги» об аккаунте, на котором деньги есть, и зажёгся бы аккаунт-флаг ReadUsage. Резюм такого прогона отвечает 409 с диагностикой, а не гоняет попытки в цикл: day_usd живёт в book.yaml оператора, платформа его не ставит и поднять не может, а граница дня принадлежит ледджеру ДВИЖКА — таймер здесь был бы догадкой, которая тратит спавны. ОСТАЁТСЯ открытым то, ради чего строка заведена: платформа по-прежнему не выбирает и не видит day_usdstatus --json его нет), поэтому книга с низким дневным потолком остановится на лимите, которого никто на этой стороне не назначал. Пины: pgstore.TestTheCeilingScopeDecidesWhichPauseTheRunGets · internal/runs/seam_test.go:146=func TestAResumeOfARunPausedAtALimitIsRefusedWhoseverLimitItWas (переименование, коммит 9b23e8c) · httpapi.TestOnlyTheContractsOwnPausedReasonReachesTheWire ⚠ Дополнено рефутером: контракт 0.3.0 РАСШИРИЛ свойство — резюм отбивается ErrCeilingReached на ЛЮБУЮ паузу, а различение day против credit пинится не этим тестом, а pgstore.TestTheCeilingScopeDecidesWhichPauseTheRunGets и httpapi.TestOnlyTheContractsOwnPausedReasonReachesTheWire. Посадка мутации (снят ErrCeilingReached в ветке paused) роняет пин под НОВЫМ именем — свойство держится open собственная сверка шва при F1 (чтение движка + D39.122)
PD-162 bug minor internal/runs/spawn.go journalSize, internal/runs/reconcile.go Книга, чей каталог удалён или перемещён, принимает прогон и заклинивает его навсегда с открытым холдом. journalSize мапит ENOENT в «ноль, ошибки нет» (законно для первого прогона), поэтому Start отдаёт 202 и берёт холд; дальше bookMeter вечно падает (спавн отказывает), либо расчёт вечно откладывается — ни один путь не приходит к терминальному состоянию: прогон вечно translating, деньги вечно в холде, пользователю видно только «идёт». Дизайн «холд лучше догадки» осознан (строка 136), но отсутствие И валидации каталога на старте, И эскалации после N неудач — дыра. Закрывать вместе с эскроу/uncertain либо проверкой каталога при допуске. ⚠ Ре-чек V2 расширил КЛАСС строки: обязательность reserved_usd даёт тот же клин без всякого удаления каталога — прогон, запиненный к СТАРОЙ сборке движка (строка 139), у которой поля ещё нет, вечно отказывает спавну с открытым холдом. Лечится тем же терминальным состоянием после N неудач; отказ сам по себе верен (без цифры потолок считать нечем) ⚠ Сужено паком P5 с одной стороны и НЕ закрыто с другой: у ИНТЕЙКА терминальное состояние после N неудач теперь есть (books.parseAttempts = 5 → rejected с причиной parser_unavailable), и прогон на книге, не прошедшей интейк, отвергается до денег (runs.ErrBookNotReady). Клин ЖИВОГО прогона на удалённом каталоге не тронут: там нужен тот же счётчик неудач на стороне реконсилятора либо эскроу-половина строки 136 ⚠ Дополнено кросс-семейным ревью: терминальность интейка теперь НЕ применяется к not_configured — книга без конфигурации ждёт в parsing вместо уничтожения, потому что это состояние ДЕПЛОЯ, а не книги. Цена названа: пока развилка book.yaml не ратифицирована, такие книги копятся, и видит их метрика tm_platform_books_in_intake{status="parsing"}ДИСПОЗИЦИЯ P7: не бралась. Клин живого прогона на удалённом каталоге лечится терминальным состоянием после N неудач у реконсилятора, а это половина эскроу (строка 136) — строить её рядом с проектируемым целым значит ставить второй, более слабый ответ на тот же вопрос. P7 добавил к строке одно наблюдение: материализатор читающей поверхности зовёт движок в том же каталоге и на ту же ошибку отвечает логом, не трогая деньги ⚠ ПАК P8-REVIEW 24.08, сужено рефутером: для половины «спавн отказал» (попытка до движка не дошла) построены ДИАГНОЗ (runs --stalled, гейдж tm_platform_runs_stalled) и РУЧНОЙ вердикт оператора (tmplatformctl run abandon --run <id> --reason <text> [--release-hold], коммит 31f1f82, строка П-20 зонного бэклога). Автоматического терминального состояния после N неудач НЕТ и оно не планируется вне эскроу — это записанное решение (internal/runs/reconcile.go:249-257 «what it must not buy is this platform deciding on its own»). Проба end-to-end на стенде, восемь проходов при отказывающем движке: status=translating unit="" failures=8, гейдж 1, reserved=0.300000 — прогон висит и деньги заморожены, пока не придёт человек. Плюс возврат холда только с флагом: без --release-hold холд ждёт до 30 минут (PD-391). Значит сужать строку до «клина с юнитом или базовой линией» НЕЛЬЗЯ: беспризорный деплой на удалённом каталоге по-прежнему висит вечно ⚠ Границы улики: числа пробы (failures=8, гейдж 1, reserved=0.300000) сняты рефутером на его стенде, и ЛОГ прогона в артефакты не попал — пере-ранить их приёмка не сможет, воспроизводить придётся по описанию. Механизм при этом проверяем чтением: internal/pgstore/runs.go:689=select finished_at from runs where id = $1 ⚠ якорь пере-нацелен паком P11: прежняя строка отвечала ErrNoRun законченному прогону — она и была дефектом; теперь это ветвление, читаемое ПОД блокировкой книги open самопроверка дофикса (ревью вне карты)
PD-175 hardening minor internal/books/, internal/httpapi/v0.go createBook Квоты на интейк нет: аутентифицированный аккаунт может писать на диск оператора неограниченно. Пер-маршрутный потолок (PD-72) ограничивает ОДИН аплоад (64 МиБ по умолчанию), число аплоадов — ничто: ни лимита книг на аккаунт, ни ретеншена. Отклонённая по вине ИСТОЧНИКА книга свой файл теряет (каталог удаляется), а отклонённая по вине ДЕПЛОЯ — сохраняет намеренно (удалять чужую загрузку из-за своей поломки нельзя), и такие каталоги не чистит никто. Лечится квотой на аккаунт плюс свипом ретеншена по rejected; и то и другое — продуктовая политика (сколько книг входит в фри-тир), поэтому заведено, а не выбрано зоной ⚠ Третья половина того же вопроса — УДАЛЕНИЕ книги: pgstore.DeleteBook убирает строку и закрытые резервации и НЕ трогает каталог книги на диске, а ручки удаления в контракте нет вовсе (гейт PD-122). То есть сегодня утечки нет, потому что удалять нечем; день, когда ручка появится, — это и день, когда каталог обязан уходить вместе со строкой, и гард «только под BooksDir» для этого уже есть (books.owns). Диспозиция: закрывать ВМЕСТЕ с ручкой удаления, не раньше и не позже ⚠ Дополнено адверсариальным ревью P5 двумя фактами, которые делают строку острее, чем она написана: (1) пока развилка book.yaml не ратифицирована, ЛЮБАЯ загрузка приходит к rejected/not_configured — то есть путь «интейк пишет на диск и никто не убирает» сегодня ординарный, а не краевой; (2) у rejected нет ВЫХОДА вовсе: ни перепарса, ни удаления в контракте нет, строка остаётся в библиотеке навсегда. ⚠ Дополнено кросс-семейным ревью (Fable, 11.08): крэш-окно «строка закоммичена — каталог ещё не снесён» оставляет каталог-сироту, которого не найдёт никто (StuckIntake берёт только uploading и parsing). У отказа порядок перевёрнут в самоизлечивающийся — сначала каталог, потом строка, — а у брошенной загрузки перевернуть нельзя (каталог можно сносить только убедившись, что строки нет), так что окно там остаётся и закрывается тем же свипом ретеншена, что и вся строка ⚠ ПРОДУКТОВАЯ ПОЛОВИНА СНЯТА — D39.176 п.1 (слово владельца 30.08); диспозиция записана паком P12 31.08 по пингу аудита доков. Квот НЕТ и не будет, фри-тир-лимиты не проектируются: живём на покупке API, бонусы зачисляются из админки. Вопрос «сколько книг во фри-тир» не ждёт владельца — его больше нет. Остаётся ИНЖЕНЕРНАЯ половина, и она НЕ требует ничьего слова: ретеншен rejected-книг, свип каталогов-сирот, потолок диска — зона решает сама. Гейт один и не продуктовый: открытая регистрация, которой в закрытой бете нет. open сессия P5 (самопроверка, ось «что этот маршрут создаёт»)
PD-371 bug minor internal/pgstore/runs.go RunsToReconcile, internal/runs/reconcile.go deferItem Исключение «стоп перевешивает отсрочку» обходит отсрочку БЕЗ ГРАНИЦЫ, и для прогонов с запрошенным стопом голодание PD-169 внутри фазы возвращается. Прогон, чей stop_requested_at не пуст, выбирается КАЖДЫМ проходом независимо от reconcile_after, сколько бы раз подряд он ни падал. Измерено живьём при приёмке (дев-демон на дереве пака, хост без пользовательской шины systemd, поэтому Runner.Alive падает по-настоящему): reconcile_after стоял на ~30 минут вперёд (NEXT TRY 14:21:21Z), а FAILS дорос до 6 за ~90 секунд, то есть на каждом 15-секундном такте — отсрочка не действовала ни разу. На ДЕШЁВОЙ ошибке это безвредно и было именно так в пробе. На дорогой (шина или чтение журнала книги висит до конца бюджета) прогон снова держит голову списка весь бюджет фазы реконсиляции, а класс запускает ЛЮБОЙ пользователь кнопкой «остановить». Что смягчает и почему это не блокер: расчёт денег живёт во ВТОРОЙ фазе и не страдает (это и есть половина лечения PD-169), счётчик всё равно растёт, гейдж tm_platform_runs_stalled и run abandon работают — проверено той же пробой. Комментарий у RunsToReconcile называет цену НЕ-исключения («стоп ждал бы истечения бэкоффа») и не называет цену исключения. Направление, не решение: исключать до пересечения StalledAfter, а дальше подчинять стоп общему бэкоффу — переиздание стопа идемпотентно, и на пятой неудаче подряд «переиздать немедленно» уже ничего не покупает open приёмка P8-FIX (живая проба оркестратора №18, вне карты пака)
PD-372 bug minor internal/pgstore/runs.go DeferRun, internal/pgstore/books.go truncateReason, internal/pgstore/isolation_test.go Починку текста ошибки пинит только САМА функция, но ни один из четырёх её вызовов. TestAnEnginesOwnErrorTextSurvivesBeingRecorded зовёт truncateReason напрямую и доказывает, что Postgres принимает её результат, — а того, что вызывающий её ЗОВЁТ, не проверяет ничто. Посажена мутация оркестратором вне списка автора: truncateReason(reason)reason в DeferRun — батарея (./internal/pgstore/ + ./internal/runs/) осталась ЗЕЛЁНОЙ. Цена ровно та, которую комментарий этой же функции называет вслух: невалидный UTF-8 из stderr движка Postgres отвергает, запись отказа не проходит, счётчик не растёт и попытка держит голову списка вечно — то есть механизм PD-169 отключается тем самым текстом, ради которого заведён. Класс — PD-1 («свойство без пинящего теста не закрыто»), и он тут в форме «пин есть, но не на пути». Лечение дешёвое: провести один случай через DeferRun/DeferReadModelDebt и прочитать колонку назад open приёмка P8-FIX (посадка мутации оркестратором №18)
PD-217 bug minor internal/pgstore/books.go BooksForMigration, deploy/README.md Книга, застрявшая на daily_ceiling или на вечно незакрытом холде, блокирует апгрейд движка бессрочно, и выхода у оператора нет. Оба состояния снимаются только тем, чего платформа сделать не может: дневной потолок живёт в book.yaml оператора, и резюм по нему отказан 409; холд закрывается расчётом, который читает движок ЗАПИНЕННЫМ бинарём. Форсирующего флага у books --migratable нет намеренно — он и был бы способом пере-оплатить уже купленные вызовы. Лечится либо ручным разрешением в БД, либо каналом «признать попытку невосстановимой», которого в контракте нет ⚠ ПОЛОВИНА ЗАКРЫТА P7: paused вышел из предиката Resumableс 0.3.0 прогон, остановленный ЛЮБЫМ потолком, resume продолжить не может вовсе (409 ceiling_reached), значит к старой сборке ничего не пришпилено и мигрировать такую книгу безопасно; лечение пользователя — НОВЫЙ прогон, который спавнится ТЕКУЩИМ бинарём. Книга на дневном потолке апгрейд больше не запирает. Связь названа в коде: вернётся резюм паузы — вернётся и предикат. Пин TestEachOfTheThreeBlockersAloneKeepsABookOutOfTheMigrationList (перечень «безопасных» проверяется целиком, а не по одной книге). ⚠ ОСТАЁТСЯ вторая половина: книга с вечно незакрытым холдом блокирует по-прежнему, и это правильно — расчёт читает движок запиненным бинарём open приёмка P6 (дофикс, ФП-7)
PD-219 bug minor internal/runs/reconcile.go drainJournal, reopen Перезапуск после упавшего дрейна теряет непримененный хвост журнала навсегда. Новая попытка получает курсор с РАЗМЕРА журнала на момент допуска, поэтому строки, которые прошлый дрейн не успел применить, не прочитает уже никто. Счётчики восстанавливает ре-синк, а unit_resolutions — нет: их единственный источник — поток. Сегодня невидимо (читающей поверхности единиц ещё нет), к P7 станет расхождением read-модели ⚠ ДИСПОЗИЦИЯ P7 (взято на учёт, НЕ закрыто): предсказание строки сбылось ровно наполовину, и половины теперь названы. Что САМОЛЕЧИТСЯ: текст и состояние пары приходят не из потока, а из tmctl export на границе работы (readmodel.Refresh), поэтому недодрейненный хвост их не искажает. Что НЕ лечится: unit_resolutions — единственный источник ЗАМЕЧАНИЙ и счётчиков главы, и потерянная строка это замечание, которого пользователь не увидит никогда, плюс units_done, занижённый навсегда. Лечение — перечитывание хвоста журнала при переоткрытии прогона (курсор новой попытки начинается с размера журнала на допуске); это работа того же класса, что эскроу (строка 136), и в P7 не бралась осознанно open приёмка P6 (дофикс, ФП-7)
PD-407 bug minor internal/runs/bank.go (ветка ctx.Err() после BankApply), internal/runner/bankapply.go:40 bankStopGrace Единственный путь двери правок, способный оставить полу-приземлённую пару файлов БЕЗ отчёта, отвечает общим 503 service_unavailable вместо своего слова bank_corrections_incompleteа комментарий ветки обосновывает её контрактом SIGTERM, который сюда не попадает. Ветка ctx.Err() != nil достижима только при res.Exited=false, т.е. когда глагол НЕ вышел сам — SIGKILL по WaitDelay (грейс 10 с) или несостоявшийся старт; глагол, поймавший SIGTERM, выходит 5 и идёт через bankVerdict. SIGKILL между двумя rename — ровно класс 15, и адресат его слова другой («машина шлёт ТО ЖЕ, никто не пере-решает»). Смягчение: ре-сенд сходится байтовым no-op движка в обе стороны, цена — неточное слово. Рядом: bankStopGrace 10 с КОРОЧЕ самого длинного непрерываемого участка глагола (движковый замер: 5000 declines ≈ 11.3 с до пред-записной проверки ctx) — на документе-максимуме SIGKILL опережает штатный останов для большинства моментов отмены ⚠ ГРЕЙС-ПОЛОВИНА ЗАКРЫТА фикс-раундом P9 (28.08, D39.162): bankStopGrace поднят до 30 с, число обосновано движковым замером в комментарии константы (×2.5 запас на медленный хост); комментарий SIGKILL-ветки переписан честно (описывал соседний путь). ОТКРЫТЫМ остаётся слово: полу-приземлённая пара без отчёта по-прежнему отвечается общим 503 вместо bank_corrections_incomplete — смягчение (ре-сенд сходится) в силе open (грейс-половина закрыта D39.162) воркфлоу-ревью P9 28.08 (линзы door:interleave · door:crash-windows), диспозиция оркестратора 28.08: строкой; грейс — фикс-раундом
PD-412 bug minor internal/pgstore/readmodel.go:434-481 (draftChapters ×3 текстовых вхождения, editChapters, chaptersDone, noteCount), internal/pgstore/books.go bookColumns/lastRunTx/listBooksTx Карточка книги стоит 5 коррелированных сканов chapters на один GET, и 2 из них повторяются на КАЖДУЮ строку страницы библиотеки (×100). PG повторные текстовые вхождения одного подзапроса НЕ дедуплицирует (доказано воркфлоу side-effect-последовательностью на живом PG 18.4), CASE-ветки честно short-circuit; замер на фикстуре в форме миграции 00002 и книге 2283 глав: выражение как написано — 1.359 мс, те же три числа одним LEFT JOIN LATERAL (count(*) filter (...)) — 0.307 мс (×4.4); контрольный EXPLAIN сессии на реальной схеме tmp9stand: 6 SubPlan в плане, 5 исполняются. Лечение названо (один LATERAL-проход рядом с существующим lastRun); проект уже применял этот класс лекарства уровнем ниже (миграция 00022, note_count) open воркфлоу-ревью P9 28.08 (линза cost:per-request, замер) + контрольный EXPLAIN сессии P9, диспозиция оркестратора 28.08: строкой
PD-413 bug minor internal/pgstore/sink.go:293-309 emitProgress (тот же 3-скан агрегат) под lockBook из RunSink.Apply (sink.go:80-104), источник событий: движок шлёт progress на КАЖДЫЙ разрешённый юнит каждой волны Каждое progress-событие пересобирает полосный агрегат по ВСЕЙ книге — под удержанной блокировкой строки книги: стоимость прогона растёт как O(глав × юнитов) вместо O(юнитов). Изменение счётчиков ОДНОЙ главы (unitDone трогает одну строку) на следующей же строке потока триггерит 3 полных скана chapters (≈1 мс на книге 2283 глав, замер воркфлоу), сериализуя относительно себя любого другого писателя книги (RequestStop, дверь правок, следующее событие) — за прогон это десятки тысяч повторов, секунды суммарного удержания блокировки. Лечение — то же, что у PD-412 (один LATERAL), плюс возможная инкрементальность; оркестратор 28.08: «вторая пахнет хуже первой» open воркфлоу-ревью P9 28.08 (линза cost:per-request, замер), диспозиция оркестратора 28.08: строкой
PD-418 bug minor internal/pgstore/runs.go StalledRuns (settling-ветвь), internal/pgstore/observe.go, AbandonRun (ветвление по runs.finished_at), deploy/README.md У settling-строки ЖИВОГО прогона нет ручки, а рантбук обещает оператору обратное. PD-385 называет ДВЕ популяции, и вторая — «прогон, который ЖИВ, но чья ПРЕДЫДУЩАЯ попытка не рассчиталась после рестарта». Пак P11 дал ей обе поверхности ВИДИМОСТИ (таблица и гейдж ключуются по концу ПОПЫТКИ, так что строка показывается), но run abandon ветвится по runs.finished_at и на живом прогоне уходит в живую ветку: осиротевший холд он не тронет, а ответит про процесс. Рантбук при этом описывает PHASE=settling как то, что лечится этой командой. ⚠ Сегодня состояние НЕДОСТИЖИМО и это часть строки, а не оговорка: единственный не-тестовый путь к второй открытой резервации — reopen, который отказывается стартовать следующую попытку, пока холд предыдущей открыт. То есть документ расходится с кодом на состоянии, которого код пока не производит, — и разойдётся заметно, если этот инвариант когда-нибудь ослабнет. Лечение — либо ветвление по НАЛИЧИЮ осиротевшей попытки вместо finished_at, либо оговорка в рантбуке ⚠ пак P12 (3031.08): сделана дешёвая половина (оговорка в рантбуке), долговечная подписана диспозицией. В platform/deploy/README.md дописано, что ветвление run abandon идёт по runs.finished_at, а не по наличию осиротевшей попытки, и что живой прогон с нерассчитанной ПРЕДЫДУЩЕЙ попыткой ушёл бы в живую ветку — то есть документ больше не обещает оператору того, чего код не делает. ⚠ Пак P12 состояние достижимее НЕ сделал, и это проверено: лечение PD-424 заставляет блокированную расплату считаться и быть видимой, но рестарт по-прежнему НЕ происходит, значит второй открытой резервации не возникает; пин PD-424 утверждает это прямо (строка отчитывается как live, а не settling). Долговечное лечение — ветвить по НАЛИЧИЮ осиротевшей попытки вместо finished_at, как уже делает settling-ветвь StalledRuns. ⚠ СДЕЛАНО паком «закрыть цикл» (04.09), и это ровно названная долговечная половина. Из запроса осиротевших попыток (abandonSettlement) убрано r.finished_at is not null, а живая ветвь AbandonRun спрашивает про орфана ПЕРВЫМ делом — значит живой прогон с нерассчитанной ПРЕДЫДУЩЕЙ попыткой попадает в settling-обработку по построению, и рантбук больше не обещает того, чего код не делает. ⚠ Живая попытка при этом НЕ трогается и прогон НЕ заканчивается: застряли деньги одной попытки, а не перевод; settled_at живому прогону не ставится, иначе список расчётов перестал бы видеть попытку, чей холд ещё открыт. Пин: internal/runs/abandon_orphan_test.go TestALiveRunsOrphanedAttemptIsSettledWithoutEndingTheRun — состояние он пишет ПРЯМО, потому что код его сам по-прежнему не производит, и это ровно та оговорка, ради которой строка заведена. Строка остаётся открытой на дешёвой половине: рантбук deploy/README.md описывает ветвление по finished_at, и его текст этой правкой ещё не пере-снят. open пак P11 (самопроход, линза соответствия заказу) + приёмка оркестратора №19
PD-419 bug minor internal/pgstore/migrations_test.go TestReleasedMigrationsAreUnchanged, internal/pgstore/migrations.sha256 Гейт выпущенных миграций слеп к ПЕРЕ-ПОДПИСИ и по построению не может отличить её от нарушения. Он сверяет файлы против migrations.sha256, лежащего в ТОМ ЖЕ дереве, поэтому ловит ровно один сценарий: правку миграции тем, кто забыл про манифест. Автор, который правит ВЫПУЩЕННУЮ миграцию и пере-подписывает её строку одним движением, проходит молча. Оба отказа, ради которых гейт написан, остаются достижимыми через пере-подпись: файл, отредактированный после накатки, больше никогда не запускается (goose применяет по НОМЕРУ и хранит только его), а переиспользованный номер лишает базу отката. Комментарий гейта при этом заявляет «this is the check that makes that true rather than intended». Единственный носитель «что уже выпущено», не лежащий рядом с правкой, — git: гейт мог бы брать git show HEAD:…migrations.sha256 и требовать побайтового совпадения строк СУЩЕСТВУЮЩИХ там миграций, свободно допуская новые; прогон в дереве без git обязан тогда ГРОМКО скипаться, иначе гейт возвращается туда же, откуда ушёл open приёмка оркестратора №19 по паку P11 (замечена на законной пере-подписи 00028)
PD-420 bug minor internal/pgstore/runs_test.go TestAClaimThatLostARaceToAReleaseIsRetriedAndNotAnError Тест гонки холда против релиза краснеет под ПАРАЛЛЕЛЬНЫМИ батареями, а зона ратифицировала рецепт, который их требует. D39.159 §2 предписывает сажать мутации в КОПИЮ дерева, и всякая сессия, которая делает это всерьёз, гоняет несколько батарей разом. Замер пака P11: при трёх параллельных прогонах краснеет в ЧИСТЫХ копиях (2 раза из 15); в СЕРИЙНОМ прогоне зелен — пере-проверено трижды подряд отдельным прогоном, все три ok. ⚠ ВТОРАЯ ТОЧКА, 29.08, внешнее ревью: упал 1 раз из 4 ПОЛНЫХ прогонов зоны со всеми гейтами — то есть краснеет и без параллельных копий, просто редко. Изолированно 5/5, пакет целиком 2/2, три последующих полных прогона чистые; сообщение поймать не удалось, и ревьюер честно остановился на «редком флейке контенции», диагноз НЕ установлен. Пакет ни одним коммитом эры P11 не тронут. ⚠ Обе точки вместе сдвигают формулировку: это не «краснеет от параллельной нагрузки», а «редкая гонка, которую нагрузка делает вероятнее», — и мой первый диагноз («флейк параллельных батарей») был выведен из совпадения, ровно как в PD-423. Меряет ресурс, общий для копий на машине. ⚠ Цена не косметическая: красная ЧИСТАЯ копия маскирует дельту, а красный прогон посадки читается как «мутация поймана», когда она не поймана, — ровно та ложная улика, ради которой мутации и сажают. Обход пака — судить по ДЕЛЬТЕ множеств, а не по коду выхода; лечение — изоляция ресурса либо честный скип под нагрузкой ⚠ пак P12 (3031.08): пере-замерена ПОСЛЕ фикса PD-369, серийно — 80 из 80 зелёных, 0 FAIL, 0 SKIP (-count=80, счёт по ^--- PASS). Это снимает ПЕРВУЮ из двух точек строки по построению: флейк был не в тесте, а в том, что исчерпание раундов отвечало 500, и тест это честно ловил. ⚠ ВТОРАЯ точка (редкая гонка контенции, 1 из 4 полных прогонов, диагноз НЕ установлен) пере-замерена под ТРЕМЯ параллельными полными батареями в чистых копиях: 18/18 пакетов в каждой, 0 красных, EXIT=0 ×3. Это ОДНА точка, а не опровержение: строка сама говорит, что краснеет редко (1 из 4), и три чистых прогона такой частоты не исключают. Строка остаётся открытой на ней с дополненным замером; закрыть её может только серия, а не прогон. open пак P11, мутационная кампания (15 прогонов) + пере-проверка серийными прогонами
PD-423 standards minor internal/runner/systemd_test.go TestARunIsBoundedByItsOwnCgroup, docs/STACK_DECISIONS.md «Гейты батареи» Батарея зоны требует ЧЕТВЁРТОГО условия хоста, которого рецепт не называет: пользовательский менеджер systemd должен РЕАЛЬНО применять MemoryMax к транзиентным юнитам. Замерено на этом хосте 29.08: тест трижды подряд зелен в полных батареях (baseline2, final2, final4), затем пять раз подряд красен в изоляции — при неизменном коде пакета, которого пак не касался вовсе. Причина установлена ВНЕ батареи и вне Go: systemd-run --user --scope -p MemoryMax=64M … даёт процессу спокойно занять 400 МиБ и выйти с кодом 0. ⚠ МЕХАНИЗМ уточнён приёмкой оркестратора №19, и уточнение решает, воспроизводимо ли это: «systemd не применяет потолок» верно по симптому и мимо по причине. Свойство ДОЕХАЛО — systemctl --user show <scope> -p MemoryMax печатает 67108864; делегирование в порядке — memory pids и в cgroup.controllers, и в subtree_control. Пропал не потолок, а cgroup-КАТАЛОГ: в app.slice нет ни одного scope-каталога, а cut -d: -f3 /proc/self/cgroup для оболочки даёт /init.scope. То есть вызывающий процесс живёт ВНЕ user@<uid>.service; systemd-run --user заводит юнит в модели менеджера, а процесс остаётся в исходном cgroup, и лимита не получает никто — молча. ⚠ Отсюда и наблюдение «условие отваливается между двумя прогонами одной сессии»: оно зависит от того, из какого cgroup стартовал прогон. Тест при этом ПРАВ и его сообщение точное («check that the leaf cgroup of tm-runs.slice has memory.max»): он ловит ровно то, ради чего написан, — что потолок памяти прогона на этом хосте иллюзорен. ⚠ Следствие для процесса, а не только для теста: рецепт STACK_DECISIONS называет три условия батареи, а их четыре, и четвёртое — свойство ХОСТА, которое может отвалиться между двумя прогонами в одной сессии, что здесь и произошло. Сессия, наступившая на это, потратит время на поиск дефекта в своём диффе. ⚠ Предложенная этой строкой формулировка четвёртого условия («вызывающий процесс обязан жить ВНУТРИ user@<uid>.service», проверка cut -d: -f3 /proc/self/cgroup/init.scope) ОПРОВЕРГНУТА точками 2 и 3 ниже и СНЯТА из рецепта (PD-432). Живой остаток лечения — дать тесту различать «хост не применяет лимит» (честный скип с причиной) и «раннер не передал лимит» (настоящий отказ) ⚠⚠ ВТОРАЯ ТОЧКА, 29.08, и она ПРОТИВОРЕЧИТ механизму выше — строку не закрывать, а пере-проверить. Пак sqlc на ТОМ ЖЕ хосте и при том же cut -d: -f3 /proc/self/cgroup = /init.scope получил тест зелёным 5 из 5 в ИЗОЛЯЦИИ (протокол, в котором приёмка №19 видела 5 из 5 красных) плюс трижды в полных батареях; скипов в этих прогонах ноль — проверено по логам, то есть systemdOrSkip и проверка python3 не срабатывали и тест НЕ был пустым: он требует настоящего oom-kill после касания 400 МиБ под MemoryMax=64M. Прямая проба механизма: systemd-run --user --scope -p MemoryMax=64M -- sh -c 'cut -d: -f3 /proc/self/cgroup' печатает /user.slice/user-1000.slice/user@1000.service/app.slice/run-….scope, то есть процесс ВСЁ-ТАКИ попадает внутрь user@<uid>.service, а не остаётся в исходном cgroup. Сам tm-runs.slice при этом существует и лежит глубже, чем ищут: user@1000.service/**tm.slice**/tm-runs.slice (cgroup.controllers = memory pids). Следствие практическое: предложенная этой же строкой одна команда-проверка (/proc/self/cgroup не должен давать /init.scope) на этом хосте даёт ЛОЖНЫЙ ОТРИЦАТЕЛЬНЫЙ — она говорит «условие не выполнено» там, где лимит применяется и тест честно зелёный. Значит cgroup ВЫЗЫВАЮЩЕГО процесса условие не предсказывает, диагноз строки неполон, и в рецепт STACK_DECISIONS эту команду в нынешнем виде вносить нельзя. Что различает две точки — не установлено; кандидат — состояние cgroup.subtree_control целевого среза в момент прогона (сейчас у tm-runs.slice он пуст, а systemd включает контроллер сам при старте юнита с лимитом). ⚠ ТРЕТЬЯ ТОЧКА, пак P12 (3031.08), и она согласна со второй: у оболочки этой сессии cut -d: -f3 /proc/self/cgroup даёт / (не /init.scope и не путь внутри user@<uid>.service), а TestARunIsBoundedByItsOwnCgroup при этом ЗЕЛЁН в полной батарее со скипами 0 — проверено четырьмя полными прогонами. Прямая проба systemd-run --user --scope кладёт процесс в …/user@1000.service/app.slice/run-….scope; tm-runs.slice существует, cgroup.controllers = memory pids, а его cgroup.subtree_control ПУСТ — и тест всё равно зелен. То есть команда-проверка не предсказывает условие ни в одну сторону, и она СНЯТА из рецепта STACK_DECISIONS этим паком (см. PD-432). Что различает точки — по-прежнему НЕ УСТАНОВЛЕНО; строка остаётся открытой на диагнозе, а не на рецепте. open пак P11 (финальная батарея; воспроизведено голым systemd-run вне Go)
PD-250 vuln minor cmd/tmplatformd/main.go (слушатель метрик) /metrics отдаётся БЕЗ аутентификации; вся защита — привязка к 127.0.0.1. Для одной VM это честная граница, и она записана (STACK §24). Но на хосте с несколькими пользователями любой локальный процесс читает оперативную картину сервиса, а на деплое, где слушатель однажды переедет на 0.0.0.0 «чтобы Prometheus дотянулся», защиты не останется вовсе. Денег в метриках нет (D39.84), поэтому это minor, а не major. Лечение — bearer-токен на слушателе или mTLS, решать при первом внешнем Prometheus ⚠ ПАК P8-REVIEW 24.08: это ТОТ ЖЕ факт, что PD-179, и он стоит в регистре в ДВУХ статусах одновременно (accepted-risk(платформа P5, 11.08) против open). Код и ручка одни: cmd/tmplatformd/main.go serveMetrics без аутентификации, дефолт 127.0.0.1:9464; рантбук deploy/README.md называет строкой риска именно PD-179. Своя добавка у этой строки есть (многопользовательский хост), но статус один факт должен нести один. Предложение пака: свести ⚠⚠ Условие сведения, найденное рефутером: у PD-179 довод про ОДНУ VM, а добавка этой строки — многопользовательский хост, где любой локальный непривилегированный процесс скрейпит экспозицию, — в PD-179 ОТСУТСТВУЕТ. Плюс при сведении из выборок безопасности исчезает класс vuln (у PD-179 он hardening). Сводить только ВМЕСТЕ с перенесённой фразой и с пометкой класса open research/28 §9 (пинг оркестратора №17), сверено P7
PD-454 bug minor Makefile цель check; гейт internal/gates.TestTheBatteryCannotReportCleanlinessWithoutItsLog БАТАРЕЯ УМЕЛА ВЫДАТЬ ЧИСТУЮ СПРАВКУ О ПРОГОНЕ, КОТОРОГО НЕ ИЗМЕРЯЛА. Каждая строка, которую печатает make check о прогоне — список пакетов, ряды ALARM, список падений, счёт скипов, — есть ГРЕП по одному файлу. Греп отвечает на ОТСУТСТВУЮЩИЙ файл ровно тем же, чем на чистый: ничем. Поэтому рецепт доходил до финального else и печатал «every test ran: no host condition was missing» о прогоне, чей лог исчез. ⚠ Наблюдено 06.09, а не выведено: три подряд grep: .check.log: No such file or directory, следом эта самая строка — на прогоне, где скипов было ПЯТЬ. Код возврата при этом 0, потому что статус берётся от go test ДО грепов. Причина — ФИКСИРОВАННОЕ имя лога, общее для всех прогонов в каталоге, а два прогона в одном каталоге здесь НОРМА: батарею гоняют и зона, и оркестратор, и первый закончивший удаляет улику второго посреди рецепта. ⇒ лечение двумя половинами, и они не дублируют друг друга: имя лога стало ПОПРОГОННЫМ ($$ — PID шелла), что снимает сегодняшнюю ПРИЧИНУ, и добавлен гвард «нет лога ⇒ выйти красным ДО первого чтения», что снимает КЛАСС — отказавшийся редирект, полный диск, рука. Проверено исполнением: подменённый GO, не пишущий лога, даёт красный выход и две строки объяснения вместо справки. ⚠ Гейт держит ФОРМУ, а не эту починку: лог может быть попрогонным или нет, гвард может быть -s или -f, но ни одно чтение не смеет идти раньше проверки, которая умеет выйти. ПЯТЬ мутаций ловятся — гвард удалён · гвард после первого чтения · тест есть, exit убран · exit 1exit 0 · exit без кода. ⚠ Две последние добавлены ВТОРОЙ редакцией гейта, и нашёл их не я, а приёмка оркестратора: первая редакция требовала лишь, чтобы после гварда был шаг, начинающийся с exit, и exit 0 проходил зелёным — то есть батарея объявляла вслух, что улики нет, и возвращала УСПЕХ. Замерено им исполнением (exit 0 + подменённый GO: MAKE-EXIT = 0 при напечатанном «THE BATTERY LEFT NO LOG»), воспроизведено зоной. Это тот же дефект, переодетый, и в одном отношении ХУЖЕ исходного: прежняя ложная справка была СТРОКОЙ, которую человек ловит глазами, эта — КОД ВОЗВРАТА, который потребляет машина (CI, лендинг). ⚠ И сообщение гейта обещало «exits RED», проверяя лишь наличие выхода, — та же болезнь, что он лечит, в нём самом; формулировка подтянута под проверяемое. Теперь требуется ЛИТЕРАЛ ненулевого кода: голый exit несёт статус предыдущей команды (echo, то есть ноль), а exit $$var из рецепта не судится вовсе. ⚠ Первая редакция гейта СЧИТАЛА ЧТЕНИЕМ слово grep внутри объясняющего echo самого гварда — ровно тот substring-vs-invocation капкан, о котором соседний гейт пишет абзацем выше; поймано прогоном, не рассуждением. Девятая форма ложной зелени (D39.202)ЛЕЧЕНИЕ В ДЕРЕВЕ, статус флипает ЛЕНДИНГ open замер зоны 06.09, заказ оркестратора

Открытые — info

ID Класс Серьёзность Где Суть Статус Источник
PD-445 doc info internal/httpapi/bank.go:386=Undecided int (проекция квитанции); честный счёт — internal/pgstore/readmodel.go:324 signature.undecided не сдвинулся после ПРИНЯТОЙ правки (82 → 82), и оператору нечем отличить «правка не зашла» от «зашла, но счёт про другое». Замерено 04.09: preview и apply вернули signature.surfaces 82, undecided 82 при accepted[0].state = applied. ⚠ Арифметика ВЕРНА: счёт неопределённости — движковый и считает МАЙНЕННЫЕ поверхности, а термин правки был не из них, поэтому двинуться числу было не от чего. Дефект — в обратной связи: единственное число, которое оператор видит после правки, на успешную правку не реагирует вовсе, и чтобы убедиться, что правка ЗАШЛА, потребовалась отдельная сверка по другому пути. ⚠ Вес info, потому что квитанция уже несёт точный ответ (accepted[].state) — это не ложь механизма, а то, что глаз читает первым. open живой платный прогон пака «закрыть цикл», наблюдение H13, 04.09
PD-396 standards info internal/pgstore/books.go:326=chunker_version = $4, parse_started_at = null,, internal/pgstore/sink.go:127=update books set chunker_version = $2 where id = $1, internal/ingest/resync.go:36=UnsignedBankTerms int json:"unsigned_bank_terms"`` Мёртвые поля шва: у books.chunker_version ДВА писателя и НОЛЬ читателей, и это второй экземпляр класса, первый назвал оркестратор. Колонку пишет интейк из манифеста движка (FinishParse) и пишет тейлер из хендшейка потока (effect); ни одного select по ней в зоне нет — грепом ноль. Сегодня это безвредно, но следствие названо ЗАРАНЕЕ, потому что оно семантическое, а не техническое: в день, когда читатель появится, РАСХОЖДЕНИЕ двух писателей (чанкер прогона против чанкера разбора) станет значением, и решать, какой из них правда, придётся задним числом — по колонке, у которой уже накоплена история из обоих источников. Родня — п.4 пинга оркестратора №19: ingest.StatusReport.UnsignedBankTerms разбирается из ответа движка и не используется НИ ОДНОЙ строкой продакшн-кода (грепом — только объявление и его доккомментарий, где поле описано как опора экрана подписи). Формулировка оркестратора применима дословно к обоим: мёртвое поле в структуре шва читается как контракт. ⚠ Заведено ОТДЕЛЬНОЙ строкой, а не дописком в PD-166: та про потерю значения между автокоммитами, и её свойство построено. Диспозиция — вопрос владельца, а не зоны: либо назначить владельца колонки (один писатель), либо записать расхождение как ожидаемое до появления читателя Воспроизведение — две команды без конвейера (символ вертикальной черты в ячейку регистра не влезает): grep -rn chunker_version platform/internal --include=*.go даёт два update и ни одного select, grep -rn UnsignedBankTerms platform/internal platform/cmd --include=*.go даёт только объявление и его доккомментарий open ревью-пак P8-REVIEW (побочная находка сверки реестра, оформлена по указанию закрывающего ревью)
PD-378 bug info internal/pgstore/books.go:1175=u.RemainingPercent = int(balance * 100 / granted), internal/httpapi/v0.go usageState, канон 14-api-contract remaining_percent /v0/usage отдаёт remaining_percent вне контрактных 0..100 и зажигает предупреждение «low» на полном счёте: balance * 100 переполняет int64. Порог измерен точно: баланс 92 233 720 368 547 758 микро ещё даёт 99%, следующий микро-доллар даёт минус 99. Ответ нарушает схему (minimum: 0, maximum: 100), и хуже того usageState видит отрицательное значение ниже порога lowCredit и отдаёт state: "low" — «денег почти нет» счёту на сто миллиардов. Замерено на проводе: до гранта {"state":"ok","remaining_percent":96}, после grant --usd 100000000000{"state":"low","remaining_percent":-84}; соседняя арифметика (pricing.Scale, balance) при том же балансе отвечает верно, то есть переполнение локально именно в этой строке. ⚠ Рефутер сузил minor → info: чтобы туда попасть, оператор должен добавить на счёт не меньше 92.23 млрд долларов, ни одна пользовательская ручка кредит не пишет; прецедент веса — PD-39. ⚠ Оговорка рефутера в другую сторону: более правдоподобный носитель — не разовая команда, а конфиг TM_PLATFORM_SIGNUP_GRANT_USD, у которого верхней границы нет и значение НАМЕРЕННО не печатается в стартовый лог, так что промах в нём сломал бы /usage каждому новому аккаунту невидимо. Воспроизведение: docs/p8-review/axis1-money/a1-usage-overflow.sh (сам откатывает грант) open ревью-пак P8-REVIEW, ось 1 (живой провод, сужено рефутером с измеренным порогом)
PD-381 hardening info internal/auth/middleware.go:38=a.Deny.ServeHTTP(w, r) и та же строка на :52, internal/auth/cookie.go ClearSession 401 по мёртвой сессии не стирает куку: браузер продолжает слать отозванный токен до конца её Max-Age (по умолчанию 14 суток). Обе ветки отказа зовут a.Deny.ServeHTTP и к a.Cookies не обращаются, перекрытия выше по стеку нет — живой 401 не несёт ни одной строки Set-Cookie. Норму формулирует сам код: комментарий ClearLogin говорит, что кука, пережившая свой круг, это «a replay waiting for an accident», а PD-88 заведена ровно на тот исход, при котором кука переживает сессию. Дешёвое лечение — чистить куку на пути отказа, где она была предъявлена. Отдельно от PD-88 (та про Max-Age меньше секунды) и от PD-5/PD-70/PD-74/PD-103 Воспроизведение: docs/p8-review/axis2-auth/csrf-matrix.sh и session-clocks-probe.sh (обе пробы поднимают демон и печатают ПОЛНЫЕ заголовки ответа, включая отсутствие Set-Cookie на 401); проверка чтением — grep -n 'Cookies' internal/auth/middleware.go, ни одного вхождения на путях отказа open ревью-пак P8-REVIEW, ось 2 (чтение + живая проба, подтверждено рефутером)
PD-382 hardening info internal/auth/cookie.go:52=func (c Cookies) ClearSession(w http.ResponseWriter) { c.set(w, c.SessionName(), "", -time.Second) }, internal/auth/cookie.go ClearLogin Путь ИСТЕЧЕНИЯ куки не запинен: две мутации, стирающие выход из браузера, прошли батарею целиком. ClearSession и ClearLogin — единственные места, где кука получает отрицательный Max-Age, и порча этого выражения ничего не роняет. ⚠ Рефутер поправил ЦЕНУ, названную первой редакцией находки: set пишет значение вызывающего, а обе Clear-ручки передают ПУСТУЮ строку, поэтому мутант не перевыпускает куку с живым токеном — он оставляет пустую куку, и следующий запрос всё равно приходит без сессии. То есть вреда сегодня нет, а не запинено СВОЙСТВО «выход удаляет куку из браузера», и это класс PD-1, родня PD-86/PD-87. Воспроизведение: docs/p8-review/axis2-auth/mutations-axis2.shПере-проверено на ПОЛНОЙ батарее координатором пака (мутации M13 и M14), и по дороге поймана СВОЯ ошибка метода: первый прогон M13 дал красный, но упавшим оказался TestAClaimThatLostARaceToAReleaseIsRetriedAndNotAnError — известный флейк PD-369, к ClearSession отношения не имеющий. То есть вердикт, вынесенный по ЦВЕТУ батареи, а не по ТОПИЧНОСТИ упавшего теста, даёт ложное «пойман» и тихо теряет находку. Пере-прогон обеих мутаций даёт пустую дельту против чистой копии. Правило записано здесь, потому что цена его забывания — потерянная находка о недостающем пине: docs/p8-review/mutations-round2.log open ревью-пак P8-REVIEW, ось 2 (посадка мутации, цена поправлена рефутером)
PD-383 hardening info cmd/tmplatformd/main.go:257=const loginJournalRetention = 180 * 24 * time.Hour, cmd/tmplatformd/main.go sweepLogins, internal/pgstore/identity.go DeleteOldLoginEvents Ретенция журнала входов работает и не запинена ничем: и срок хранения, и сам свип переживают батарею. Механизм построен (константа 180 суток, тикер 15 минут, delete from login_events where at < $1, монтируется в обеих ветках входа) и проверен ЖИВЬЁМ: строка возрастом 200 суток исчезла на ближайшем тике, демон напечатал "login sweep" events=1. Но ни срок, ни вызов не пинятся: DeleteOldLoginEvents не зовёт ни один тест, а sweepLogins — неэкспортируемая функция пакета main без теста. Класс PD-1 на механизме, который ЛЕЧИТ уже закрытую строку. Воспроизведение: docs/p8-review/pd23-retention-probe.sh и pd23-result.txtПере-проверено на ПОЛНОЙ батарее координатором пака (мутации M15 и M16): срок хранения поднят со 180 суток до 180 ЛЕТ и, отдельно, предикат свипа обезврежен (delete from login_events where at < $1 and 1=0) — обе дельты против чистой копии ПУСТЫ на всех 18 пакетах. Лог — docs/p8-review/mutations-round2.logПОЛОВИНА ЗАКРЫТА паком sqlc (63fcee5), и обе половины пере-проверены посадками — строка остаётся open ровно на остатке. ЗАКРЫТ сам свип-предикат: DeleteOldLoginEvents теперь зовёт pgstore.TestTheLoginJournalRetentionDeletesOnlyWhatIsOlderThanTheCutoff, и он утверждает ЧИСЛО удалённых строк плюс границу (в фикстуре есть строка РОВНО на отсечке, потому что предикат строгий и без неё < неотличимо от <=). Мутация M16 (and 1=0) теперь красная адресно — «deleted 0 rows, want exactly 1». НЕ закрыто и остаётся живым: сам СРОК хранения и вызов свипа. Мутация M15 — loginJournalRetention 180 суток → 180 ЛЕТ (cmd/tmplatformd/main.go:257) — пере-прогнана 29.08 на полной батарее конвертированного дерева: EXIT=0, ноль красных. Причина остатка структурная и не лечится в pgstore: константа и sweepLogins живут в пакете main, куда тест pgstore не достаёт. То есть класс PD-1 здесь снят с ЗАПРОСА и стоит на КОНФИГУРАЦИИ open ревью-пак P8-REVIEW, ось 2 (находка рефутера, живая проба координатора)
PD-388 hardening info internal/readmodel/readmodel.go:198=if deadline, ok := ctx.Deadline(); ok && time.Until(deadline) < MaterializeBudget {, cmd/tmplatformd/runner.go refreshSweepBudget, internal/books/parse.go (запиненный близнец) Пара чисел в разных пакетах, не выводимая и не запиненная, и на неверной её стороне материализатор молча не делает ничего. Drain начинает книгу, только если у прохода осталось не меньше ЦЕЛОГО MaterializeBudget (5 минут), а число прохода живёт в cmd/tmplatformd голым литералом 10 минут, без ссылки на константу, которую обязано превышать. Это единственный член семьи без страховки: intakeSweepBudget ВЫВЕДЕН формулой и разъехаться не может, а claimGrace и выведен, и запинен отдельным тестом. Цена неверной стороны — не деградация, а полное молчаливое отключение: замерено пробой, проход 4м59с даёт claims=0, вызовов движка 0 и nil, ошибки нет, лога нет, sweep_unfinished_total не растёт. Мутация «refreshSweepBudget 10м → 1м» пережила ПОЛНУЮ батарею. ⚠ Вторая половина, найденная рефутером: сам гейт бюджета между книгами — живой носитель ЗАКРЫТОЙ PD-293, чья эррата прямо на него ссылается, — не покрыт ни одним тестом (во всём дереве нет теста, который даёт Drain дедлайн), поэтому его можно выключить целиком, и батарея останется зелёной; точный близнец у интейка при этом запинен своим TestAPassTooShortForAParseStartsNoneAtAll. ⚠ Рефутер опроверг приписку финдера «проход по построению начинает максимум 2 книги из 4»: гейт сравнивает остаток перед КАЖДОЙ книгой, и при проходе 10 минут стартуют все четыре. Воспроизведение: docs/p8-review/axis3-queue/probe_drain_budget_pair_test.go.txt open ревью-пак P8-REVIEW, ось 3 (посадка мутации, расширено рефутером на носитель PD-293)
PD-393 hardening info internal/metrics/metrics.go:216=if unfinished {, internal/metrics/metrics.go (два счётчика из двенадцати коллекторов), deploy/README.md (рецепт алерта) Счётчиков событий на весь демон два, и оба отвечают не на тот вопрос: «ошибки» из четырёх золотых сигналов закрыты только для HTTP. sweep_unfinished_total поднимается ИСКЛЮЧИТЕЛЬНО на context.DeadlineExceeded, поэтому проход, упавший обычной ошибкой, регистрируется как быстрый здоровый проход — замерено: 4 строки ERROR «runs sweep failed» в журнале и НИ ОДНОГО изменения в экспозиции, кроме счётчика длительности. Ни у чего остального счётчика нет вовсе: отказ спавна, неудача расчёта, карантин, ненулевой выход движка, упавший прогон. Всё состояние снимается гейджами раз в такт, поэтому событие, уместившееся между двумя проходами, в экспозиции не существует, и на вопрос «сколько прогонов сегодня упало» ответить нечем. Отдельная мелочь того же корня: sweep_unfinished_totalCounterVec, и пока он ни разу не вырос, семейства в экспозиции НЕТ вовсе, поэтому готовый рецепт алерта рантбука («растёт tm_platform_sweep_unfinished_total») даёт «no data», а не ноль; практика Prometheus велит инициализировать известные наборы лейблов нулём. ⚠ Речь о СЧЁТЧИКАХ СОБЫТИЙ, не о суммах денег: запрет D39.84 на денежные числа в метриках соблюдён, проверено. Воспроизведение: docs/p8-review/axis4-metrics/40-exposition-check.sh open ревью-пак P8-REVIEW, ось 4 (живой замер, заголовок сужен рефутером)
PD-395 doc info internal/gates/contract_test.go:14=const canonPath = zoneRoot + "/../docs/architecture/14-api-contract/openapi.yaml", docs/ENGINEERING_STANDARDS.md §3, промты ревью-паков зоны Копия platform/, вынутая из репозитория, даёт красную батарею по причине, не имеющей отношения к коду. Гейт контрактной версии читает канон по пути ВЫШЕ модуля (internal/gates/contract_test.go:14=const canonPath = zoneRoot + "/../docs/architecture/14-api-contract/openapi.yaml") и при его отсутствии t.Fatalf, а не сообщает, что запущен вне репозитория. Стоило времени КАЖДОМУ, кто работал в копии — все девять субагентов пака и координатор, — и один агент едва не завёл ложный красный находкой. ⚠ ДИСПОЗИЦИЯ 24.08: лечение — РЕЦЕПТ, а не гейт. Три довода, каждый проверен исполнением: (1) носители названы шире, чем есть — ENGINEERING_STANDARDS §3 копий на момент находки НЕ требовал (grep -ci 'копи' давал 0; ПОСЛЕ правки этого пака даёт 6 — рецепт туда и записан, см. ниже), норма копий живёт только в промте ревью-пака и в D39.113; значит сталкиваются не гейт и стандарт зоны, а гейт и РЕЦЕПТ ОДНОГО ПРОМТА. (2) «точечное решение, а не стиль» — НЕВЕРНО: в backend/internal/standdata/standdata.go живёт целый репо-паттерн чтения выше корня модуля с поиском корня по МАРКЕРУ и env-переопределением, и его потребители читают даже чужую зону (internal/miner/miner_parity_test.go читает eval/). Платформа реализовала тот же паттерн грубее — голым счётом .., — и комментарий standdata объясняет, чем именно это хуже. (3) довод «модуль обязан быть самодостаточным» к этой зоне не применяется: её собственный DoD определяет полную приёмку через ТРИ ВНЕШНИХ условия. Ценность зоны — не герметичность, а громкий учёт непроверенного. ЛЕЧЕНИЕ — РЕЦЕПТ, А НЕ ГЕЙТ (сам рецепт и его довод — ENGINEERING_STANDARDS §3 п.3). Второй, необязательный шаг: резолвить канон маркер-обходом по образцу standdata.Root() и дописать в сообщение Fatalf вторую гипотезу «ты вне репозитория» — это убивает стоимость повторной диагностики и остаётся падением, а не скипом. ОТВЕРГНУТО с доводами: гейт-переменная батареи с названным скипом (вне make гейт выключался бы сам — зона уже осудила эту форму словами internal/gates/toolchain_test.go:55-57 «the Makefile is a convenience… a build that skips the battery gets a toolchain the battery would have refused»; плюс это легализует поломку рецепта как «условие среды») · переезд проверки на репо-уровень в counts.py как ЗАМЕНА (единственная репо-точка принуждения НИКОГДА не блокирует, только предупреждает — гейт остался бы без зубов; как ВТОРАЯ сеть законен) · кодоген из спеки (посылка протухла: oapi-codegen пере-подписан D39.132 в КАНДИДАТА и P7 решил НЕ БРАТЬ с доводом; и класс он не убивает, а переносит — генерат коммитится внутрь модуля, то есть второй источник истины) · вендорить снапшот канона в модуль (то же самое) ⚠ Рецепт вместе с доводом про маскировку дельты и правилом топичности вердикта записан пунктом 3 в ENGINEERING_STANDARDS §3 — долговечный зонный носитель, а не промт пака (промты после лендинга архивируются) open ревью-пак P8-REVIEW (координатор пака, цена замерена этой же сессией)
PD-281 bug info internal/pgstore/readmodel.go runProgress Полоса прогона над книгой, уже полной в считаемом проходе, стоит на 0/N и не достигает единицы — против канона §Progress («the fraction always reaches one»). Штатный цикл: книга доведена до конца → пользователь подписал решения → запустил прогон, чтобы движок их применил → прогресс платной работы невидим до ready. Остаток подхода «числитель считает ГЛАВЫ, законченные проходом», а не регрессия: до правки PD-263 бар был неверен в другую сторону. Лечение требует продуктового решения (считать главы, ПЕРЕразрешённые после started_at), а не тихой правки ⚠ Дописка пака P9 (27.08), диспозиция: НЕ ТРОНУТА. Сквозная полоса (строка 200) переписала runProgress в runDone/runTotal, но ровно для сценария этой строки — книга полна в считаемом (последнем) проходе, значит и в черновом — ничего не меняет: обе базы равны счёту глав, draftWork = C, полоса стоит 0/2C и до единицы не доходит; числитель по-прежнему считает завершения глав от баз прогона. Знаменатель сменился только для промежуточного класса «черновик впереди редактуры», который эта строка не описывает. Живой факт того же пробоя: старт нового прогона над ПОЛНОЙ книгой сегодня вообще отвечает 409 (шкале нечего продать, ChaptersLeft = 0) — то есть у правки готовой книги нет и входа, которым её сложили бы в прогон; это смежный продуктовый вопрос той же строки. Лечение прежнее — продуктовое решение «считать пере-разрешения после started_at». Канонному минору к полосе НЕ наследовать обещание «the fraction always reaches one» без этой оговорки open приёмка правок P7 (fable-5)
PD-297 bug info internal/pgstore/readmodel.go writeChapters/writeUnits Материализация дерева делает один round-trip на СТРОКУ под эксклюзивной блокировкой книги — на корпусной книге (2283 главы, ~7 тыс. пар) это ≈11 тыс. последовательных обращений, и всё это время за блокировкой стоят emitFrame потока, StartRun и фолд юнитов. Штатный инструмент — tx.SendBatch (pgx v5, уже драйвер модуля) или CopyFrom во временную таблицу. НЕ сделано осознанно: рефутеры первой приёмки понизили до DOUBT/LOW, цена не замерена на форме этого деплоя (unix-сокет против управляемого PG по TCP — разница на два порядка), а путь — самый опасный на запись. Мерить прежде правки: время удержания блокировки на 2283-главной книге до и после ⚠ P8-FIX: НЕ ВЗЯТ, причина названа и она не «не успели». Сама эта строка объявляет замер на здешнем стенде НЕпредставительным (unix-сокет против управляемого PG по TCP — разница на два порядка), а корпусной книги нет: она появляется на холодном прогоне движка, которым гейчена строка 202 единого бэклога (решение владельца 20.08). Мерить нечем и не на чем, а правка самого опасного на запись пути без замера — ровно то, что эта строка запрещает. Берётся вместе с холодным прогоном open доработка 20.08 (сверка находок против дерева)
PD-298 bug info internal/pgstore/readmodel.go ListNotes, internal/pgstore/sink.go unitDone Снятие флага с замечания дельта-чтение выразить не может. Резолюция, пере-разрешённая как не-flagged (редрайв), обновляет строку и двигает revision, но дельта фильтруется предикатом ur.flagged — строка не возвращается, и клиент никогда не узнаёт, что замечание снято: оно остаётся на экране навсегда. Канон §AfterVersion: «A DELETION cannot be expressed this way», и требует одного из двух ответов — resync_required либо 400 version_too_old; здесь не даётся ни один. ⚠ НЕ подтверждено, что движок вообще пере-издаёт unit_done для той же тройки (глава, юнит, волна) с flagged=false — комментарий sink.go это УТВЕРЖДАЕТ («a redrive re-attacks a flagged one»), но чтением движка не сверено. Порядок: сначала сверка у движка, потом либо счётчик замены для замечаний, либо строка «переход недостижим» ⚠ ДИСПОЗИЦИЯ АКТА 5 (сверка с движком контрактной сессией 20.08): переход НЕДОСТИЖИМ и механизма не строим. Движок объявляет вердикт юнита один раз на волну, его announce-once-леджер не пере-announce-ит, поэтому единственная пере-доставка — ТОТ ЖЕ вердикт. Инвариант записан в коде (sink.go unitDone): если что-то научится снимать флаг, фолд обязан выдать кадр — иначе счётчик разъедется со списком молча. Строка держится открытой этим долгом, а не живым дефектом open приёмка P7 → доработка 20.08 (сверка)
PD-299 standards info internal/httpapi/conditional.go acceptsGzip Accept-Encoding: identity;q=0 не отвечает 406. Клиент, потребовавший ЛЮБОГО кодирования кроме identity, получает identity. Половина RFC 9110 §12.5.3, которую правка PD-268 не закрыла: gzip-сторона (именованное кодирование выигрывает у *, нулевой вес — отказ) закрыта и пиньётся, эта — нет. Достижимо только специально сконструированным клиентом; ни один генерённый по контракту клиент так не делает open доработка 20.08 (сверка находок против дерева)
PD-368 hardening info internal/config/config.go, internal/runs/reconcile.go phaseBudget Пара TM_PLATFORM_SWEEP_BUDGET/TM_PLATFORM_RUN_BUDGET не проверяется на когерентность на буте, и поднять ОДИН из них — тихий no-op: фазе достаётся половина прохода, поэтому любое значение RunBudget от половины прохода и выше наблюдаемо неотличимо от дефолта. Ревью заходило сюда как в major («поднять бюджет = объявить здоровый прогон застрявшим») и это опровергнуто исполнением: поднятие ручки не меняет вообще ничего, вердикт при дефолтах существует и без оператора, а строгая проверка RunBudget < SweepBudget/2 отвергла бы сами шиппящиеся дефолты (60 с против 60 с). Остаётся эргономика: поднимать надо SweepBudget, и об этом не сказано нигде, кроме доккоммента ⚠ ПАК P8-REVIEW 24.08: остаток («поднимать надо SweepBudget, и об этом не сказано нигде, кроме доккомментария») УЖЕ ЗАКРЫТ, и закрыт тем же паком, чья приёмка эту строку написала. deploy/README.md несёт таблицу обеих ручек и прямо под ней ⚠-абзац «Поднимать надо ПАРУ, а не одну… RUN_BUDGET выше половины SWEEP_BUDGET наблюдаемо ничего не меняет», со ссылкой на этот же номер. Верным остаётся только то, что когерентность пары не проверяется на буте. ⚠ Связанное, но ДРУГОЕ: сама ручка не покрывает такт свипа целиком — PD-387 ⚠⚠ Уточнение рефутера, меняющее диспозицию: ЗАКРЫВАТЬ строку ЦЕЛИКОМ нельзя, только сузить. Абзац рантбука приехал коммитом 31f1f82ТЕМ ЖЕ, который внёс саму строку, то есть остаток родился уже закрытым. А проверки когерентности пары на буте по-прежнему нет: internal/config/config.go:460-463 грузит обе ручки без сверки. Сузить до этого и оставить open. ⚠ И соседство: абзац стоит под строкой deploy/README.md (греп деньги В СВОЕЙ транзакции), которая сама предмет открытой PD-387 open воркфлоу-ревью волны 2 (P8-FIX), находка опровергнута, остаток зафиксирован
PD-373 doc info internal/readmodel/readmodel.go:64=const maxAttempts = 5, deploy/README.md:577=После пяти неудач У числа попыток материализации ДВА носителя и ничего между ними. Код держит const maxAttempts = 5, рантбук оператора пишет «После пяти неудач долг списывается». Посажена мутация оркестратором вне списка автора: maxAttempts 5 → 500000, батарея зелёная — пин readmodel_test.go ездит for attempts := range maxAttempts, то есть доказывает МЕХАНИЗМ при любом значении константы, что само по себе правильно. Незакрытым остаётся другое: подняли константу — рантбук молча начал лгать оператору о том, когда платформа сдаётся. ⚠ Тот же ход у runs.StalledAfter проверен и НАРУШЕНИЯ НЕ ДАЛ: там носитель ровно один (рантбук пишет «сколько неудач подряд» без числа), мутация тоже выжила и это законно. Лечение — либо гейт на второй носитель ровно той формы, что пак построил для ContractVersion (internal/gates/contract_test.go читает канон, а не копию числа), либо число уходит из прозы open приёмка P8-FIX (посадка мутации оркестратором №18)
PD-374 doc info docs/STACK_DECISIONS.md «Гейты батареи», internal/runner/systemd_test.go systemdOrSkip, Makefile цель check Рецепт объявляет у батареи ДВА гейта и ждёт «скипов 0» — а условий три, и третье не названо. Кроме TM_PLATFORM_TEST_DSN и пары TM_PLATFORM_TEST_ENGINE_BIN/_BOOK_TEMPLATE есть systemdOrSkip: без ДОСТИЖИМОГО пользовательского менеджера systemd три теста internal/runner скипаются. Поймано пере-прогоном батареи при приёмке: на этом хосте /run/user/1000 не существует (сессия logind не поднята), поэтому «скипов 0» недостижимо в принципе — make check дал 18 пакетов, exit 0, линтер 0 issues и 3 скипа. Мимо: сама цель check печатает над списком скипов «set TM_PLATFORM_TEST_DSN», отправляя читателя к ручке, которая тут ни при чём. Отчёт пака честен и это подтверждает — он мерил отдельно и получил 0, что верно на хосте с живым менеджером. Лечение: назвать третий гейт в рецепте вместе с двумя и не обещать «скипов 0» без него; заодно сделать сообщение цели check не называющим одну переменную из трёх ⚠ ПАК P8-REVIEW 24.08: половина лечения УЖЕ в дереве. docs/ENGINEERING_STANDARDS.md §3 п.1 переписан и называет ТРИ условия поимённо, включая достижимый пользовательский менеджер systemd, со ссылкой на этот номер. Названные строкой носители не тронуты: docs/STACK_DECISIONS.md по-прежнему пишет «Гейты батареи — их ДВА» и «скипов 0», а сообщение цели check в Makefile называет одну переменную из трёх. Предложение пака: сузить до этих двух. ⚠ На ЭТОМ хосте все три условия выполнимы (/run/user/1000 жив), поэтому пак снял базовую линию со скипами 0 — см. docs/p8-review/battery-final.logПАК P13 03.09: половина Makefile вылечена в дереве. Цель check над списком скипов печатает условия хоста через новую цель make conditions, и перечень ВЫВОДИТСЯ из тестовых исходников по ВЫЗОВУ, а не по имени в прозе: os.Getenv("TM_PLATFORM_TEST_…") даёт переменные, exec.LookPath("…") — бинари, которых требуют хелперы (systemd-run, python3, make), плюс своя проба systemdOrSkip (достижимый пользовательский менеджер systemd); отдельной строкой назван CREATEDB, который спрашивается только при создании скретч-базы и пробой не проверяется. Литерала с одной переменной больше нет. Гейт internal/gates TestTheBatteryNamesEveryHostConditionItsTestsRead держит цель к исходникам: краснеет, когда перечень перестаёт выводиться (литеральный список), когда колонка состояния печатает слово вместо ответа хоста и когда колонка «read by» приписывает условие пакету, который его не читает. ⚠ Новая переменная в любом тесте гейт НЕ краснит — цель и гейт выводят её из одних и тех же исходников, поэтому она появляется в обоих; краснеет именно ОТКАЗ от вывода. STACK_DECISIONS «Гейты батареи» уже называет четыре условия. Статус — акт лендинга open приёмка P8-FIX (пере-прогон батареи оркестратором №18)
PD-6 hardening info internal/auth/csrf.go:51 GET освобождён от CSRF (верно), но SSE-хендшейк — GET с амбиентной кукой: origin-чек хендшейка потока (STACK §5) не покрыт ничем. Закрыть при постройке SSE (P1) ⚠ ПАК P8-REVIEW 24.08: условие закрытия НАСТУПИЛО, лекарство не приехало, а защита оказалась ТРАНЗИТИВНОЙ. SSE построен (internal/httpapi/v0.go маршрут /books/{bookId}/events, internal/httpapi/stream.go streamEvents, коммит 9b23e8c), origin-чека не появилось: GET освобождён в internal/auth/csrf.go cookieUnsafe, а http.CrossOriginProtection судит только unsafe-методы. Живая проба: хендшейк с Origin: https://evil.example и амбиентной кукой отвечает 200 и стримит, без куки 401, заголовок X-TM-Client не требуется (docs/p8-review/sse-origin-probe.txt). ⚠ Вес поднимать НЕ предлагается, и это измеренная поправка к предложению аудита: браузерный случай сегодня закрыт ТРЕМЯ механизмами, ни один из которых не является чеком хендшейка — кука SameSite=Lax не уходит на кросс-сайтовый подзапрос, префикс __Host- не отдаёт её соседнему поддомену, а CORS-слоя нет вовсе (PD-96), поэтому кросс-origin EventSource браузер странице не отдаст. Это ровно класс PD-86: свойство держится, и держат его посторонние механизмы, ни один из которых не запинен как защита хендшейка. Диспозиция — вопрос приёмке: чек хендшейка или явное принятие с записью трёх носителей open приёмка P0 (security-линза)
PD-23 hardening info internal/pgstore/migrations/00001_identity.sql Журнал входов растёт без ретенции и чистится только каскадом при удалении аккаунта. Нужен свип по возрасту (год?) — вопрос политики, не кода ⚠ ПРОВЕРЕНО ПАКОМ P8-REVIEW 24.08 И ПРОТИВ ДЕРЕВА ЛОЖНО: ретенция ПОСТРОЕНА и РАБОТАЕТ. internal/pgstore/identity.go DeleteOldLoginEvents (delete from login_events where at < $1) зовётся из cmd/tmplatformd/main.go циклом sweepLogins каждые 15 минут с окном loginJournalRetention = 180 * 24 * time.Hour, и свип монтируется в ОБЕИХ ветках входа. Приехало коммитом 9b23e8c (лендинг P7), строка не обновлялась с P1. Доказано не чтением: строка возрастом 200 суток исчезла на ближайшем тике, демон напечатал "login sweep" events=1 (docs/p8-review/pd23-result.txt). Предложение пака: ЗАКРЫТЬ, срок 180 суток отметить как выбранную политику. Незапиненность самого механизма вынесена отдельной строкой PD-383 open самопроверка P1
PD-45 hardening info internal/ingest/procgroup_unix.go syscall.Kill(-pid, SIGINT) идёт мимо os.Process, поэтому в узком окне между проверкой живости и сигналом ребёнок может быть пожат, и сигнал уйдёт в переиспользованную группу. Окно ~микросекунды и родитель ещё не звал Wait; переписывать на pidfd-путь — отдельная работа open ревью «вне карты»
PD-86 hardening info internal/pgstore/queries/sessions.sql LookupSession / TouchSession Два клауза-близнеца не запинены, и абсолютный потолок держится ТРАНЗИТИВНО: снятие absolute_expires_at > $2 из Lookup батарею переживает, потому что потолок навязывается через least($3, absolute_expires_at) в Touch (это запинено — TestSessionLifecycle). Снятие revoked_at is null из Touch тоже переживает (класс PD-4). Дефекта сегодня нет ни в одном; риск в том, что каждый слой по отдельности выглядит избыточным, а вместе они — единственное, что ограничивает жизнь сессии ⚠ Указатель пере-нацелен паком sqlc (63fcee5), и это НЕ смена дефекта: оба клауза уехали из sessions.go:23,50 в queries/sessions.sql, потому что SQL этих запросов теперь генерируется. Прежний указатель counts.py --lint не ловил — у него нет =токена, так что он проверял только существование файла и молча указывал на строки, где этих клауз давно нет. Сами клаузы целы и по-прежнему не запинены open приёмка P2 (посадки мутаций)
PD-87 hardening info internal/httpapi/server.go:82, internal/login/login.go:31 Ещё два незапиненных: снятие LimitBody с поддерева /auth и stateTTL 10 мин → 240 ч проходят батарею. Первое — родня PD-72 (та про общий внешний слой, эта про конкретное поддерево), второе — окно жизни неиспользованного авторизационного запроса open приёмка P2 (посадки мутаций)
PD-88 bug info internal/auth/cookie.go:62-66 TTL меньше секунды выпускает куку БЕЗ атрибута Max-Age: int(ttl.Seconds()) даёт 0, а Go при MaxAge == 0 атрибут опускает ⇒ кука становится браузер-сессионной. Достижимо в последнюю секунду абсолютного срока (скольжение выдаёт min(idle, остаток абсолютного) при гарде ttl > 0) — то есть ровно тот исход, который самопроверка P2 называла нежелательным: кука переживает сессию, и следующий запрос даёт 401 вместо чистого «вы вышли». Подтверждено исполнением (ttl 500 мс/999 мс) open приёмка P2 (панель ×2, подтверждено исполнением)
PD-90 bug info cmd/tmplatformctl/main.go (греп case "books": и Idempotency is OPT-IN) grant и adjust делят пространство ключей source="admin": --key, потраченный грантом, молча гасит корректировку с тем же ключом. CLI честно скажет «ключ уже потрачен», но оператор ждал другой операции ⚠ ПАК P8-REVIEW 24.08: якорь дрейфанул. cmd/tmplatformctl/main.go:112 сегодня это case "books":; пара, которую строка описывает, живёт на :149=return store.Grant(ctx, *user, micro, "admin", id, *note, now) и :171=return store.Adjust(ctx, *user, micro, "admin", id, *note, now). Суть верна: одно пространство ключей source="admin" open приёмка P2 (панель)
PD-92 bug info internal/ingest/supervisor.go:118-121 Дренаж стоит ДО cmd.Wait(), поэтому WaitDelay его не размораживает: io.Copy(io.Discard, stdout) ждёт EOF, а EOF придёт только когда закроются ВСЕ копии пишущего конца пайпа; внук, унаследовавший stdout и игнорирующий SIGINT, вешает Run навсегда — backstop WaitDelay действует внутри Wait, до которого управление не доходит. ⚠ Сегодня недостижимо и это пере-проверено приёмкой: в backend/ вне тестов нет ни одного exec.Command и нет cgo. ⚠ Пере-диспозиция (эррата №15): вес понижен до info — весь пайп-путь supervisor.go объявлен ДЕВ-РЕЖИМОМ (D39.106 п.3), в проде родителя у движка нет и cmd.StdoutPipe() не существует; чинить только если дев-путь остаётся open приёмка P2 (панель, граница зоны пере-проверена)
PD-93 bug info internal/ingest/supervisor.go:109 Фикс PD-77 («наша остановка — не сломанный синк») сверяет только context.Canceled и пропускает context.DeadlineExceeded: как только у runCtx появится дедлайн (потолок времени прогона — очевидная будущая ручка), штатное истечение снова поднимет ERROR «stream could not be materialized» open приёмка P2 (панель)
PD-94 bug info internal/httpapi/middleware.go:61-70 Recover глотает http.ErrAbortHandler — sentinel, которым хендлер намеренно обрывает соединение (net/http его не логирует и рвёт коннект). Замерено приёмкой: паника ErrAbortHandler превращается в 500 с problem-телом, то есть усечённый поток становится неотличим от полного. Латентно (сегодня им никто не паникует), но именно SSE-хендлер — типовой его пользователь open приёмка P2 (замер оркестратора №15)
PD-96 hardening info internal/httpapi/server.go:39-43, internal/auth/csrf.go:28 TrustedOrigins обещает отдельно развёрнутый фронт, но CORS-слоя нет вовсе. Живая проба: preflight OPTIONS с Origin: https://app.example.org получает 401 от гарда (браузерный preflight креденшелов не носит и не должен), заголовков Access-Control-* нет ни на одном ответе. Сценарий «фронт на другом origin» браузером сегодня неисполним: либо CORS приезжает вместе с контрактными ручками (П-1), либо фронт живёт на том же origin, и тогда TrustedOrigins — мёртвая ручка open приёмка P2 (панель + живая проба)
PD-98 doc info internal/pgstore/store.go:75-79 Случай «схема НОВЕЕ бинаря» в Ready беззвучен — признано ⚠-комментарием на месте, но ни одной строки лога: оператор, запустивший старый бинарь на новой схеме, сигнала не получит open приёмка P2 (панель)
PD-107 hardening info internal/pgstore/migrations/00007_credits.sql:85, 00002_readmodel.sql:13 Удаление аккаунта обходит защиту PD-25: составной FK reservations → books(id, owner_id) on delete restrict блокирует DeleteBook, но users каскадит в reservations НАПРЯМУЮ, поэтому delete from users уносит и ОТКРЫТУЮ резервацию. Замерено приёмкой: аккаунт с открытым холдом удаляется. Учётной дыры нет — леджер и кэш баланса каскадятся тем же удалением, — но прогон, идущий против этого холда, останется без того, кто его закроет. Кода удаления аккаунта в дереве нет вовсе (грепнуто) ⇒ строка = гейт перед появлением такой операции (и перед ASVS 7.4.2 в полной форме). ⚠ Заодно ОПРОВЕРГНУТА обратная версия этой находки от панели («удаление падает на композитном FK даже при закрытых резервациях») — мой прогон: удаляется и с закрытой резервацией, и без неё open приёмка P2 (замер оркестратора №15; версия панели опровергнута)
PD-122 bug info internal/pgstore/books.go ListBooks Library.revision НЕ монотонна: она выведена как max(books.revision) по книгам аккаунта и падает, когда удаляется книга, державшая максимум. Контракт требует монотонности внутри области и предписывает клиенту ОТБРАСЫВАТЬ чтение с меньшей ревизией — то есть после такого удаления библиотека замирает, пока чей-нибудь книжный счётчик не перерастёт старый максимум. Замерено ревью: 10 → 0 после удаления книги. ⚠ Сегодня недостижимо через API: ручки удаления книги нет вовсе (DeleteBook есть в сторе, маршрута нет). Правильное решение — собственный счётчик области у аккаунта, который двигается на изменение состава и статусов. ⚠ Уточнено ревью доков 09.08: КОЛОНКА уже есть — users.library_revision из 00001_identity.sql:13, и её не читает и не пишет ни один Go-путь (грепнуто), так что нужна не миграция, а пути записи и чтения; правка нескольких мест, поэтому она НЕ сделана в этом паке, а названа. ⚠ Сужено паком P5: ДОБАВЛЕНИЕ книги ревизию области двигает — новая книга входит в библиотеку с revision = max(revision по владельцу) + 1 (pgstore.nextLibraryRevision, обе точки входа: дев-интейк и загрузка), и каждая смена статуса интейка её тоже двигает; пин books.TestABookThatJoinsTheLibraryMovesItsRevision, посадка «входить с нулём» падает. Открытым остаётся исходный случай: УДАЛЕНИЕ книги может уронить максимум, и лечится это собственным счётчиком области. Ручки удаления по-прежнему нет. Гейт строки: закрыть ДО появления ручки удаления книги — обратная сторона этой же пары стоит в PD-175 (греп гейт PD-122) ⚠ ПЕРЕ-ФОРМУЛИРОВАНА дофиксом приёмки (FP5-1): клейм «добавление закрыто» был ЛОЖЕН. Пол области поднимался при удалении, а вставка читала только максимум — книга, загруженная после отменённой загрузки, входила НА или НИЖЕ пола, и одна ревизия отвечала за три разных состояния библиотеки. Теперь вставка берёт greatest(max, пол) + 1, пин books.TestABookThatJoinsAfterACancelledUploadStillMovesTheRevision гоняет именно сценарий «отмена → повторная загрузка». ОТКРЫТЫМ остаётся: смена статуса книги, которая НЕ самая новая, ревизию области не двигает — ни одна из половин не растёт; лечится тем же счётчиком области, который двигают все писатели состава и статусов ⚠ Дополнено ре-чеком (FP5-11): правило «брать следующий номер БИБЛИОТЕКИ» применено и к пяти писателям жизненного цикла прогона (старт, реоткрытие, пауза, два закрытия) — без них staleness оставалась на статусах прогона: книга не-максимума аккаунта уходила translating, а библиотека не двигалась. Пин pgstore.TestARunTransitionOnAnOlderBookStillMovesTheLibrary. Прогресс-путь синка НАМЕРЕННО не тронут (главная шкала считается от книжной до инкремента; прогресс бывает только у книги с идущим прогоном, а её старт уже поднял номер) open (добавление закрыто P5; удаление — нет) адверсариальное ревью (исполнением)
PD-123 doc info internal/pgstore/migrations/00009_runner.sql:11 runs.ceiling_chapters имеет default 0, а контракт объявляет Run.ceiling_chapters minimum: 1. Сегодня недостижимо: единственный путь вставки — StartRun, и он отказывает на неположительном значении. Строка заведена как гейт: строка прогона, записанная мимо StartRun (миграция данных, правка оператором), спроецируется на провод нулём, которого схема клиента не допускает open адверсариальное ревью (чтение схемы)
PD-137 hardening info deploy/tmplatformd.service [Unit] BindPaths=/run/user/%U требует существования каталога на старте юнита, а создаёт его logind вместе с пользовательским менеджером; в [Unit] упорядочения на него нет. На первом бутe это гонка, которую лечит Restart=on-failure (сервис поднимается со второй попытки). Строка не закрыта кодом намеренно: UID сервисного пользователя site-specific, поэтому After=user@<uid>.service добавляется установкой — инструкция вписана в шапку юнита open адверсариальное ревью (чтение)
PD-139 hardening info internal/runs/reconcile.go ERROR-строки Путь каталога книги попадает в ERROR-логи внутри обёрнутых ошибок (*fs.PathError тейлера, обёртки спавна). PD-99 закрывал ДРУГОЕ — argv на INFO, — и та половина проверена (grep по логу сквозной пробы = 0). Здесь диспозиция иная и её надо принять осознанно: оператор чинит именно этот путь, а ERROR — не INFO. Заведено, чтобы это было решением, а не побочным эффектом; если норма зоны распространяется и на ERROR, путь придётся заменить на id прогона ⚠ Дополнено P5: у класса появилась ВТОРАЯ площадка — интейк. Путь книги уходит в ERROR только в двух местах и намеренно: терминальный отказ разбора (err движка несёт путь исходника) и неудавшееся удаление каталога. На INFO/WARN идентификатора книги нет вовсе, и это запинено books.TestNoBookIdentifierReachesAnInfoLine. Диспозиция по-прежнему нужна одна на класс open адверсариальное ревью (чтение)
PD-153 bug info internal/runs/reconcile.go spawnGrace Грация спавна мерится от run_attempts.started_at, а не от момента заявки права на спавн: окно между RecordSpawn и возвратом systemd-run не покрыто, и второй инстанс платформы, у которого грация уже истекла, может решить, что попытка потеряна, и перезапустить прогон, который вот-вот стартует. Дёшево закрывается временем заявки, записываемым в RecordSpawn, и грацией от него ⚠ Сужено дофиксом приёмки: у СТОП-пути окно закрыто с обеих сторон — заявка на спавн не выдаётся прогону с висящим интентом, а закрытие «стопа до спавна» спрашивает systemd про детерминированное имя юнита, если у попытки есть базовая линия (то есть заявка когда-то бралась). Само окно PD-153 — грация от started_at, а не от момента заявки — не тронуто open приёмка P4 (N2)
PD-154 bug info internal/runs/reconcile.go settle runs.settled_at может остаться NULL между Settle и MarkSettled: это два вызова, и падение между ними оставляет прогон с закрытой резервацией и без отметки. Потребителей у отметки сегодня нет (рабочий список расчёта построен на открытой резервации, а не на ней), деньги целы и второй расчёт отвергается самой резервацией. ⚠ Дофикс 09.08 добавил вторую половину той же строки: settle прерванной попытки ЖИВОГО прогона (путь UnsettledRuns) ставит settled_at прогону, который ещё идёт. Потребителей у колонки по-прежнему нет, дрейф только операторский. Заведено как известность, а не как долг: закрывается вместе с эскроу (строка 136) ⚠ ПЕРЕ-ДИСПОЗИЦИЯ (P6): остаётся открытой в прежней формулировке. Пак трогал settle (запиненный бинарь, аддендум оркестратора 14.08) и окно не закрывал: закрытие требует write-ahead intent и состояния closing, то есть эскроу строки 136, а строить половину эскроу рядом с проектируемым целым — это второй, более слабый ответ на тот же вопрос. Потребителей у колонки по-прежнему ноль open приёмка P4 (N3)
PD-170 hardening info internal/pgstore/credits.go Settle Settle отбрасывает флаг applied строки расчёта, тогда как releaseHold на соседней строке из того же флага делает ErrReleaseKeySpent (PD-97). Недостижимо без правки леджера в обход кода — резервация должна быть открыта, чтобы дойти сюда, — но асимметрия в денежном пути стоит строки: либо симметричный отказ, либо явная причина, почему здесь он не нужен open самопроверка дофикса (ревью вне карты)
PD-176 hardening info internal/httpapi/middleware.go LimitBody, internal/metrics Сигнал requestTooLarge от http.MaxBytesReader до сервера не доходит через наши обёртки. MaxBytesReader пытается сказать net/http «запрос слишком большой» приведением ResponseWriter к НЕЭКСПОРТИРУЕМОМУ интерфейсу пакета net/http; наши обёртки (statusRecorder, metrics.recorder) его удовлетворить не могут в принципе — метод неэкспортируемый, а значит квалифицирован чужим пакетом. Следствие мягкое и замерено рассуждением по коду net/http: 413 отдаётся штатно, а соединение закрывается не немедленным сигналом, а обычным путём — после хендлера сервер дренирует остаток тела и, не сумев дочитать, закрывает соединение, причём дренаж ограничен ReadTimeout (PD-2). Лечение (если понадобится): ставить лимит без обёрток над ResponseWriter либо закрывать соединение самим через Connection: close open сессия P5 (чтение stdlib при постройке маршрута)
PD-177 bug info internal/books/books.go counter, пин — internal/books/counter_test.go TestTheIntakeCounterCountsTheWriteStreamAndNotCharacters ПЕРЕ-ФОРМУЛИРОВАН 05.09 — ряд был УЖЕ ряда: для EPUB это не «оценка», а ДРУГАЯ ВЕЛИЧИНА. Интейк принимает .epub (extensionOf, движок диспетчеризует читатель по расширению), и тогда счётчик считает не-продолжающие байты ZIP-АРХИВА — свойство контейнера, к числу знаков книги отношения не имеющее. Верно ровно для UTF-8-текста; для GB18030/UTF-16 — приближение (прежняя формулировка ряда). Что сделано в зоне 05.09: имя и доккомментарий больше не лгут (counter.runes, не characters), и факт ЗАПИНЕН тестом, чтобы следующий читатель узнал его из батареи, а не с экрана. Что НЕ сделано и не может быть сделано здесь: поле character_count стоит в каноне required с типом [integer, 'null'], и null уже несёт смысл «книга ещё загружается» — значит «не заполнять» ломает и контракт, и потребителя, а лечение требует минора канона (признак точности рядом с числом ЛИБО смена семантики null) или суммы знаков из манифеста движка, которой там пока нет (строка бэклога 278). Родня — строка бэклога 282 character_count для не-UTF-8 источника — оценка, а не счёт. Интейк считает символы потоково как байты, не являющиеся продолжением UTF-8 (b&0xC0 != 0x80), что для валидного UTF-8 ТОЧНО равно числу рун и не требует состояния между чанками. Движок принимает и GB18030, и UTF-16, декодируя их сам — на таком файле цифра неверна (для UTF-16 занижена примерно вдвое). Точный ответ есть на шаг позже: манифест несёт source_bytes и encoding, но не число символов. Лечится либо запросом числа символов у движка (строка единого бэклога), либо перерасчётом после разбора open сессия P5 (названо при постройке)
PD-201 bug info deploy/README.md, движок tmctl migrate Самолечение деплой-деадлока v15 ЖДЁТ движковую половину. Read-only status отказывает файлу проекта старее бинаря, платформа зовёт его перед каждым спавном и на расчёте денег — значит выкат движка запирает книги до tmctl migrate. Порядок апгрейда записан и исполним (tmplatformctl books --migratable пропускает книги с живыми прогонами, резюмируемыми попытками и незакрытыми холдами — все три блокируют по РАЗНЫМ причинам, аддендум оркестратора 14.08). Чего нет: движковый migrate придёт с машиноразличимой ошибкой «версия не совпала», и тогда платформа сможет лечиться сама — поймала → migrate (если у книги нет открытых попыток) → повтор. Вслепую не строится: без формы ошибки любой матчер был бы догадкой по тексту. ⚠ Проверка исполнением самого шага migrate тоже ждёт и НЕ помечена сделанной ⚠ ПОЛОВИНА ЗАКРЫТА P7 (проверка исполнением): рантбук деплоя прогнан end-to-end на стенде живым migrate — проектная БД, отведённая на схему v14 при бинаре v15, даёт tmctl status --json exit 13 с токеном schema_mismatch found=14 expected=15; tmctl migrate делает пред-миграционный бэкап и переводит v14 → v15; тот же status после — exit 0. Мёртвая цитата ошибки вычищена из deploy/README.md и из П-1 бэклога, пример в tmplatformctl/runs.go переписан на ВЕРСИОНИРОВАННЫЙ путь бинаря. ⚠ ОСТАЁТСЯ сам автомат самолечения («поймал 13 → migrate → повтор») — он не построен: строить его правильно значит решать, кто имеет право мигрировать книгу с открытыми попытками, а это тот же вопрос, что у books --migratable open аддендум оркестратора 14.08 (ресёрч деплоя)
PD-204 standards info internal/runner/systemd_test.go мета-пин Мета-пин systemd-гейта ПЕРЕЧИСЛЯЕТ формы отказа вместо того, чтобы спрашивать способность. Хвост (г) третьего раунда, у которого не было носителя. Сам гейт вылечен (PD-178: спрашивает, доходит ли процесс до своего менеджера), а тест, который его сторожит, по-прежнему знает список сообщений — у стража та же болезнь, от которой лечили охраняемого. Принято как ОСТАТОК с названной ценой: systemd меняет тексты между версиями, и мета-пин протухнет молча; вреда сегодня нет, потому что он не гейт, а страж гейта. Заведён по пингу оркестратора 14.08 open третий раунд P5, хвост (г)
PD-205 hardening info internal/login/dev.go login, internal/auth/csrf.go cookieUnsafe Дев-вход защищён от login-CSRF только ОДНИМ слоем из двух. auth.CSRF требует заголовок X-TM-Client лишь на unsafe-запросе, который УЖЕ несёт сессионную куку, а на входе куки по определению нет — значит остаётся только http.CrossOriginProtection, и клиент, не присылающий ни Sec-Fetch-Site, ни Origin (тот самый браузер до 2023, ради которого второй слой и заведён), может кросс-сайтом ввести браузер жертвы в ДЕВ-аккаунт. Последствие на стенде ничтожно — аккаунт один и общий, — но свойство слабее, чем «POST, значит безопасно», и записано, а не подразумевается. ⚠ Тот же класс у боевого /auth/login (он вообще GET) и по той же причине; лечится либо требованием заголовка на login-маршрутах, либо явным принятием open адверсариальное ревью P6 (кросс-семейное, Fable)
PD-212 bug info движок cmd/tmctl/main.go exitCode, internal/runs/reconcile.go outcome Непойманная паника движка неотличима от «завершено с флагами». Go-рантайм завершает процесс с кодом РОВНО 2 на непойманной панике, а 2 — это CompletedWithFlags, единственный «успешный» код контракта; recover в cmd/tmctl/internal/pipeline отсутствует (грепнуто ревьюером). Платформа читает только $EXIT_CODE/$EXIT_STATUS и записывает ready для прогона, который упал посреди работы. Деньги целы (расчёт берёт цифру из status --json), врёт статус. Лечится НЕ здесь: либо recover в main движка, либо другой номер для флагов — запрос уходит строкой единого бэклога через оркестратора. ⚠ Платформа МОГЛА бы различить по отсутствию терминальной строки finished в журнале, но сознательно не судит прогон по строке, которую крэш обрезает ⚠⚠ ПРОТУХЛА 02.09, чиню фактом: ДВИЖОК ЭТО СДЕЛАЛ, и посылка строки («recover отсутствует») больше не верна. backend/cmd/tmctl/main.go несёт exitOfmain идёт под recover, паника заворачивается в obs.NewPanicError и exitCode проверяет *obs.PanicError ПЕРВЫМ, отдавая 1, а не 2 (греп func exitOf и case errors.As(err, &panicked)); волновые воркеры несут свой recover на шве (греп recover() в backend/internal/pipeline/waverun.go). Пины движка — backend/cmd/tmctl/panic_exit_test.go; лендинг f840867. То есть «паника читается как завершено-с-флагами» больше не воспроизводится. Статус не перевожу — закрытие есть акт лендинга и лекарство в ЧУЖОЙ зоне; оркестратору: строка готова к закрытию, либо к пере-формулировке в остаток (различение крэша по обрезанному журналу платформа по-прежнему не строит и строить не собирается) open адверсариальное ревью P6 (линза шва)
PD-215 bug info internal/runs/reconcile.go restart, interruptedBySomeoneElse У петли перезапусков нет ни счётчика, ни бэк-оффа. Каждый цикл — строка run_attempts, три строки леджера (холд/возврат/расчёт), два вызова tmctl status и транзиентный юнит. Источник, который на каждой попытке тратит ~0 (движок, мгновенно падающий по внешней причине), крутит это вечно: remaining не убывает, значит exhausted никогда не наступает. Денег не теряется, но это неограниченная работа и рост таблиц. Класс существовал и до пака (путь ребута, строка 138), а ветка «exit 5 без намерения = прерывание» его РАСШИРИЛА. Лечить счётчиком перезапусков на прогон или бэк-оффом по времени последней попытки open адверсариальное ревью P6 (линза денег и гонок)
PD-216 bug info internal/runs/reconcile.go outcome (полоса отказов) Exit 12 (project_locked) закрывает прогон терминально, хотя это единственный класс полосы, который проходит САМ. Обоснование «отказ воспроизводим по построению, поэтому перезапуск — петля» верно для 10/11/19 и неверно для лока: другой процесс отпустит проект. Денег не теряется (холд возвращается целиком, списывается 0), но прогон убит и пользователь покупает новый. Не исправлено намеренно: перезапуск на 12 без бэк-оффа — это PD-215 в чистом виде, поэтому оба лечатся вместе open адверсариальное ревью P6 (линза денег и гонок)
PD-243 hardening info internal/ingest/resync.go DecodeStatus Декодер принимает null, {} и объект сплошь незнакомых полей за валидный отчёт. На денежных путях это перекрыто (PD-40: Spend == nil откладывает расчёт) и при exit 2 — согласием отчёта (PD-237); остаётся eta_seconds, который присваивается безусловно, так что пустой отчёт стирает ETA живого прогона. Лечится проверкой того же класса, что в манифесте: отчёт обязан называть книгу open кросс-семейное ревью дофикса P6 (линза шва)
PD-244 bug info internal/runs/reconcile.go settle Единственный выход settle, который оставляет холд открытым молча: при s.Engine == nil возвращается nil — ни возврата, ни MarkSettled, ни строки в логе. В проде недостижимо (cmd/tmplatformd/runner.go всегда ставит Engine), но это ровно та тишина, из-за которой класс PD-162 искали глазами open кросс-семейное ревью дофикса P6 (денежная линза)
PD-246 standards info internal/ingest/notes.go, движковый pipeline/status.go flagReasonSeverity Платформа держит РУКОПИСНУЮ КОПИЮ закрытого словаря чужой зоны, и единственный страж — строка в логе. Флаг-причины принадлежат движку (flagReasonSeverity, 15 значений), карта «причина → контрактный код» живёт на платформе, а выпускаются две программы независимо. Значит существует окно, в котором движок уже эмитит причину, которую эта сборка не знает, и расхождение видно только по ERROR в логе — то есть тогда, когда кто-то его прочитает. ⚠ Проводу окно закрыто и это НЕ дефект: причина вне карты проецируется кодом unspecified, а канон уже предписывает клиенту нейтральную фразу для незнакомого кода — то есть значение отдано ветке, которая ратифицирована, а не изобретено правило. Открытым остаётся ДВОЕ: (1) фразы для unspecified в приложении А нет (пишет владелец, строка 148) — как и ступеней у всех 15 строк, где граница attention/glance сегодня догадка платформы; (2) гейта на расхождение нет и со стороны платформы быть не может — импортировать backend/internal запрещено ревью-гардом модулей (D39.85). Предложение зоны (пинг оркестратору, строкой в бэклог ДВИЖКА): публиковать список флаг-причин данными — артефакт рядом с манифестом либо tmctl flag-reasons --json ($0, таблица уже существует), — тогда тест платформы читает его и ПАДАЕТ, если в карте нет строки. Класс «словарь разъехался тихо» закрывается насовсем. ⚠ Обратное решение — чтобы движок эмитил сразу контрактный код — отвергнуто с доводом: это зеркальная утечка, продуктовое слово (source_residue, term_not_applied) поехало бы в движок, который о контракте знать не должен, и отменило бы причину самого переименования ⚠ ЗАКРЫТА ЧАСТЬ ПРО ПЛЕЙСХОЛДЕР (акт 5, ответ контрактной сессии 20.08): unspecified ратифицирован как ОБЯЗАННОСТЬ сервера, а не строка чужого словаря — то, что зона уже отдаёт, стало легальным. Открытым остаётся то, ради чего строка заведена: рукописная копия закрытого словаря движка и отсутствие стража, кроме строки в логе ⚠ СОБЫТИЕ, РАДИ КОТОРОГО СТРОКА ЗАВЕДЕНА, НАСТУПИЛО — 04.09, и оно поймано ПИНГОМ, а не стражем. Бэкенд-сессия (textmachine-main-7e) завела ШЕСТНАДЦАТУЮ причину FlagOffTargetLang = "off_target_lang" (backend/internal/pipeline/disposition.go) — ответ не на том языке, который просили, и это не эхо исходника. ⚠ Счёт пере-снят СВОЕЙ рукой, а не принят на слово: grep -c '^\t"[a-z_]*":' internal/ingest/notes.go15 ключей, flagReasonSeverity движка на HEAD → те же 15 имён, шестнадцатое живёт пока только в НЕЗАЛЕНДЖЕННОМ дереве бэкенда. То есть окно, которое строка описывает, открыто ровно сейчас, и предсказанная деградация верна: причина вне карты проецируется в unspecified и пишет ERROR в лог. Карту НЕ дополняю, и это не лень: контрактного кода у новой причины ещё нет, а изобрести его значило бы править приложение А канона своей рукой — состав минора идёт оркестратору на ратификацию (off_target_lang → новый код замечания + ступень). И самое существенное: класс поймал не гейт, а ПИСЬМО соседней сессии — то есть стража по-прежнему нет, и предложение зоны (движок публикует причины ДАННЫМИ, строка 204 единого бэклога) остаётся единственным, что закрывает его насовсем ⚠ СОБЫТИЕ, КОТОРОЕ ЭТОТ РЯД ПРЕДСКАЗЫВАЛ, НАСТУПИЛО 04.09 — и ряд остаётся ОТКРЫТЫМ, потому что наступление не есть лечение. Движок завёл шестнадцатую причину off_target_lang, канон ратифицировал ей код wrong_language (минор 0.10.0, D39.194), а рукописная карта платформы знала пятнадцать — то есть причина ехала читателю как unspecified со ступенью «взгляд», и единственным стражем был предсказанный этим рядом лог (internal/httpapi/reading.go «notes carry a flag reason this build cannot name»). Строка добавлена, батарея зелёная, но окно между двумя независимо выпускаемыми программами закрывает не строка, а механизм, которого по-прежнему нет. Замер стоимости окна теперь есть: между лендингом движковой причины и лендингом платформенной строки прошёл один рабочий день, и всё это время код на проводе был ложным. open сессия P7 (самопроверка против приложения А)
PD-247 bug info internal/httpapi/stream.go pump Живой поток ОПРАШИВАЕТ базу дважды в секунду на каждое открытое соединение (кадры + состояние книги). Кадры минтят писатели в своих транзакциях и в других процессах, поэтому подписки у этой стороны нет; выбран опрос, а не LISTEN/NOTIFY, потому что кадр это ПОКА, и секунда задержки на поке не наблюдаема рядом с переводом. Цена названа числом: 12 вкладок на книгу = 24 запроса/с к Postgres, оба по индексу и по одной книге. Лечится pg_notify в emitFrame + один слушатель на процесс — работа на полдня, которая нужна не раньше второго десятка одновременных читателей open сессия P7 (собственная оценка)
PD-248 bug info internal/readmodel/readmodel.go Refresh Материализация читающей поверхности стоит ДВУХ полных ре-чанков исходникаtmctl manifest --json и tmctl export --json --pairs, каждый из которых заново ингестит и режет книгу (~1,41,5 с CPU на 23 МБ, строка 100 единого бэклога). Зовётся на границах работы (конец интейка, конец прогона), то есть не на запрос пользователя, но на книге в 2283 главы это секунды CPU и десятки мегабайт JSON через пайп на каждый конец прогона. Дешевле было бы читать сайдкар манифеста напрямую (он уже лежит рядом с БД проекта) и просить у движка экспорт ТОЛЬКО изменившихся глав — второго канала у движка нет, это запрос строкой единого бэклога Доработка 20.08: интейк больше не платит за ТРЕТЬЮ ре-нарезку — books.Parse передаёт уже прочитанный манифест в readmodel.RefreshCut; остаются два (манифест + экспорт), и это цена самих каналов. open сессия P7 (собственная оценка)
PD-249 bug info internal/pgstore/runs.go ReadRunForSpawn Чтение прогона под спавн линейно по числу ЖИВЫХ прогонов: читает их все и ищет нужный в цикле. Сегодня незаметно (живых прогонов единицы), но это O(живых) на КАЖДУЮ задачу спавна, и растёт ровно тогда, когда платформа становится нужной. Находка §9 контракт-ревью (research/28), проверена чтением кода: запрос действительно без предиката по id open research/28 §9 (пинг оркестратора №17), сверено P7
PD-251 bug info internal/login/login.go (лимитер входа) Лимитер входа один на ПРОЦЕСС, а не на адрес: один клиент, долбящий /auth/login, расходует общее ведро и запирает вход всем. Выбор объяснён комментарием (за прокси адрес клиента без доверенного X-Forwarded-For — это адрес прокси, и пер-адресное ведро тогда защищает не то), но следствие не было записано. Лечение появляется вместе с доверенным заголовком прокси на деплое ⚠ ПАК P8-REVIEW 24.08: это ТОТ ЖЕ факт, что PD-42, и он стоит в регистре в ДВУХ статусах одновременно. PD-42 (accepted-risk(платформа P1, 05.08)) описывает то же самое ведро с замером «8 отказов из 10 при фоне 5 rps», и код один — internal/login/login.go startLimit с комментарием «Deliberately GLOBAL rather than per-address». Клейм этой строки «следствие не было записано» неверен: оно записано раньше и принято. Предложение пака: свести — либо закрыть эту дублем, либо снять принятие риска с PD-42 ⚠⚠ Условие сведения, найденное рефутером и обязательное к переносу: НИ ОДНА из двух строк не называет ВТОРОЕ глобальное ведро. Кроме startLimit на /auth/login есть finishLimit на /auth/callback (internal/login/login.go:71,129,232) — тот же дефект на второй половине потока входа, и он не покрыт ни одной строкой регистра. Сводить PD-251 в PD-42 можно только назвав в объединённой строке ОБА ведра и ПЕРЕ-подтвердив принятие риска явно, а не унаследовав его: иначе открытый баг молча превращается в принятый риск, а половина дефекта исчезает из регистра вовсе open research/28 §9 (пинг оркестратора №17), сверено P7
PD-252 bug info internal/runs/reconcile.go (карантин проекции) Сырой Go-текст ошибки сохраняется причиной карантина в пользовательской строке БД. На провод он не идёт (карантин не проецируется ни одним полем контракта), но это движковый и внутренний текст в таблице, которую читают дампами; тот же класс, что Problem.detail, только в базе. Лечение — закрытый словарь причин карантина, как у books.reject_reason open research/28 §9 (пинг оркестратора №17), сверено P7
PD-256 bug info internal/ingest/export.go DecodeExport Сигнал дрейфа конфигурации движка не читается. tmctl export отдаёт config_drift/current_snapshot — «текущая конфигурация рендерит не тот снапшот, что несут сохранённые строки», то есть диспозиции могут не описывать то, что прогон делал. Платформа материализует текст без этого признака: на проводе места для него нет (и не должно быть — это операторский факт), но в логе материализатора он был бы уместен open кросс-модельное ревью P7 (линза шва)
PD-345 hardening info internal/runner/engine.go readEngine, firstLine stderr движка читается БЕЗ потолка, вплотную к намеренному потолку на stdout. doc берётся через io.LimitReader(out, limit+1) и переполнение отдельно судится (drain, «a cap that is at the mercy of the thing it bounds is not a cap»), а errOut — простой bytes.Buffer, и firstLine отдаёт из него префикс любой длины. Асимметрия и есть находка: рядом стоит комментарий, объясняющий, почему потолок обязателен, и на соседнем канале его нет. Сегодня цена ограничена (процесс короткий, движок свой), но строка уезжает в ошибку, в лог и — до P8-FIX — в колонку БД (это половина PD-340, там закрыта на стороне хранения). ⚠ Найдено ревью P8-FIX и НЕ чинится этим паком: вне его скоупа, названо, чтобы не потеряться open ревью P8-FIX (контракт-конформность, соседство с PD-340)
PD-245 standards info internal/ingest/supervisor.go У дев-супервизора нет ни одного потребителя вне собственных тестов (grep по зоне): это дев-путь D39.106 §3, который пережил постройку продового шва. Его чинят и держат в шаге с продовым (PD-224, PD-237) — но либо он должен быть подключён к дев-режиму демона, либо снят вместе со своими тестами; сейчас это код, который стоит сопровождения и ничего не обслуживает open кросс-семейное ревью дофикса P6 (линза шва)
PD-218 bug info internal/pgstore/migrations/00015_seam_ceiling_and_units.sql Down-путь 00015 падает на данных, которые накатанная схема уже допускает: он сужает runs_paused_reason_check обратно к одному значению, а строки с daily_ceiling/ceiling_unknown к этому моменту существуют. Откат транзакционный, поэтому падение ничего не портит, но плана отката ниже 15 нет — как и ниже 5 (deploy/README.md). Лечится либо переводом таких строк в down-пути, либо честной записью «ниже 15 не откатываемся» open приёмка P6 (дофикс, ФП-7)
PD-220 hardening info internal/config/config.go Load Резерв имени dev сравнивается байт-в-байт: TM_PLATFORM_OIDC_PROVIDER=Dev проходит гейт. Сегодня инертно, и ровно по той же причине: Postgres сравнивает identities.provider тоже байт-в-байт, поэтому в пространство имён дев-входа такой издатель не попадает. Станет опасным в день, когда сравнение личности станет регистронезависимым open приёмка P6 (дофикс, ФП-7)
PD-221 bug info internal/runs/spawn.go engineStreamID, internal/pgstore/runs.go EngineStreamID Имя потока переиспользуется при повторном спавне ТОЙ ЖЕ попытки: оно детерминировано по паре (прогон, номер попытки), а движок отвергает id, который уже писал события этой книги, и чеканит свой (store.EventsUsed, pipeline/events.go). Тогда платформа не узнаёт собственный поток и живёт на медленном ре-синке — свежесть, не деньги. Ре-спавн одной попытки бывает после отданной назад заявки на спавн open приёмка P6 (дофикс, ФП-7)
PD-222 standards info cmd/tmplatformctl/seed.go HTTP-променад сида не покрыт тестом: вход, грант, загрузка и ожидание интейка проверены только живым прогоном на стенде (P6), автоматически — лишь разбор аргументов (TestASubjectThatIsOnlyPaddingIsNoSubject). Дев-инструмент, но именно он — единственный потребитель контракта в репозитории, и его поломка видна только тому, кто поднимет стенд open приёмка P6 (дофикс, ФП-7)
PD-408 doc info internal/runs/bank.go (бюджет двери = s.runBudget()), internal/runner/bankapply.go (errOut без лимита; Stderr: firstLine) Две операционные оговорки двери правок, названные воркфлоу-ревью; обе — цена конфигурации, не дефект пути. (1) Бюджет двери — та же ручка TM_PLATFORM_RUN_BUDGET, что у прохода свипа; движок выбирал потолок 5000 решений против ЖЁСТКИХ 60 с («пять раз внутри бюджета»), и оператор, понизивший ручку (к чему соседние комментарии подталкивают), делает легальный документ-максимум навсегда неприменимым — вечный 503 вместо «разбей документ»; связка ручки и капа нигде не названа. (2) stderr глагола читается в НЕограниченный bytes.Buffer, хотя потребляется только первая строка, — не-тот бинарь по сконфигурированному пути (полудеплой, обёртка) может раздуть демона до OOM за 60-секундный бюджет; stdout той же команды капнут 64 МиБ open воркфлоу-ревью P9 28.08 (линзы door:lock-lifecycle · door:crash-windows), диспозиция оркестратора 28.08: строкой
PD-428 doc info, деньги internal/pricing (TM_PLATFORM_USD_PER_CHAPTER, Pricing.Ceiling) Цена продажи не знает о накладных, которые масштабируются КНИГОЙ, а не грантом. Замер движкового охотника (лендинг 6ec9f8a): терминолог переигрывается на КАЖДОЙ покупке ЦЕЛИКОМ по книге — три покупки по одному юниту дали три полнокнижных консолидации по $0.005460 каждая, при том что сам юнит дешевле. То есть книга на 500 юнитов, проданная по одному, оплатит 500 полнокнижных проходов. ⚠ Сегодня это НЕ дефект платформы и заведено только как калибровка: продажа идёт ГЛАВАМИ (Ceiling(chapters)), а не юнитами, так что нарезки, при которой накладные обгоняют полезную работу, в продукте нет. Строка существует, чтобы факт не потерялся к моменту, когда мелкая нарезка появится: любая будущая единица продажи мельче главы обязана нести в цене эту книжную составляющую, иначе COGS растёт быстрее выручки на самых дешёвых покупках. Носитель — константа цены, а не код движка ⚠⚠ НОСИТЕЛЬ УМЕР И ПОСЫЛКА ПЕРЕВЕРНУЛАСЬ — паком «форма заказа» 05.09, строка пере-написана ПО СУЩЕСТВУ. Названные тут TM_PLATFORM_USD_PER_CHAPTER и Pricing.Ceiling(глав) удалены вместе со ставкой (строка бэклога 280). ⚠ И оговорка ряда «сегодня это НЕ дефект платформы: продажа идёт ГЛАВАМИ, так что нарезки, при которой накладные обгоняют полезную работу, в продукте нет» — больше не верна: этот же пак ввёл заказ В ЗНАКАХ, который разрешается в ПРЕФИКС ЮНИТОВ, то есть единицу МЕЛЬЧЕ главы, и ровно её ряд и ждал. Что с этим сделано и чего НЕ сделано, раздельно. Книжная составляющая ТЕПЕРЬ ВИДНА и названа: движок публикует её отдельным числом book_once_usd (плоские $2.00 на боевом pipeline-c1), платформа читает его и НЕ кладёт в основу холда — иначе короткая книга непокупаема, — а кладёт СВЕРХ, когда баланс несёт, и говорит покупателю term_consistency_funded: false, когда не несёт (pricing.Model.Hold, пин TestAFlatBookLevelBoundDoesNotPutATwoDollarThresholdUnderEveryPurchase). НЕ сделано главное, ради чего ряд заведён: цена мелкой покупки по-прежнему не несёт книжной составляющей ПРОПОРЦИОНАЛЬНО — заказ в один юнит и заказ во всю книгу видят один и тот же бонд, поэтому COGS на самых дешёвых покупках растёт быстрее выручки ровно так, как ряд и предупреждал. Ряд остаётся open и с этого дня ПРЕДМЕТЕН, а не гипотетичен. Носитель — pricing.Model.Hold и ingest.BookPrice.BookOnceUSD. open движковый пак «деньги» (охотник), передано оркестратором №19 сессии P11
PD-421 hardening info internal/pgstore/sessions.go StillLive и SweepSessions, docs/STACK_DECISIONS.md §13 Открытый поток теряет свою сессию по ПОДМЕТАНИЮ строки, а не по клаузе бездействия, — и это остаток закрытия PD-379, названный прямо. Проверка живости потока намеренно НЕ содержит клаузы idle_expires_at: окно бездействия скользит на ЗАПРОСЕ, а поток — один запрос на всю жизнь, поэтому гашение по idle рвало бы связь активному читателю. Но SweepSessions раз в час УДАЛЯЕТ строки и по бездействию тоже, а «строки нет» ОБЯЗАНО значить «мертва» — иначе отозванная сессия держала бы поток до свипа, то есть дыра ровно в час. Следствие: сессия, протухшая по бездействию и подметённая, теряет поток с опозданием до часа. Это не idle гасит поток, а отсутствие строки; к тому моменту любой другой запрос того же вызывающего — 401. Лечение, если сочтётся недопустимым, — скольжение окна бездействия ИЗ потока, но это правка ПОЛИТИКИ §13: открытая вкладка держала бы сессию до абсолютного потолка, а это слово владельца open пак P11 (назван при закрытии PD-379)
PD-422 bug info internal/runs/runs.go:408=`resnapshot := book.BankMoved book.HasPriorRun, internal/pgstore/books.go:1048=HasPriorRun bool` --resnapshot платформа передаёт УСЛОВНО, а условие ставит только ПРАВКА банка — рост авто-банка от майнинга его не ставит. Флаг выводится под if book.BankMoved, а единственный писатель bank_moved_at — дверь правок банка. На книге, которая МАЙНИТ банк, вторая покупка без правок банка идёт без флага, и движковый джоб-гард останавливает прогон (exit 1failed на стороне платформы): авто-банк растёт от покупки к покупке, edit-снапшот съезжает, а гард банк-онли-движение от смены конфига не отличает. ⚠ Сегодня БЕСПРЕДМЕТНО: проводка tmctl translate --max-units на платформе гейчена оркестратором до лечения, а без неё вторая покупка этой формы не возникает. Строка заведена, чтобы условность не всплыла сюрпризом при снятии гейта. Найдено бэкенд-сессией textmachine-e4 (пак «деньги»), проверено чтением платформенной стороны сессией P11 ⚠ БЕСПРЕДМЕТНОСТЬ КОНЧИЛАСЬ И ДЕФЕКТ ЗАКРЫТ ТЕМ ЖЕ ПАКОМ (05.09), статус флипает ЛЕНДИНГ. Гейт на проводку --max-units снят строкой 280, флаг едет в argv — значит условность --resnapshot перестала быть теоретической ровно в тот момент. Условие расширено: `resnapshot := book.BankMoved
PD-439 standards info docs/DEFECT_REGISTER.md (строки PD-59, PD-115, PD-122, PD-273, PD-380, PD-407, PD-44), шапка регистра (словарь статусов), internal/gates/register_test.go (греп registerStatus) Семь строк несут в колонке статуса не статус, и каждая из них невидима для всякого счёта, который эту колонку читает. Словарь шапки — open · fixed(<commit>) · accepted-risk(<кем, когда>); в дереве встречаются open (наблюдаемость закрыта P5; ops и конфигурация — нет), open (грейс-половина закрыта D39.162), open (добавление закрыто P5; удаление — нет), closed (решение владельца 17.08), fixed без коммита (дважды) и **закрыт ратификацией**, работа уходит строкой 103. Зонный awk сверяет ячейку с open ТОЧНО, поэтому три аннотированных открытых ряда не попадают ни в одно число, которое зона печатала (включая «open=96»), а fixed без коммита нарушает правило «закрытие — только с коммитом фикса». ⚠ Найдено новым гейтом класса: он читает статус по словарю с аннотацией, считает такие ряды открытыми, называет нечитаемые поимённо и краснеет, если нечитаемый ряд стоит под заголовком «Открытые» и несёт маркеры тревоги. Строки НЕ правлю: смена статуса — акт лендинга, а здесь под вопросом и форма, и содержание вердикта open самопроверка пака P13 (гейт класса, первый прогон)

Принятый риск

Не дефекты, а решения: цена названа и принята, чтобы это не выяснилось молчанием.

ID Класс Серьёзность Где Суть Статус Источник
PD-22 hardening info deploy/ Ограничителя одновременных соединений нет ни в процессе, ни описанного edge-прокси: ReadTimeout ограничивает УДЕРЖАНИЕ одного соединения 30 секундами, но не их число. Осознанно оставлено деплой-слою (LimitNOFILE, edge) — строка заведена, чтобы это было решением, а не забывчивостью accepted-risk(платформа P1, 05.08) самопроверка P1
PD-42 hardening info internal/login/login.go Лимит /auth/login глобальный: один хост держит ведро пустым и выключает вход всем (замерено: 8 отказов из 10 у «легитимного» пользователя при фоне 5 rps). Пер-адресный лимит здесь неверен, пока нет доверенного edge-прокси — за прокси RemoteAddr один на всех. Место лимита — edge accepted-risk(платформа P1, 05.08) ревью безопасности (исполнением)
PD-71 bug info internal/pgstore/migrations/00005_identity_oauth.sql:72 Down-путь 00005 не исполним на данных, которые его же up-путь делает законными, поэтому откат ниже версии 5 недоступен. Он восстанавливает users_email_key и email NOT NULL, а боевой код пишет email = NULL у неподтверждённой личности и кладёт один подтверждённый адрес на два аккаунта (следствие «почта не ключ»). Перепроверено моим прогоном, не принято со слов ревью: три реальных аккаунта (один с email = NULL, два с общим подтверждённым адресом) — DownTo(5) проходит, DownTo(4) падает с could not create unique index "users_email_key" (SQLSTATE 23505); первым срабатывает индекс, до NOT NULL выполнение не доходит. Данные целы — down транзакционный, Up() вернул схему на версию 8 со всеми тремя аккаунтами, — но плана отката ниже 5 не существует. Править 00005 запрещает append-only, а чужой down-текст новая миграция не заменяет — принято как ЦЕНА ПРАВИЛА: записано в STACK_DECISIONS §8 и в deploy/README.md разделом «Откат релиза: не ниже версии 15» (⚠ раздел переименован 04.09: реальный пол — 15, прежнее имя «…не ниже версии 5» в дереве больше не встречается), чтобы оператор не узнал это в момент отката accepted-risk(зона P2, 05.08) ревью P2 (линза sql-money)
PD-179 hardening info cmd/tmplatformd/main.go serveMetrics, deploy/ Эндпоинт /metrics не аутентифицирован и защищён только адресом привязки. Дефолт 127.0.0.1:9464, то есть снаружи недостижим; экспозиция несёт операционную форму деплоя (глубина очереди, число прогонов, возраст холдов), но не пользовательские данные и не деньги. Второй модели авторизации ради скрейпера зона не заводит — это ровно тот довод, по которому админ-поверхность стала CLI (§10). Риск принят: оператор, поднявший TM_PLATFORM_METRICS_ADDR на внешний адрес, открывает её сам, и об этом сказано в deploy/README.md accepted-risk(платформа P5, 11.08) сессия P5
PD-400 doc info internal/runs/bank.go bankVerdict (класс 12) и шапка файла, internal/runner/engine.go Manifest/Export Слово run_in_flight у двери правок покрывало больше, чем прогон; пер-книжный мьютекс внутрипроцессный. Разобрана на две половины по слову владельца 27.08 («техдолг в паке не держим»), диспозиция каждой явная. (1) ЗАКРЫТА этим же деревом: раскладка слова сделана точной по построению — run_in_flight отвечается ТОЛЬКО из проверки собственной строки прогона под мьютексом, а класс 12 от самого глагола под тем же мьютексом прогоном быть НЕ МОЖЕТ (Start/Resume ждут этот мьютекс, реконсилер рестартует только живые строки, которые проверка видит) — это транзиентный держатель флока (границная материализация, ручной tmctl), и он едет 503 service_unavailable «занято, повтори позже», а не словом про прогон, которого нет; пин — кейс «a held project = a transient holder» в TestBankVerdictKeepsTheRemediesApart. (2) ГРАНИЦА v1, названная с условием и ценой (на перевод в «Принятый риск» словом лендинга; ⚠ цена ПЕРЕ-ОПИСАНА по воркфлоу-ревью 28.08 и слову оркестратора — прежняя редакция говорила «ложного слова нет», и это неверно): мьютекс runs.Service.books — внутрипроцессный; вторая реплика платформы над одним хранилищем сужает сериализацию до пер-репличной. Цена ДВУСТОРОННЯЯ: (а) translate, заспавненный в флок глагола, умирает холостой попыткой — деньги и данные целы; (б) обратная сторона ТОЙ ЖЕ гонки: глагол проигрывает флок НАСТОЯЩЕМУ прогону, который соседняя реплика допустила своим мьютексом по чистой на тот миг таблице, — и класс 12 тогда отвечается 503 «транзиентный держатель, повтори позже» про живой многочасовой прогон, то есть ЛОЖНЫМ словом (канонно верное там — 409 run_in_flight). Принятие риска включает эту ложь, а не только холостую попытку; оговорка внесена и в комментарий ветки класса 12 (bank.go). Условие: граница ПЕРЕСТАЁТ держать в день второй реплики; лечение тогда — арбитр в хранилище, не больший мьютекс. Сегодняшний деплой однорепличный по всей зоне (in-memory resynced в том же сервисе — тот же допуск) accepted-risk(акт D39.162; половина 1 — fixed тем же актом; перевод по аудиту 28.08. Цена риска — по пере-описанию Р8 в теле: «ни денег, ни порчи» держится, «ни ложного слова» — НЕТ, мульти-репличный класс 12 может ответить 503 про живой прогон) пак P9: опровергатель раскладки кодов (находка 2) · разбор по слову владельца 27.08 · воркфлоу-ревью 28.08 (линза code-layout): цена включает ложное слово · аудит документации 28.08

Закрытые ратификацией

Строки, у которых лечением был не код, а решение владельца контракта или оркестратора.

ID Класс Серьёзность Где Суть Статус Источник
PD-59 bug info docs/platform-PROGRESS.md, вопрос оркестратору №4 Канал не меняем — но решает это не тот довод, который обсуждали. Вопрос вынесен абстрактно (без контекста репозитория) двум независимым агентам, с доступом в сеть и без. По каналу они РАЗОШЛИСЬ, зато независимо сошлись на трёх вещах, которых не было ни в записке зоны, ни в первых трёх редакциях приёмки. (1) SIGPIPE зависит от НОМЕРА дескриптора (os/signal: обрыв на fd 1/2 убивает процесс, на любом другом — возвращает EPIPE). Замерено: поток на fd 1 → ребёнок УБИТ broken pipe; на fd 3 → write вернул EPIPE и процесс доработал до конца. Для нас это деньги: сегодня падение платформы убивает движок на следующей же записи события, а после переезда движок станет сиротой и часами будет жечь оплаченные вызовы, пока холд висит в леджере и некому его закрыть. Свойство несущее и нигде не записано. (2) Настоящая защита — не выбор канала, а перехват на уровне дескриптора в main движка: dup(1) в приватный fd, затем dup3(2,1,0). Он герметичен там, где предложенный приёмкой os.Stdout = os.Stderr дыряв: переживает var out = os.Stdout в зависимости, cgo и унаследованный fd 1 у внуков. (3) Дискриминатор, при котором переезд был бы прав — «ребёнок исполняет чужой код, наследующий stdio». Проверено: у нас нетgrep по backend/ не находит ни одного exec.Command вне тестов и ни одного cgo. Плюс сверено: ловушка bufio.Scanner, которую оба назвали самым вероятным латентным багом (переполнение строки читается как чистый EOF), у нас закрыта — Buffer поднят до 1 МиБ и sc.Err() проверяется (decoder.go:45,115) закрыт ратификацией, работа уходит строкой 103 приёмка P1 (четвёртая итерация: два независимых агента + замер SIGPIPE)

Закрытые — эра P1 (вход · кредиты · админ-CLI)

ID Класс Серьёзность Где Суть Статус Источник
PD-1 hardening minor internal/pgstore/pg_test.go:89 Свойство «в БД только SHA-256, не токен» НЕ запинено тестом: посадка «Digest возвращает плейнтекст» выживает — тест сверяет хранимое через тот же auth.Digest (self-consistent). — закрыто: internal/pgstore/pg_test.goTestStoredCredentialIsAHashNotTheToken: оракул SHA-256 считается в тесте, плюс поиск плейнтекста в отрендеренной строке. Посадка «Digest возвращает плейнтекст» ПАДАЕТ (проверено) fixed(P1, дерево сессии) приёмка P0 (посадка №1)
PD-2 vuln major, ЖИВАЯ (не латентная) cmd/tmplatformd/main.go:73-82 Нет ReadTimeout ⇒ соединения пиннятся уже СЕГОДНЯ, без единой body-принимающей ручки: net/http дренирует непрочитанное тело <256 КБ ВНУТРИ chunkWriter.writeHeader до отправки заголовка ответа (net/http/server.go:1389-1435), и этот чтение-шаг наследует отсутствующий дедлайн. Репродуцировано оркестратором на собранном бинаре: 50 полу-кормленных POST на охраняемый /v0/* → сервер отработал и залогировал 50×401 ms:0, клиенты получили НОЛЬ байт, fd 7→57 и держались, пока не закрыл КЛИЕНТ (агент-скептик независимо пинил 500 соединений). Ограничителя соединений и документированного edge-прокси в зоне нет. — закрыто: ReadTimeout 30 с в httpapi.DefaultTimeouts; пин — TestHalfFedRequestIsDroppedByTheServer на РЕАЛЬНОМ http.Server. Живая проба: полу-кормленный POST теперь отпускается через 30.0 с (был бесконечно). Вторая половина закрыта LimitBody на поддереве /v0 и /auth. Побочное обязательство «ReadTimeout рубил бы и SSE» ОПРОВЕРГНУТО в P2 (PD-51/PD-63): net/http снимает дедлайн сам, помощник ClearReadDeadline удалён как воспроизводивший ровно этот дефект; поток пинит TestStreamOutlivesReadTimeout fixed(P1, дерево сессии) приёмка P0 (security-линза + скептик + собственная репродукция)
PD-3 bug minor internal/httpapi/middleware.go:57 Recover логирует сырой r.URL.Path на ERROR — id книг/прогонов утекают в лог, против собственной дисциплины AccessLog (route-pattern, не путь) — закрыто: Recover логирует route, не r.URL.Path fixed(P1, дерево сессии) приёмка P0 (security-линза)
PD-4 hardening minor internal/pgstore/sessions.go:41 WHERE у Touch слабее, чем у Lookup (нет idle_expires_at > now): прямой вызов воскресил бы idle-истёкшую сессию. Через Require недостижимо (Touch только после успешного Lookup) — закрыто: клауза idle_expires_at > $2 добавлена; пин — TestTouchCannotResurrectAnIdleExpiredSession (посадка падает) fixed(P1, дерево сессии) приёмка P0 (security-линза)
PD-5 bug minor internal/auth/middleware.go:36,45 Ошибки стора невидимы: сбойный Lookup → 401 без единой строки лога (аутентификационный DB-outage выглядит как шторм 401), Touch глотается _ =. На проводе различать нельзя (оракул) — но лог обязан различать — закрыто: Authenticator.Log: сбой Lookup (кроме ErrNoSession) и сбой Touch уходят в ERROR с request_id; на проводе по-прежнему неразличимо fixed(P1, дерево сессии) приёмка P0 (security+blind линзы)
PD-7 bug info internal/pgstore/sessions.go:78 DeleteExpiredSessions никем не вызывается — закрыто: свип сессий раз в час в демоне (sweepSessions), плюс свип брошенных логинов раз в 15 минут fixed(P1, дерево сессии) приёмка P0
PD-8 hardening info internal/auth/session.go:18 Писателя куки ещё нет; __Host- требует Secure ⇒ локальный dev по HTTP куку не поставит. — закрыто: auth.Cookies{Insecure} — dev-профиль меняет ИМЯ вместе с атрибутами (tm_session без __Host-), TM_PLATFORM_INSECURE_COOKIES=1, демон предупреждает в лог fixed(P1, дерево сессии) приёмка P0
PD-9 bug minor cmd/tmplatformd/main.go:81 BaseContext возвращает signal-контекст ⇒ SIGTERM мгновенно рубит контексты ВСЕХ in-flight запросов, и 15-секундный дренаж Shutdown мёртв для ctx-aware хендлеров. — закрыто: BaseContext — собственный контекст, отменяется ПОСЛЕ Shutdown; пин — TestShutdownDrainsInFlightRequests (посадка «BaseContext = сигнальный ctx» падает) fixed(P1, дерево сессии) приёмка P0 (faults-линза)
PD-10 bug minor internal/ingest/decoder.go:42,61,67 Три ужесточения декодера: (а) hello с пустым engine_run_id принимается — а это половина ключа идемпотентности; (б) seq самого hello не пинится к 1 — потеря пре-хендшейковых строк недетектируема; (в) mid-stream hello (любой версии, вкл. мажор 9.9) уходит в Sink как обычное событие — version-гейт держит только строку 1 — закрыто: три ужесточения + ErrBadHandshake/ErrRepeatedHello; пины — TestHandshakeMustIdentifyTheStream и FuzzDecoder (4.4 млн исполнений, инварианты — оракулы) fixed(P1, дерево сессии) приёмка P0 (faults-линза)
PD-11 bug minor internal/pgstore/migrations/00002_readmodel.sql:137,177 Неиндексированные FK-каскады: notes.chapter_id и bank_decisions.term_id — каскадное удаление сканирует таблицы — закрыто: notes_chapter_idx + bank_decisions_term_idx; notes.unit_id уже был fixed(P1, дерево сессии) приёмка P0 (faults-линза)
PD-12 bug info internal/ingest/supervisor.go:82-84 Сбой Sink в начале прогона ⇒ платформа дренирует ВЕСЬ оставшийся поток в io.Discard часами: ceiling/bank_stop-события выбрасываются, никто не оповещён. — закрыто: сбой синка ОСТАНАВЛИВАЕТ прогон (stop() после Ingest), а не дренирует его в io.Discard; пин — TestFailingSinkStopsTheRun. Политика ретраев самого синка — при постройке материализатора fixed(P1, дерево сессии) приёмка P0 (faults-линза)
PD-13 bug info internal/ingest/supervisor.go:64 Краш платформы осиротляет процесс движка: ни process-group, ни pidfile, ни пути реаттача (поток невосстановим, повторный спавн упрётся в EXCLUSIVE-лок) — закрыто: группа процессов (Setpgid + сигнал группе) закрывает обычную остановку; краш платформы закрывает cgroup юнита — deploy/tmplatformd.service (проверен systemd-analyze verify, живого прогона под systemd не было) fixed(P1, дерево сессии) приёмка P0 (faults-линза)
PD-14 hardening info internal/httpapi/server.go:79 readyz: ping без собственного таймаута (WriteTimeout нет намеренно — SSE), эндпоинт неаутентифицирован и без rate-limit — задушить дешёво — закрыто: собственный таймаут 2 с на ping; rate-limit на ops-слое (edge), в зоне не строим fixed(P1, дерево сессии) приёмка P0
PD-15 bug info internal/ingest/resync.go:32 Деньги в ре-синке — float64, а usage_windows хранит micro-USD именно против дрейфа: дрейф входит шагом раньше (JSON-парс + суммирование дельт) — закрыто: деньги на шве — money.MicroUSD через big.Rat, округление ВВЕРХ; пины — TestSpendConvertsExactlyAndRoundsUp, TestParseUSDIsExactAndRoundsUp fixed(P1, дерево сессии) приёмка P0 (faults-линза)
PD-16 bug minor internal/httpapi/server.go:81 readyz глотает ошибку ping вопреки собственному комменту «the reason stays in the log» — лога нет — закрыто: ошибка ping уходит в ERROR fixed(P1, дерево сессии) приёмка P0 (blind-линза)
PD-17 bug minor Makefile:41-42 Баннер «did NOT run (no database)» печатается и при ПРОГНАННЫХ БД-тестах (безусловный); батарея гоняет сьют дважды ради имён скипов (второй прогон без -race) — закрыто: один прогон сьюта под -race, баннер печатается только при наличии скипов fixed(P1, дерево сессии) приёмка P0 (blind-линза)
PD-18 bug info internal/pgstore/migrations/00002_readmodel.sql:9,139 Коммент шапки «engine vocabulary never crosses this seam» противоречит notes.reason (движковая причина хранится, не проецируется) — закрыто: шапка миграции переписана: исключение (notes.reason) названо там же fixed(P1, дерево сессии) приёмка P0 (canon-линза)
PD-19 bug info internal/ingest/resync.go:44 WorstFlagReason задокументирован «stored», а колонки в chapters нет — закрыто: WorstFlagReason убран из аллоулиста — в контракте v0 у главы нет читателя для него fixed(P1, дерево сессии) приёмка P0 (canon-линза)
PD-20 bug minor internal/ingest/supervisor.go:78 Один сигнал остановки ТЕРЯЕТСЯ, если послан в первые миллисекунды жизни ребёнка: воспроизведено на стенде отдельным экспериментом (8 запусков, промах на нулевой задержке) и как флейк собственного теста PD-12 (1 падение из 3). Последствие серьёзнее самого промаха: единственный оставшийся механизм — SIGKILL по WaitDelay, а движок держит ЭКСКЛЮЗИВНЫЙ лок на файле проекта, и после kill лок остаётся — закрыто: askToStop повторяет SIGINT на 30/120/400 мс с проверкой «процесс ещё наш» через os.Process; пин — TestFailingSinkStopsTheRun (25 прогонов подряд зелёные, до фикса падал) fixed(P1, дерево сессии) самопроверка P1 (флейк собственного теста)
PD-21 vuln minor internal/login/login.go:safeReturnTo Открытый редирект в ?return_to: /\evil.example проходил проверку — url.Parse читает это как обычный путь, а браузер нормализует \ в / и получает протокол-относительный URL, то есть чужой хост. Найдено ПОСАДКОЙ мутации: ослабление проверки тест пережило, значит тест был слабый — закрыто: аллоулист (первый символ /, второй не /, обратных слэшей нет, Scheme/Host/Opaque пусты), тест переписан на «каждый враждебный вход даёт ПУСТО»; посадка теперь падает. Дефект не покидал дерево сессии fixed(P1, дерево сессии) самопроверка P1 (посадка мутации)
PD-24 bug major internal/pgstore/migrations/ Переиспользование номера миграции: удалённый 00003_usage.sql и новый 00003_credits.sql заняли одну версию. goose применяет ТОЛЬКО по номеру (ни имени, ни хеша), поэтому база, доехавшая до версии 3, рапортует «migrations applied» и не получает ни одной новой таблицы, вход и кредиты падают в рантайме, а DownTo на ней ломается навсегда. Обоснование «до деплоя правим на месте» было допущением без механизма — закрыто: выпущенные 0000100003 возвращены байт-в-байт, новое приехало номерами 0000400007; гейт migrations.sha256 + TestReleasedMigrationsAreUnchanged; апгрейд со старого релиза пинится TestDatabaseAtAnOlderReleaseCatchesUp fixed(P1, дерево сессии) ревью «вне карты» (исполнением)
PD-25 bug major internal/pgstore/credits.go, 00007_credits.sql Ключ идемпотентности леджера не содержал user_id: грант с ключом, потраченным на другом аккаунте, молча проглатывался, а CLI печатал «granted». Плюс каскад удаления книги уносил ОТКРЫТУЮ резервацию, оставляя строку hold в леджере (деньги списаны, вернуть нечем), после чего освободившийся engine_run_id давал холд БЕЗ списания, а его релиз печатал деньги — закрыто: ключ стал (user_id, source, source_id), пустой ключ запрещён DDL, book_id перешёл на составной FK к books(id, owner_id) с on delete restrict, appendLedger возвращает «применилось», Hold падает при повторе. Пины: TestBookWithAnOpenHoldCannotBeDeleted, TestSecondHoldOnOneAttemptIsRefused, TestGrantIsIdempotentBySource fixed(P1, дерево сессии) ревью денежного пути (исполнением)
PD-26 bug minor internal/pgstore/credits.go Инверсия порядка блокировок Hold↔Settle/Release: 41 взаимоблокировка на 300 раундов, замерено. Settle/Release брали строку резервации раньше баланса — закрыто: lockBalance первым во всех операциях fixed(P1, дерево сессии) ревью денежного пути (исполнением)
PD-27 bug minor internal/pgstore/credits.go Settle принимал любую сумму: одно завышенное committed_usd уводило баланс в минус, дальше каждый прогон получал ErrInsufficientCredit без диагностики — закрыто: расчёт capped потолком холда, факт записан в note; пин TestSettlementIsCappedAtTheHold fixed(P1, дерево сессии) ревью денежного пути · ревью «вне карты»
PD-28 bug minor internal/ingest/supervisor.go cmd.Wait() на отменённой команде возвращает context.Canceled, а не *ExitError, поэтому исход читался как failed: штатный SIGTERM пометил бы ВСЕ идущие прогоны провалившимися — закрыто: исход из ProcessState, факт остановки едет в ошибке; пин TestStoppedRunKeepsTheEnginesOutcome fixed(P1, дерево сессии) ревью стиля (клейм) + собственная проверка исполнением
PD-29 vuln minor internal/login/login.go GET /auth/callback — неаутентифицированная ручка, ПИШУЩАЯ в БД, без лимита и без ретеншена: замерено 2000 строк за 2.28 с с одного хоста (~76 млн строк/сутки), строки отказов недостижимы через API и не удалялись никогда — закрыто: лимитер на колбэке, ретеншен журнала 180 дней свипом fixed(P1, дерево сессии) ревью безопасности (исполнением)
PD-30 vuln minor internal/pgstore/identity.go Грант фри-тира выдавался за каждую новую пару (provider, subject) без учёта email_verified: провайдер с саморегистрацией превращал каждый новый sub в $5, потолок задавал только глобальный лимитер (~$864k/сутки на бумаге) — закрыто: грант только подтверждённой личности — НЕПОДТВЕРЖДЁННАЯ создаёт аккаунт с нулём, и его начисляют руками из админки. ⚠ Уточнено 08.08 (PD-104): ПОДТВЕРЖДЁННАЯ личность получает автогрант TM_PLATFORM_SIGNUP_GRANT_USD (дефолт $5, config.go), закрыт был только путь саморегистрации. ⚠ Продуктовое следствие — вопрос владельцу в журнале fixed(P1, дерево сессии) ревью безопасности (исполнением)
PD-31 bug minor internal/login/login.go discover держал мьютекс на время сетевого вызова без таймаута: шесть параллельных входов при медленном IdP заняли 4/8/12/16/20/24 с вместо ~4 — закрыто: запрос вне лока, свой таймаут 5 с fixed(P1, дерево сессии) ревью безопасности (исполнением)
PD-32 vuln minor internal/login/login.go, cmd/tmplatformd/main.go Имя провайдера захардкожено "google" независимо от issuer, а State.Provider писался и не сверялся: смена issuer тихо кладёт чужие sub в старое пространство имён (новые аккаунты, старые недостижимы), а при двух провайдерах стейт одного редимится колбэком другого (IdP mix-up) — закрыто: TM_PLATFORM_OIDC_PROVIDER, сверка st.Provider в колбэке fixed(P1, дерево сессии) ревью безопасности · ревью «вне карты»
PD-33 vuln minor internal/auth/csrf.go Требование X-TM-Client снималось ЛЮБЫМ заголовком Authorization, включая мусорный: покрытие CSRF-слоя выбирал атакующий (сегодня упиралось в 401, но пережило бы любое послабление в present) — закрыто: снимает только валидный Bearer, через ту же функцию, что аутентифицирует fixed(P1, дерево сессии) ревью безопасности (исполнением)
PD-34 bug minor internal/httpapi/serve.go, cmd/tmplatformd/main.go Две регрессии остановки: второй SIGTERM больше не прерывал дренаж (процесс жил ровно 15 с), а просроченный дренаж возвращал ошибку и давал exit 1 — при Restart=on-failure штатная остановка читается systemd как крах — закрыто: сигнал разрегистрируется при начале дренажа, просрочка логируется WARN и даёт exit 0, добавлена строка stopped fixed(P1, дерево сессии) ревью «вне карты» (исполнением)
PD-35 bug minor internal/httpapi/middleware.go Лимит тела стоял самым внешним слоем, поэтому обещанное «ручка загрузки регистрирует свой, больший лимит» не работало: вложенный MaxBytesReader не может ослабить внешний, а контракт требует загрузку книги (23 МБ) — закрыто: лимит стал пер-маршрутным аргументом guard fixed(P1, дерево сессии) ревью «вне карты»
PD-36 hardening minor internal/httpapi/middleware.go Не было HSTS, CSP и запрета фрейминга; __Host- защищает запись куки, а не первый навигационный запрос — закрыто: Content-Security-Policy: default-src 'none'; frame-ancestors 'none', X-Frame-Options: DENY, HSTS в прод-профиле (в dev выключен: пин политики на localhost — долгая ошибка) fixed(P1, дерево сессии) ревью безопасности
PD-37 bug minor internal/login/login.go safeReturnTo заявляла защиту, которой не давала: проверка обратного слэша работала по уже раскодированной строке, а браузер декодирует цель редиректа ещё раз (/%5c/evil.example). Эксплуатируемого редиректа не получено, но три проверки из четырёх держались на поведении браузера — закрыто: проверка обеих форм, теста добавлены процент-кодированные входы fixed(P1, дерево сессии) ревью безопасности (исполнением)
PD-38 hardening info internal/pgstore/sessions.go, 00005_identity_oauth.sql Отозванные сессии не удалялись до абсолютного срока (90 дней); журнал входов каскадно стирался вместе с аккаунтом, хотя объявлен доказательством для расследования — закрыто: свип берёт отозванные и idle-протухшие, login_events.user_id перешёл на on delete set null (строка анонимизируется, не уничтожается) fixed(P1, дерево сессии) ревью безопасности
PD-39 bug info internal/money/money.go Док обещал округление «от нуля», код округляет к +∞; отрицательные дроби не были покрыты тестом вовсе. Плюс USD() на MinInt64 печатал мусор, а вход не имел ограничения длины (2 МБ → 6.1 с и сообщение об ошибке на 2 МБ) — закрыто: док приведён к коду, отрицательные кейсы запинены, потолок длины 64 символа, рендер без отрицания fixed(P1, дерево сессии) ревью денежного пути · ревью стиля
PD-40 bug info internal/ingest/resync.go Отсутствующий/null/пустой committed_usd декодировался в 0 — неотличимо от «попытка не стоила ничего»; на пути расчёта это освободило бы холд и не списало ничего — закрыто: Spend стал указателем, пустая строка — ошибка fixed(P1, дерево сессии) ревью «вне карты»
PD-41 bug info internal/login/login.go, internal/httpapi/ Поверхность /auth/* отвечала stdlib-телами text/plain на 404/405 вопреки нормативу «ответы problem+json»; ошибки стора и сработавший лимитер не логировались; паника писалась без стека; успешный вход не оставлял следа, а недоступность провайдера классифицировалась как «токен отвергнут» — закрыто: метод проверяется в обёртке с problem+json, добавлены login succeeded, sign-in rate limit engaged, provider_unreachable, стек паники, login_start_id для склейки двух половин входа fixed(P1, дерево сессии) ревью логов (исполнением)

Закрытые — эра P2 (фикс-пак приёмки P1)

ID Класс Серьёзность Где Суть Статус Источник
PD-46 hardening minor internal/httpapi/serve.go:33-40 Запинена ПРОВОДКА ReadTimeout, но не ЗНАЧЕНИЕ, с которым едет демон. Тесты строят свой Timeouts (fastTimeouts), поэтому посадка «DefaultTimeouts().Read = 0» проходит ВСЮ батарею зелёной — а main.go:121 берёт именно DefaultTimeouts(). Посадка «убрать ReadTimeout из NewServer» ловится (проверено), то есть дыра ровно в дефолтах. Это форма, в которой PD-2 пережил P0: свойство проверено не на том объекте, который едет в прод. — закрыто: httpapi.TestTheServerTheDaemonRunsHasEveryDeadlineSet утверждает не литералы, а сам *http.Server, который строит NewServer(…, DefaultTimeouts()): каждый дедлайн >0, WriteTimeout ОБЯЗАН быть нулём (иначе резал бы SSE), ReadHeaderTimeout <= ReadTimeout, grace >0. Закрывает обе половины — значение и проводку. Пять посадок поймано поимённо: DefaultTimeouts().Read=0, снятие ReadTimeout из NewServer, снятие IdleTimeout, добавление WriteTimeout «для симметрии», снятие Unwrap fixed(P2, дерево сессии) приёмка P1 (посадка M23/M43)
PD-47 bug minor internal/login/login_test.go:266 Закрытие PD-37 заявлено неверно: «в тесты добавлены процент-кодированные входы» — их там нет (список: //evil.example/, https://…, http:/…, /\evil.example, /\/evil.example, /\tevil, evil.example, ``). Посадка «судить только сырую форму, без второго декода» батарею ПЕРЕЖИВАЕТ. Побочно: посадка «убрать обратный слэш из ContainsAny» тоже переживает — на тестовых входах её дублирует проверка s[1]. — закрыто: в таблицу добавлены процент-кодированные входы (/%5c/, /%5C/, /%09, /%00, /%0d%0a) — их ловит ТОЛЬКО второй декод — и /%2f/evil.example, который ловит ТОЛЬКО проверка s[1] на декодированной форме; плюс FuzzSafeReturnTo, который пинит СВОЙСТВО независимым оракулом (url.URL.ResolveReference после браузерной нормализации \/), 3,1 млн исполнений без контрпримера. Посадки «судить только сырую форму», «убрать класс символов», «убрать protocol-relative» падают каждая. ⚠ Побочно установлено: условия u.Scheme/u.Host/u.Opaque НЕДОСТИЖИМЫ как отказ (при raw[0]=='/' схемы и Opaque не бывает, Host требует //), пин на них невозможен — оставлены бэкстопом, это названо в коде fixed(P2, дерево сессии) приёмка P1 (посадки M15/M16)
PD-48 hardening minor internal/login/login.go:268-271 Правило PD-30 «грант только подтверждённой личности» не запинено ничем: удаление if !claims.EmailVerified { grant = 0 } проходит все тесты internal/login. pgstore.TestUnverifiedAddressStaysOffTheAccount пинит другое свойство (адрес не поднимается на аккаунт), денежное — никто. По правилу шапки этого файла PD-30 закрытым не считается — закрыто: login.TestSignupGrantGoesOnlyToAVerifiedIdentity гоняет обе ветки через настоящий поток и сверяет САМ грант, дошедший до стора (memStore теперь его запоминает — раньше отбрасывал, потому правило и было незапинено). Посадка «убрать условие EmailVerified» падает fixed(P2, дерево сессии) приёмка P1 (посадка M14)
PD-49 hardening minor internal/login/login.go:239-242 Вторая половина PD-32 не запинена: удаление сверки st.Provider != h.cfg.Provider проходит все тесты. Сегодня провайдер один, поэтому свойство латентное — но заведено оно ровно под появление второго (IdP mix-up) — закрыто: login.TestStateFromAnotherProviderIsRefused подменяет провайдера в сохранённой строке состояния — форма, которую даёт появление второго провайдера, — и требует 400, отсутствия сессии, причины state_from_another_provider в журнале и НУЛЯ обращений к token endpoint. Посадка «убрать сверку» падает. Норму при этом закрывает не она, а PD-57 fixed(P2, дерево сессии) приёмка P1 (посадка M19)
PD-50 hardening info internal/auth/csrf.go:55-60 Закрытие PD-33 сформулировано сильнее кода: «снимает только ВАЛИДНЫЙ Bearer» — на деле Present только ПАРСИТ, поэтому Authorization: Bearer <мусор> требование X-TM-Client снимает. Привилегии это не даёт, проверено живой пробой (кука + мусорный Bearer + без заголовка → 401, не хендлер): безопасность держит правило «Bearer побеждает куку» в Present, а не «валидность». Посадка «снимать любым непустым Authorization» батарею переживает. — закрыто формулировкой + пином: доккоммент cookieUnsafe переписан на то, что верно (Present ПАРСИТ, не валидирует; безопасность держит правило «есть Authorization ⇒ кука не участвует», а не валидность). auth.TestAnAuthorizationHeaderTakesTheCookieOutOfPlay пинит именно это на пяти формах заголовка; посадка «падать обратно на куку при неразобранном заголовке» падает fixed(P2, дерево сессии) приёмка P1 (посадка M30 + живая проба)
PD-51 bug minor internal/httpapi/serve.go:118-127, STACK_DECISIONS §12 Механизм заявлен неверно. Утверждение «ReadTimeout убил бы и поток, поэтому стриминговый хендлер ОБЯЗАН снять read-дедлайн» на Go 1.26.5 не подтверждается: connReader.startBackgroundRead сам делает SetReadDeadline(time.Time{}) (net/http/server.go:687-698) и для запроса без тела вызывается ДО хендлера (:2062). Проверено исполнением на трёх формах запроса (GET без тела · POST с непрочитанным телом · POST с вычитанным телом) — поздний кадр доезжает во всех шести комбинациях, звали ClearReadDeadline или нет. Следствие: TestStreamOutlivesReadTimeout НЕ МОЖЕТ упасть от выхолащивания ClearReadDeadline (проверено); он пинит только Unwrap (эта посадка ловится). Код безвреден, ложны обоснование и строка в таблице пинов — закрыто, и вывод приёмки уточнён исполнением: механизм подтверждён (startBackgroundRead снимает дедлайн сам, server.go:687-698, для запроса без остатка тела — до хендлера, :2059-2062; по ходу хендлера не перевзводится — проверено по всем call sites). Но «код безвреден» неверно: см. PD-63. ClearReadDeadline УДАЛЁН, STACK_DECISIONS §12 переписан, TestStreamOutlivesReadTimeout переписан на настоящее свойство (поток переживает Read БЕЗ действий хендлера) и пинит Unwrap через ошибку Flush fixed(P2, дерево сессии) приёмка P1 (посадка M24/M42 + отдельная проба)
PD-52 hardening minor internal/pgstore/credits.go:233-243 Порядок блокировок (PD-26) не запинен ни одним тестом — снятие lockBalance из closeReservation батарею переживает. Дефект воспроизведён приёмкой НЕЗАВИСИМО, в форме, которая действительно даёт цикл: конкурентные Settle(run-1) и повторный Hold(run-1)2 взаимоблокировки на 150 раундов с инверсией, 0 с фиксом. Регрессионный тест написан приёмкой и лежит готовым к вставке в docs/platform-PROGRESS.md, раздел «Ратификация приёмкой P1». — закрыто: тест приёмки вставлен как pgstore.TestHoldAndSettleOnTheSameAttemptDoNotDeadlock. ⚠ Замер приёмки не копировался, а ПЕРЕПРОВЕРЕН на своём стенде (PostgreSQL 18.4): с инверсией падает 5 прогонов из 5, 510 взаимоблокировок на 150 раундов; с фиксом 5 прогонов из 5 зелёные. Замер сессии P1 «41 на 300» так и не воспроизведён и остаётся СО СЛОВ fixed(P2, дерево сессии) приёмка P1 (посадка M07 + собственная репродукция)
PD-53 hardening info internal/httpapi/server.go:73-75 DefaultMaxBody не запинен: поднятие лимита поддерева до 1 ГиБ батарею переживает. — закрыто: httpapi.TestBodyCapIsPerRouteBecauseNestingOnlyTightens фиксирует исполнением ПРИЧИНУ пер-маршрутности — вложенный БОЛЬШИЙ лимит не поднимает внешний, — поэтому возврат общего слоя молча урезал бы аплоуд-маршрут; TestDefaultBodyCapStaysAContractSizedNumber держит дефолт в полосе контрактного размера (посадка «1 ГиБ» падает), не превращаясь в change-detector на точное число fixed(P2, дерево сессии) приёмка P1 (посадка M26)
PD-54 bug minor зонный журнал, секция эры P0 (⚠ якорь на строку снят 22.08: описанное тело было УДАЛЕНО при закрытии строки, а сам журнал с тех пор дважды срезан в слайсы — адресовать было нечего) В журнале ДВЕ несовместимые формы GET /v0/usage: новая кредитная (строка 172) и старая подписочная (строка 329) с resets_at, windows[{period}] и хранением в usage_windows — таблице, которую снесла миграция 00006. Секция P0-эры не помечена superseded, а S3 идёт читать журнал именно за формой ручки — закрыто: подписочное тело ответа УДАЛЕНО из журнала, а не помечено баннером: S3 идёт туда за формой ручки и скопировал бы тело. Осталась одна форма — кредитная, в разделе «Что предлагаем в спеку (S3)»; из П-5 сохранены абзацы, не зависящие от модели денег, ссылка на хранение переведена на credit_ledger fixed(P2, дерево сессии) приёмка P1 (свип доков)
PD-55 bug info deploy/tmplatformd.service MemoryMax=2G объявлен как «bounds the control plane», но ограничивает cgroup ЮНИТА — а по собственному аргументу этого же файла (закрытие PD-13) в этом cgroup живёт каждый ребёнок-tmctl. Значит потолок общий на платформу и все идущие прогоны, и OOM-killer выберет самый жирный процесс — движок, который держит ЭКСКЛЮЗИВНЫЙ лок на файле проекта: ровно тот исход, ради которого запрещён SIGKILL. То же про TasksMax=512. Латентно до появления воркера. ⚠ Под systemd не проверялось (нет sudo) — вывод из семантики MemoryMax=, не из замера — закрыто: семантика сверена по man 5 systemd.resource-control («absolute limit on memory usage of the executed processes in this unit… out-of-memory killer is invoked inside the unit»). MemoryMax=2G заменён на MemoryMax=80% — потолок машины, а не сервиса, как «last line of defense» и без знания о железе; TasksMax=512 оставлен с честным комментарием, что покрывает платформу и прогоны вместе; ограничение ОДНОГО прогона названо работой воркера (transient scope). Побочно найдено и закрыто следствие, которого в этой строке не было, — PD-64. ⚠ Под systemd не запускалось (нет sudo); systemd-analyze verify (systemd 259) — exit 0 fixed(P2, дерево сессии) приёмка P1 (ревью деплой-юнита)
PD-56 bug info internal/pgstore/credits.go:35-63 Grant/Adjust на несуществующий аккаунт отдают оператору сырую ошибку Postgres с именем констрейнта (credit_ledger_user_id_fkey), тогда как Balance на том же входе отдаёт ErrNoAccount. Живая проба CLI. Косметика админ-поверхности, но опечатка в id читается как поломка БД — закрыто: appendLedger мапит нарушение credit_ledger_user_id_fkey в ErrNoAccount; pgstore.TestMoneyOperationsAgreeOnAMissingAccount требует одного ответа от Grant/Adjust/Balance/ReadAccount. Посадка «убрать сверку констрейнта» падает fixed(P2, дерево сессии) приёмка P1 (живая проба CLI)
PD-57 hardening minor internal/login/login.go:239-242 Защита от IdP mix-up не та, что требует действующая норма. RFC 9700 §2.1 (OAuth Security BCP, янв. 2025) — клиент SHOULD применять параметр iss из авторизационного ответа (RFC 9207) либо иной контрмер НА ОСНОВЕ iss; MAY — различные redirect URI на провайдера. Реализована собственная сверка st.Provider с h.cfg.Provider, а внутри одного хендлера это сравнение конфигурации с самой собой: start пишет туда то же значение. iss авторизационного ответа не читается вообще (iss ID-токена библиотека проверяет — это другой шаг и другой момент). Сегодня не эксплуатируемо: провайдер один, код всегда редимится у него же. Заведено потому, что регистр объявляет PD-32 закрытием «класса IdP mix-up», а против нормы это неверно, и при втором провайдере выбор (iss или раздельные redirect URI) должен быть ОСОЗНАННЫМ, а не побочным эффектом конфигурации — закрыто реализацией нормы, а не обещанием. Первоисточники сверены: RFC 9700 §4.4.2 («When an OAuth client can only interact with one authorization server, a mix-up defense is not required» — то есть СЕГОДНЯ несоответствия нет, требование включается со вторым сервером), §4.4.2.2 объявляет раздельные redirect URI фолбэком («SHOULD therefore only be used if other options are not available»); альтернатива «iss из ID-токена» нам не подходит — при чистом code flow токен приходит уже ПОСЛЕ отдачи кода. Выбран iss авторизационного ответа: Google его шлёт (authorization_response_iss_parameter_supported: true, сверено живьём). Сделано: auth_states.issuer (миграция 00008), сверка до обмена кода, отказ на СОРВАННОМ параметре у поддерживающего провайдера (RFC 9207 §2.4). Пин — login.TestAuthorizationResponseIssuerIsChecked (4 случая); посадки «убрать вызов», «убрать ветку несовпадения», «убрать ветку сорванного параметра», «потерять issuer в сторе» падают fixed(P2, дерево сессии) приёмка P1 (сверка с RFC 9700 §2.1 / RFC 9207)
PD-58 hardening minor internal/config/config.go:60-61 Несоответствие собственной объявленной базовой линии. ENGINEERING_STANDARDS §2 берёт ASVS 5.0 L2, а L2 требует ДОКУМЕНТИРОВАТЬ сроки: 7.1.1 (срок бездействия и абсолютный предел + обоснование отклонений от NIST SP 800-63B), 7.1.2 (политика одновременных сессий), 7.1.3/7.6.1 (согласование срока НАШЕЙ сессии со сроком федеративной — у нас наша живёт своей жизнью, RP-initiated/back-channel logout нет). Сроки 14 суток бездействия и 90 суток абсолютных существуют только литералами в коде, обоснования нет ни в одном доке (проверено grep). Механические требования V7 при этом ВЫПОЛНЕНЫ и проверены: 7.2.3 энтропия (256 бит при требуемых 128), 7.2.4 ротация токена на аутентификации, 7.4.1 отзыв, 7.4.2 снос сессий при удалении аккаунта. ⚠ 7.4.5 (системный отзыв админом) покрыт только пер-пользовательским revoke; 7.5.2 (пользователь видит свои сессии) — работа П-1 — закрыто, и значение выровнено вместо сочинения оправдания. Тексты сверены дословно: ASVS 5.0 7.1.1/7.1.2/7.1.3, 7.6.1/7.6.2 и NIST SP 800-63B-4 §2.1.3 («overall timeout … SHOULD be no more than 30 days at AAL1; an inactivity timeout MAY be applied but is not required»). Абсолютный срок 90 суток был отклонением от SHOULD без причины, выдерживающей проверку, — снижен до 30 суток; бездействие 14 суток остаётся и строже нормы. Документ — STACK_DECISIONS §13: уровень AAL1, оба срока, политика одновременных сессий (лимита нет — контракт предусматривает куку и Bearer одновременно; вместо лимита отзыв, «выйти везде» и журнал), рассогласование с федеративной сессией названо прямо (RP-initiated/back-channel logout нет), 7.6.2 выполнено. Пин — config.TestSessionClocksStayWithinTheDeclaredBaseline fixed(P2, дерево сессии) приёмка P1 (сверка с ASVS 5.0 V7)
PD-62 bug minor internal/pgstore/identity.go:26-35, миграция 00005 login.State.StartID не персистился: колонки под него не было. Поле заведено в P1 и логируется колбэком как login_start_id — то есть ОБЕ строки лога, которые должны сшивать две половины входа, в проде пустые. Батарея этого не видела, потому что тесты internal/login ходят в in-memory стор, который хранит структуру целиком: свойство проверялось не на том объекте, который едет (тот же класс, что PD-46). Воспроизведено против живой БД раунд-трипом PutLoginStateTakeLoginState: положили REQ-ABC123, получили ""закрыто: колонки start_id и issuer добавлены миграцией 00008, Put/Take их несут; пин — pgstore.TestLoginStateIsSingleUseAndExpires сравнивает структуру ЦЕЛИКОМ (reflect.DeepEqual), поэтому следующее поле без колонки упадёт здесь же. Посадки «потерять start_id» и «потерять issuer» падают fixed(P2, дерево сессии) сессия P2 (найдено при правке PD-57)
PD-63 vuln minor internal/httpapi/serve.go:118-127 (удалён) ClearReadDeadline воспроизводил PD-2 — тем самым вызовом, который был заведён как его исправление. Доккоммент объявлял его ОБЯЗАТЕЛЬНЫМ для стримингового хендлера. На полу-кормленном запросе (тело анонсировано, не дослано) дренаж внутри записи заголовка ответа — единственная граница соединения, и ограничен он ReadTimeout; снятие дедлайна ДО записи заголовка эту границу убирает. Замерено: хендлер остаётся внутри WriteHeader и через 4 с после ухода клиента, соединение держится. Вызов после флаша бесполезен — контекст уже отменён дренажем. Приёмка (PD-51) заключила «код безвреден», проверив только корректные запросы; случая, где функция помогает, нет вовсе — закрыто: функция УДАЛЕНА, §12 переписан, пин — httpapi.TestHalfFedStreamingRequestIsCutLoose (контекст стримингового хендлера отменяется в пределах Read) fixed(P2, дерево сессии) сессия P2 (собственный замер при верификации PD-51)
PD-64 bug minor deploy/tmplatformd.service Дефолтный OOMPolicy=stop уронил бы платформу из-за одного прожорливого прогона. Следствие того же факта, что PD-55 (дети-tmctl живут в cgroup юнита), но в той строке не названо: по man 5 systemd.service дефолт берётся из DefaultOOMPolicy= (системный — stop), а stop означает «the unit's processes are terminated cleanly by the service manager» — то есть OOM-килл ОДНОГО tmctl останавливает контрол-плейн и все остальные прогоны, после чего юнит уходит в oom-kill failed и его подхватывает Restart=on-failureзакрыто: OOMPolicy=continue проставлен явно с обоснованием; платформа переживает килл ребёнка и штатно закрывает его резервацию. ⚠ Под systemd не проверялось (нет sudo) — вывод из доки; systemd-analyze verify (systemd 259) — exit 0 fixed(P2, дерево сессии) сессия P2 (ревью деплой-юнита при PD-55)
PD-65 vuln minor internal/login/login.go:367-382 Обмен кода и загрузка JWKS шли БЕЗ дедлайна, тогда как discovery на том же пути ограничивает себя пятью секундами и называет причину («http.DefaultClient не имеет собственного таймаута, а вызов делается, пока человек ждёт»). В проде httpClient равен nil, поэтому обмен идёт на http.DefaultClient, а go-oidc строит набор ключей от context.Background(); WriteTimeout у сервера нет по проекту — значит издатель, который принял соединение и не отвечает, держит хендлер, пока клиент сам не уйдёт. Хуже того, набор ключей ОБЩИЙ: одна зависшая загрузка паркует ВСЕ параллельные входы (замерено ревью: два независимых входа ждали 12 с за одной загрузкой) — закрыто: identify ограничен providerTimeout 10 с; пин — login.TestAStalledProviderDoesNotHoldTheCallback на обеих ногах (token и keys), посадка «убрать дедлайн» падает fixed(P2, дерево сессии) ревью P2 (линза oidc-security, подтверждено верификатором на боевой проводке)
PD-66 bug minor internal/httpapi/serve_test.go, cmd/tmplatformd/main.go:121 Мой собственный фикс PD-46 закрывал только половину и утверждал, что обе. Тест строил свой сервер NewServer(…, DefaultTimeouts()) и на него же смотрел; проводка демона осталась ненаблюдаемой, а cmd/tmplatformd тестов не имеет. Замерено ревью: замена аргумента на Timeouts{Shutdown: 15s} оставляет make check зелёным (0 issues) и бинарь снова пиннит соединения — PD-2 в полном объёме. То есть ровно та форма, которую PD-46 и называл: свойство проверено не на том объекте — закрыто устранением КЛАССА, а не тестом: NewServer больше не принимает Timeouts и берёт DefaultTimeouts() сам, передавать нечего; коротким дедлайнам тестов служит неэкспортируемый serverWithTimeouts fixed(P2, дерево сессии) ревью P2 (линза net-http)
PD-67 vuln minor internal/pgstore/credits.go:236 FOR UPDATE не был запинен ничем, а комментарий теста утверждал обратное («Mutation caught: … or the FOR UPDATE that serialises it»). Последовательный тест лока не видит по построению, а инвариант «кэш = леджер» его тоже не ловит: без лока кэш и леджер уезжают ВМЕСТЕ, оба в минус. Лок — единственное, что мешает двум прогонам потратить один и тот же кредит — закрыто: pgstore.TestConcurrentHoldsCannotOvercommitAnAccount — 60 раундов по два конкурентных холда, каждый по отдельности посильный, вместе нет; посадка «убрать for update» падает 3 прогона из 3, баланс уходит в $2. Комментарий последовательного теста исправлен fixed(P2, дерево сессии) ревью P2 (линза sql-money)
PD-68 bug minor internal/httpapi/server.go:111 /readyz рапортовал «готов» на базе БЕЗ схемы. Готовность доказывалась одним Ping, который успешен на любом достижимом Postgres, включая пустой. Migrate выключен по умолчанию, а deploy-инструкция делает миграцию отдельным шагом — значит «процесс поднят, схема не накачена» это НОРМАЛЬНАЯ середина выката, и инстанс в этом окне отвечал 200 ready, проваливая каждый запрос, который затем обслуживал — закрыто: Store.Ready сверяет goose_db_version с максимальным номером миграции, вшитой в бинарь; схема ВПЕРЕДИ бинаря готовности не отменяет (иначе выкат ронял бы старый инстанс). Пин — pgstore.TestReadinessRefusesADatabaseWithoutTheSchema, посадка «свести готовность к Ping» падает. fixed(P2, дерево сессии) ревью P2 (линза вне карты)
PD-69 bug minor internal/pgstore/store.go:42 Явный pool_max_conns из DSN молча отбрасывался. Проверка «MaxConns равен дефолту pgxpool» не отличает «оператор не выбирал» от «оператор выбрал ровно это число»: pgxpool кладёт свой дефолт в то же поле, что ParseConfig заполняет из pool_max_conns. Оператор, порезавший реплику под бюджет max_connections, получал наш 16 вместо своих 8 — и наоборот на 32-ядерной машине. С pool_min_conns хуже: дефолт pgx равен 0, поэтому явный 0 не мог пережить проверку НИКОГДА — закрыто: вопрос «упоминает ли DSN этот ключ» задан ПАРСЕРУ pgx, а не значению: pgxpool достаёт pool_* из RuntimeParams и удаляет их, поэтому второй pgx.ParseConfig их ещё видит — обе формы DSN, кавычки и service-файлы бесплатно. Пин — pgstore.TestExplicitPoolSizesInTheDSNSurvive, включая случай, сломавший ПРЕДЫДУЩУЮ эвристику: пароль, содержащий имя ключа fixed(P2, дерево сессии) ревью P2 (линза вне карты)
PD-70 bug major internal/auth/middleware.go:57, internal/auth/cookie.go:62 Скользящее окно бездействия для БРАУЗЕРА не работало: Max-Age куки пишется один раз, на входе, и больше никем. Серверная строка скользила (Touch), кука — нет, а SetSession зовётся ровно из одного места — колбэка входа. Следствие: для куки — единственной презентации, которую код вообще умеет выдавать, — срок жизни сессии был ФИКСИРОВАННЫЕ 14 суток от входа независимо от активности; человек, заходящий каждый день, выкидывался на 14-е сутки при живой серверной сессии, а абсолютный срок не мог наступить никогда. Это же делало ложным §13 — документ соответствия ASVS 7.1.1, который зона только что написала — закрыто: при скольжении окна кука переиздаётся с тем же токеном (ротация — акт границы входа, не скольжения); пин — auth.TestSlidingTheIdleWindowRefreshesTheBrowsersCookie (четыре случая: кука во второй половине окна, свежая кука, Bearer, упор в абсолютный срок). ⚠ Срок куки — min(idle, остаток абсолютного): безусловный idle-TTL дал бы куку, пережившую абсолютный срок, и 401 вместо чистого «вы вышли» fixed(P2, дерево сессии) ревью P2 (линза doc-vs-code)
PD-73 vuln minor internal/login/login.go:67-74, :126 Дедлайн identify ограничивал ОЖИДАЮЩЕГО, а не саму загрузку ключей — то есть мой фикс PD-65 был неполон. Provider.Verifier берёт набор ключей, построенный на discovery, а go-oidc хранит его через context.WithoutCancel и ходит за ключами на http.DefaultClient, у которого таймаута нет. Загрузка, которая зависла, продолжает висеть после того, как ожидающий сдался, и все последующие входы встают на тот же inflight — то есть вход не поднимается и после того, как эндпоинт выздоровел, вплоть до перезапуска процесса. Воспроизведено ревью на боевой проводке — закрыто: New ВСЕГДА ставит httpClient с таймаутом providerTimeout, клиент передаётся NewProvider безусловно (oidc.ClientContext), и его подхватывает набор ключей; nil-случая больше нет — класс устранён, а не покрыт тестом. Пины — TestTheDefaultProviderClientIsBounded (посадка «клиент без таймаута» падает) и TestAHungKeyFetchDoesNotPoisonLaterSignIns (вход ПОСЛЕ выздоровления эндпоинта обязан пройти) fixed(P2, дерево сессии) ревью P2 (линза the-fixes)
PD-74 bug minor internal/auth/middleware.go:57 Скольжение окна залипало на последней четверти жизни сессии: каждый запрос становился записью. Touch прижимает новый дедлайн через least(now+IdleTTL, absolute_expires_at), поэтому как только now+IdleTTL перевалил за абсолютный потолок, idle_expires_at больше не двигается — а условие «осталось меньше половины окна» с этого момента истинно ВСЕГДА. На горячем пути это UPDATE по первичному ключу таблицы сессий и Set-Cookie на каждом аутентифицированном запросе (после PD-70 — ещё и кука). Найдено двумя линзами независимо — закрыто: скольжение выполняется только пока IdleExpiresAt строго меньше AbsoluteExpiresAt; пин — auth.TestTheSlideStopsOnceItCannotMoveTheDeadline (пять чтений дают ноль записей, а сессия с запасом по-прежнему скользит) fixed(P2, дерево сессии) ревью P2 (линзы session-security и вне карты, независимо)
PD-75 bug minor cmd/tmplatformctl/main.go write() (греп Past this point the write is committed) CLI сообщал о ПРИМЕНЁННОМ начислении как о провале, а повтор начислял второй раз. write выполняет денежную операцию, затем отдельным запросом читает баланс, и ошибку ЧТЕНИЯ возвращает как результат команды. Оператор видит ошибку, повторяет — а --key необязателен, и без него newKey чеканит новый ключ идемпотентности, поэтому второй прогон начисляет ещё раз. Достаточно обрыва соединения между двумя запросами — закрыто: после коммита команда не может отчитаться провалом; баланс читается как любезность, его отказ печатается предупреждением на той же строке fixed(P2, дерево сессии) ревью P2 (линза вне карты)
PD-76 bug minor internal/login/login_test.go Определяющее свойство пакета — «ничего выданного провайдером не персистится» — проверялось утверждением, которое не могло упасть. memStore.notes объявлено и не заполнялось ни одним методом, поэтому strings.Join(notes) всегда пусто, а Contains всегда ложно. Свойство названо в доккомменте пакета первой строкой — закрыто: мок пишет в saw КАЖДУЮ строку, которую поток ему передал, а утверждение проверяет и непустоту записи, и отсутствие среди неё и access-токена, и любого JWT-образного значения. Посадка «положить в стор сырой id-токен» падает fixed(P2, дерево сессии) ревью P2 (линза water)
PD-77 bug info internal/ingest/supervisor.go:104 Штатная остановка живого прогона поднимала тревогу о сломанном синке. Ingest проверяет ctx.Err() в начале цикла и возвращает context.Canceled как СВОЮ ошибку; Run отличить это от отказавшего синка не мог и на обычном SIGTERM писал ERROR «stream could not be materialized», который по замыслу означает «платформа ослепла, пока тратятся деньги», плюс звал stop() на уже останавливающемся прогоне — закрыто: отменённый runCtx больше не считается отказом синка fixed(P2, дерево сессии) ревью P2 (линза вне карты)
PD-78 hardening info internal/login/login.go (было), internal/httpapi/problem.go (было), internal/pgstore/identity.go, internal/httpapi/server.go Свод воды и дублей, найденный линзой лаконичности; каждый пункт проверен удалением. (а) login.Routes мемоизировал mux через sync.Once — при этом ВТОРОЙ и последующие guard молча игнорировались, то есть это была не оптимизация, а ловушка; снято. (б) login.Fail носил *http.Request, который никто не читал, и ради несовпадения сигнатур существовал шим httpapi.Fail; параметр и шим удалены, WriteProblem подключён напрямую. (в) upsertIdentityOnce держал собственный begin/rollback/commit при наличии inTx — второй экземпляр того же кода. (г) Deps.APIPrefix — ручка, которую не выставлял ни один вызыватель; заменена константой. (д) Ready делал Ping и следом запрос — два round trip на пробу каждые несколько секунд. (е) пять полей тестовых двойников, которые писались и не читались; blockingSink не блокировал. (ж) money.USD считал руками с комментарием про переполнение MinInt64 — заменён на big.Rat.FloatString(6), проверено побайтовое совпадение на всём диапазоне fixed(P2, дерево сессии) ревью P2 (линза water) + самопроверка

Закрытые — эра P3 (деплой и фикс-пак приёмки P2)

ID Класс Серьёзность Где Суть Статус Источник
PD-79 bug minor internal/money/money.go:33-36 Строковый "null" читается как НОЛЬ денег. Кавычки снимаются strings.Trim ДО проверки s == "null", поэтому "committed_usd":"null" даёт настоящий 0 и НЕПУСТОЙ указатель, тогда как доккоммент поля обещает отказ на «absent, null and empty». Замерено приёмкой на живом декодере: голый null и отсутствие поля дают nil (защита работает), "" даёт ошибку, а "null"Spend = 0 micro-USD, NON-NIL. На пути расчёта это «попытка стоила ничего»: холд освобождается, списания нет. Латентно до воркера — закрыто: литерал null судится ДО раскавычивания и оставляет значение нетронутым; кавычки снимает encoding/json, а не strings.Trim — слово null, пустая строка и экранированная цифра выходят тем, чем являются, и каждое встречает ту же единственную проверку синтаксиса, поэтому отдельной ветки «пусто или null» не нужно вовсе. Пины: money.TestUnmarshalTellsTheNullLiteralFromTheWordNull (обе формы плюс невмешательство в значение) и ingest.TestSpendRefusesNonsense на шве. Обе посадки — «снять кавычки первыми» и «слово null есть ноль» — поймать поимённо fixed(P3, дерево сессии) приёмка P2 (замер оркестратора №15 + панель)
PD-80 vuln major internal/login/login.go:158,217-226 Вход выключается тремя запросами в секунду, и 429 колбэка ДОБИВАЕТ начатые входы. Ведро rate.NewLimiter(2, 20) одно на /auth/login И /auth/callback (login.go:122, единственный лимитер в зоне), а колбэк стирает login-куку ПЕРВОЙ строкой — до своей проверки лимитера. Следствие: анонимный поток на /auth/login не только закрывает вход всем (это PD-42, принято риском в форме «глобальный, не пер-адресный»), но и делает начатый вход невосстановимым: 429 приходит уже с Set-Cookie: __Host-tm_login=; Max-Age=0, поэтому повтор того же колбэка не пройдёт и после наполнения ведра. Воспроизведено приёмкой на боевом бинаре: 19 из 40 /auth/login прошли, дальше 429; честный колбэк с живым state получил 429 и стёртую куку. Независимо измерено панелью. — закрыто: два ведра вместо одного (startLimit/finishLimit, те же rate/burst — не делится именно ИСЧЕРПАНИЕ), и проверка лимитера ПЕРЕД ClearLogin. Пины: TestFloodingTheStartOfSignInDoesNotCloseTheEnd (поток на /auth/login не закрывает честный колбэк) и TestARefusedCallbackKeepsTheLoginItRefused (429 не стирает куку, состояние не съедено, повтор после снятия лимита доходит до 303). Посадки «одно ведро» и «очистка выше лимитера» падают fixed(P3, дерево сессии) приёмка P2 (живая проба + панель, две независимые линзы)
PD-83 hardening minor internal/httpapi/middleware.go:64 Фикс PD-3 не запинен в собственном месте: посадка «Recover логирует r.URL.Path вместо routeOf(r)» батарею ПЕРЕЖИВАЕТ, тогда как та же посадка в AccessLog ловится поимённо (TestAccessLogNamesTheRouteNotThePath). По правилу шапки этого файла половина PD-3 закрытой не считается — закрыто: TestPanicBecomesAProblemAndNamesTheRoute — паника за мультиплексором с {book} в паттерне; сверяется и route, и отсутствие идентификатора книги во ВСЕЙ строке (в ней же стек). Посадка r.URL.Path падает fixed(P3, дерево сессии) приёмка P2 (посадка мутации)
PD-84 hardening minor internal/login/login.go:222 Лимитер колбэка (фикс PD-29) не запинен: удаление всей проверки h.limiter.Allow() из callback оставляет батарею зелёной. Замер PD-29 (~880 строк/с с одного хоста) означает, что регрессия здесь тихо возвращает неаутентифицированного писателя в таблицу журнала — закрыто: TestBothLegsOfSignInAreRateLimited — десять колбэков подряд обязаны упереться в 429. Посадка «удалить проверку целиком» падает; её же ловит TestARefusedCallbackKeepsTheLoginItRefused fixed(P3, дерево сессии) приёмка P2 (посадка мутации)
PD-85 hardening minor internal/pgstore/identity.go:126-131 «Неподтверждённый адрес не поднимается на аккаунт» запинено только на ветке НОВОЙ личности: снятие условия in.EmailVerified в ветке ВОЗВРАЩАЮЩЕГОСЯ входа (обновление users.email) проходит батарею — TestUnverifiedAddressStaysOffTheAccount покрывает первый вход и переход в verified, но не обратный случай — закрыто: TestUnverifiedAddressStaysOffTheAccount продлён третьим шагом — ВОЗВРАЩАЮЩИЙСЯ вход с новым НЕподтверждённым адресом: users.email не двигается, identities.email записывает то, что пришло. Посадка «снять in.EmailVerified в ветке возвращающегося» падает fixed(P3, дерево сессии) приёмка P2 (посадка мутации)
PD-91 doc minor deploy/README.md:33-42, deploy/tmplatformd.service:43,48 Установка, исполненная дословно, даёт нестартующий юнит: /srv/textmachine не создаётся ни одной командой наброска, а ReadWritePaths= без префикса - на несуществующем пути валит сборку mount-namespace при ProtectSystem=strict. Заодно ProtectHome=yes против решения владельца «книги живут в ~/books»: детям-tmctl домашние каталоги под этим юнитом недоступны — либо книги переезжают в /srv/textmachine, либо юнит получает BindPaths=. ⚠ Вывод из systemd.exec(5), под systemd не исполнялось (sudo нет) — закрыто: каталог /srv/textmachine создаётся явной командой наброска (--system домашний каталог не создаёт), префикс - намеренно НЕ ставится (сервис без записываемого каталога обязан падать на старте, а не на первой записи через часы), ProtectHome=yes оставлен с названной ценой и двухстрочным выходом (ProtectHome=tmpfs + BindPaths=), корень библиотеки на СЕРВЕРЕ/srv/textmachine, ~/books объявлено конвенцией машины разработки. Проверено живым прогоном systemd-run --user (systemd 259), а не докой: несуществующий путь без -226/NAMESPACE, созданный → 0/SUCCESS, с -0/SUCCESS (строка игнорируется); ProtectHome=yesPermission denied на /home/<user>; tmpfs+BindPaths → каталог виден fixed(P3, дерево сессии) приёмка P2 (панель, сверено с докой)
PD-95 doc major (для промта эмиттера) internal/ingest/events.go:6-12, docs/platform-PROGRESS.md §«Транспорт потока событий» Транспортная история зоны устарела против D39.106 в ДВУХ местах, и обе версии не совпадают с ратифицированной формой. Ратифицировано (D39.106 п.2 + research/25 §Форма): движок — транзиентный systemd-юнит на прогон, платформа ему НЕ родитель; события — events.jsonl в каталоге книги, append-only, как outbox-проекция уже закоммиченных строк SQLite (та же транзакция, что чекпойнт); платформа тейлит журнал, курсор (engine_run_id, seq) коммитится в одной Postgres-транзакции с эффектом. В research/25 вариант «платформа — родитель + пайп (stdout/fd)» получил 0 голосов из 15 («время жизни движка — подмножество платформы: деплой/рестарт убивает или осиротляет прогон»), выделенный fd 3 — 0, stdout/journald как источник событий — 0. Что в зоне: (а) доккоммент events.go предлагает переезд на fd/сокет — отклонённая форма; (б) журнал зоны длинно доказывает «канал остаётся stdout» и объявляет переезд отклонённым, ссылаясь на PD-59, который сам superseded тем же D39.106 п.3 («PD-59 superseded; пайп-путь supervisor.go P1 = дев-режим; ответ PD-13 переезжает на cgroup юнита прогона»). Оба текста прочтёт эмиттер-сессия как задание. — закрыто: доккоммент пакета переписан под D39.106 §2 — транзиентный systemd-юнит на прогон, платформа НЕ родитель, events.jsonl в каталоге книги как outbox-проекция коммитов SQLite, тейл с курсором (engine_run_id, seq), повторное чтение строк — норма (PD-105). Отвергнутые формы перечислены со счётом голосов, чтобы не вернулись свежей идеей. Доккоммент Supervisor помечен ДЕВ-РЕЖИМОМ там же, где он описывает пайп. ⚠ В журнале зоны нашлась ВТОРАЯ копия снятого ответа — блок «ОТВЕЧЕНО приёмкой (PD-59)» в списке «Открытые вопросы после P1» п.4: эррата №15 пере-ставила другую секцию, эту не тронула. Текст оркестратора не переписан — над ним поставлен баннер SUPERSEDED с ратифицированной формой; проверить принадлежность правки — за приёмкой fixed(P3, дерево сессии) приёмка P2 (эррата оркестратора №15, 07.08)
PD-100 bug minor internal/login/login.go:245-261 Класс PD-5 закрыт в auth/, но не в login/: колбэк глотает ошибку стора (TakeLoginState) и ошибку discovery, репортя их как обычный отказ (unknown_state / discovery_failed) — сама ошибка не доезжает ни до одной строки лога, хотя pgstore/identity.go намеренно отличает «состояния нет» от инфраструктурного сбоя. Аутентификационный DB-outage снова выглядит штормом обычных отказов — закрыто: сбой стора и сбой discovery уходят в ERROR; на проводе и в журнале — прежний отказ. login.ErrNoState заведён у владельца интерфейса (как auth.ErrNoSession), pgstore.ErrNoLoginState — то же значение под прежним именем. Пин TestInfrastructureFailuresInTheCallbackAreLogged: три случая, включая «обычное истечение НЕ логируется как авария» — ловит и посадку «логировать всегда» fixed(P3, дерево сессии) приёмка P2 (панель)
PD-106 standards minor cmd/tmplatformctl/ Админ-CLI — единственный писатель денег в дереве — не имеет ни одного теста. В том числе не покрыто правило, которое он сам называет несущим («после коммита команда не может отчитаться провалом», фикс PD-75), и разбор флагов, и формат вывода. Батарея зоны его не видит вовсе ([no test files]) — закрыто: cmd/tmplatformctl/main_test.go — восемь тестов. Несущее правило («после коммита ничего не отчитывается провалом») пинится через balanceReader — интерфейс с одним методом, заведён ровно затем, что правило нельзя проверить на сторе, который всегда работает. Плюс: спент-ключ → «no-op», сбой ДО коммита → ошибка и ни строки вывода, ключ без --key уникален на 100 прогонах, разбор флагов десятью случаями и сквозной прогон пяти команд по живой БД. fixed(P3, дерево сессии) приёмка P2 (панель)

Закрытые — эра P4 (раннер: юнит, очередь, реконсилятор, тейлер)

ID Класс Серьёзность Где Суть Статус Источник
PD-43 bug info internal/pgstore/credits.go Денежный контур не имеет ни одного вызывающего вне тестов: Hold/Settle/Release не зовутся, Sink не реализован, TypeSpend не декодируется. При первом реальном прогоне баланс не изменится. — закрыто: денежный контур получил вызывающих: runs.Service.Start берёт холд в ОДНОЙ транзакции с созданием прогона и записью очереди (pgstore.StartRun), реконсилятор закрывает его Settle по фигуре движка. Пины: pgstore.TestAdmittingARunWritesTheRunTheAttemptAndTheHoldTogether (посадка «убрать holdTx из транзакции» падает), TestARunThatCannotBePaidForLeavesNothingBehind, runs.TestARunThatEndsIsFinishedAndSettledAtWhatTheEngineSpent. Живая проба: грант $10 → прогон с потолком 100 глав → холд $3.00 → движок отчитался $0.42 → баланс $9.58 fixed(P4, дерево сессии) ревью «вне карты»
PD-81 standards minor internal/pgstore/credits.go:169-178 Заявленный ErrDuplicateHold на реальном пути недостижим: при ЖИВОЙ резервации повторный Hold падает на первичном ключе reservations_pkey (00007_credits.sql:66) и уходит наверх сырой ошибкой Postgres SQLSTATE 23505; объявленная ошибка приходит только когда строку резервации уже смахнули, а ключ леджера остался. Замерено приёмкой на живом PG в обеих формах. Деньги целы (balance == SUM(ledger), транзакция откатывается), но воркеру не на что смотреть, кроме текста ошибки — закрыто: при ЖИВОЙ резервации коллизия reservations_pkey мапится в ErrDuplicateHold (credits.go holdTx); объявленная ошибка стала достижимой на реальном пути. Пин — TestASecondHoldOnALiveReservationIsADuplicateNotASqlstate (сверяет и то, что отказ не двинул деньги) fixed(P4, дерево сессии) приёмка P2 (замер оркестратора №15 + панель)
PD-82 bug info internal/pgstore/credits.go:236-239 Hold на НЕСУЩЕСТВУЮЩИЙ аккаунт отдаёт ErrInsufficientCreditlockBalance ErrNoRows трактуется как «нет кредита»), а не ErrNoAccount: обещание PD-56 «один ответ на несуществующий аккаунт» покрывает Grant/Adjust/Balance/ReadAccount и на Hold не распространяется. Замерено приёмкой — закрыто: lockBalance при отсутствии строки баланса спрашивает, существует ли аккаунт, и отвечает ErrNoAccount против ErrInsufficientCredit. ⚠ Одним запросом это не выражается: Postgres запрещает FOR UPDATE на nullable-стороне внешнего соединения — проверено, поэтому вторая проверка идёт только на редком пути. Пин — TestMoneyOperationsTellAMissingAccountFromAnEmptyOne fixed(P4, дерево сессии) приёмка P2 (замер оркестратора №15)
PD-97 hardening info internal/pgstore/credits.go:212-216 Settle/Release отбрасывают флаг applied у hold_release: если ключ ("run_release", engineRunID) уже потрачен, резервация закроется, а деньги не вернутся — тихий no-op на денежном пути. Требует нештатной последовательности (закрытие, смахивание строки, повторное открытие того же engine_run_id), но ровно на такой последовательности стоит ErrDuplicateHoldзакрыто: releaseHold судит флаг applied; потраченный ключ релиза = ErrReleaseKeySpent и ОТКАТ транзакции, поэтому резервация остаётся ОТКРЫТОЙ и видимой оператору вместо тихого закрытия без возврата денег. Пин — TestAReleaseWhoseKeyWasSpentIsRefusedRatherThanSilent fixed(P4, дерево сессии) приёмка P2 (панель)
PD-99 hardening info internal/ingest/supervisor.go:102 INFO-лог «engine started» пишет args целиком. Сегодня безвредно, но воркер будет передавать движку идентификатор книги и потолок аргументами ⇒ book-id и денежная сумма попадут в INFO платформы (D39.84 + норма зоны «id книги в логи не текут») — закрыто: INFO-строка старта несёт имя команды и НЕ несёт argv (runner.Start, а также дев-путь ingest/supervisor.go), поэтому ни id книги, ни потолок в долларах в поток INFO не попадают. Пин — runner.TestTheStartLineNamesTheCommandAndNotItsArguments (посадка «вернуть "args"» падает). Живая проба на боевом бинаре: grep -c 'ceiling-usd|3.000000|bk_' daemon.log = 0 за полный прогон fixed(P4, дерево сессии) приёмка P2 (панель)
PD-105 standards major (для промта эмиттера) internal/ingest/decoder.go:96 Декодер и норматив зоны расходятся на дубле seq: декодер объявляет его фатальным ErrStreamGap, а ENGINEERING_STANDARDS §2 ратифицирует «at-least-once — норма, дубль — не ошибка». После фикса PD-12 цена выросла: сбой ингеста ОСТАНАВЛИВАЕТ прогон, поэтому одна задублированная строка убивает платный прогон, хотя ратифицированный путь ремонта — status --json. Внутри одного пайпа передоставки нет, так что отказ декодера защитим; непропорциональна РЕАКЦИЯ. ⚠ Пере-диспозиция (эррата №15): вес ПОВЫШЕН до major-для-эмиттера — при ратифицированном транспорте (тейл events.jsonl с курсором, D39.106) повторное чтение строк после краша читателя — НОРМА, а не аномалия пайпа, поэтому норматив «at-least-once, дубль не ошибка» буквально верен, и фатальный отказ декодера прямо ему противоречит — ЗАКРЫТО РАТИФИКАЦИЕЙ (D39.119) И РЕАЛИЗАЦИЕЙ. Транспорт — тейл events.jsonl с курсором, поэтому повторное чтение строк НОРМА: ingest.Tail пропускает seq <= last_seq идемпотентно и не возвращает ошибку, а pgstore.RunSink.Apply пере-проверяет тот же high-water mark ВНУТРИ транзакции эффекта. Тот же seq с ДРУГИМ payload = ErrPayloadConflict → карантин ПОПЫТКИ, то есть её проекции: материализация останавливается, а жизненный цикл прогона продолжается — движок тратит зарезервированные деньги, и наша неспособность прочитать журнал не повод их выбросить (pgstore.Quarantine пишет только run_attempts.quarantine_reason, свежесть переходит на ре-синк). Сверка по sha256 строки (run_attempts.last_line_sha256). Пропасть (seq > last+1) осталась ошибкой — строки потеряны, читать дальше нечего. Пины: ingest.TestARedeliveredLineIsNormalAndChangesNothing · TestTheSameSeqWithADifferentPayloadIsRefused · TestALostLineIsReportedRatherThanSkipped · pgstore.TestARedeliveredCountingEventDoesNotCountTwice (⚠ последний написан ПОСЛЕ того, как посадка пережила первую версию пина: прогресс — присваивание и потому идемпотентен сам по себе, считающий эффект — unit_done — нет). Фатальный ErrStreamGap на дубле в decoder.go остаётся только на ДЕВ-пути пайпа, где передоставки нет fixed(P4, дерево сессии) приёмка P2 (панель)
PD-108 bug major internal/ingest/supervisor.go:161 (было), internal/runner/engine.go Ратифицированный канал ремонта не мог работать НИ РАЗУ: tmctl status --json звался БЕЗ обязательного --config. Движок требует его на каждой команде, которая трогает книгу, и падает на разборе аргументов до того, как увидит книгу. Найдено чтением cmd/tmctl/invocation.go (не доки) и воспроизведено исполнением на бинаре, собранном из HEAD в скрэтчпаде: tmctl status --jsontmctl: --config book.yaml is required, exit 1; с --config <путь> доходит до чтения файла. Дефект латентный ровно потому, что вызывающих у канала не было (PD-43) — то есть первый же резюнк воркера получил бы отказ вместо отчёта — закрыто: прод-путь runner.StatusArgs/runner.Status всегда несёт --config <workdir>/book.yaml; дев-путь Supervisor.Status исправлен там же. Пин — runner.TestEveryEngineInvocationNamesTheBookConfig fixed(P4, дерево сессии) сессия P4 (ревью вне карты: чтение парсера движка + проба на HEAD-бинаре)
PD-109 bug minor internal/runs/reconcile.go Периодический резюнк ЗАТИРАЛ более точную проекцию потока своей грубой. status --json не делит стадии по волнам (строка 99), поэтому его агрегат, положенный поверх «draft 7/20 ∥ edit 1/20», заменял пофазные счётчики одним числом — а отчёт движка, который ещё не досчитал, заменял их НУЛЯМИ. Плюс вторая половина: сторож «поток уже говорил» читал LastSeq из СНИМКА свипа, взятого ДО тейла, поэтому прогон, чьи первые события пришли в этом же свипе, выглядел молчащим. Найдено живой пробой сквозного прогона, не тестом: карточка книги показала edit 10/10 через секунды после того, как журнал сказал edit 0/10закрыто: резюнк работает только там, где чинить нечего (курсор не двигался ЛИБО материализация в карантине), сторож судит курсор ПОСЛЕ тейла, и ApplyStatus не опускает счётчики (greatest). Пины — runs.TestALiveRunIsResyncedAtMostOncePerInterval и TestTheSweepMaterializesWhateverTheJournalHasGained fixed(P4, дерево сессии) сессия P4 (живая проба сквозного прогона)
PD-110 bug minor internal/ingest/tail.go Строки ДРУГОЙ попытки судились против НАШЕГО курсора. Журнал пер-книжный и append-only, значит резюм дописывает второй hello со своим engine_run_id и seq, начинающимся заново; строка при этом не несёт идентификатора потока — его говорит только последний хендшейк выше. Первая редакция тейлера этого не отслеживала, поэтому seq 2 предыдущей попытки встречался с нашим seq 2 и читался как ИЗМЕНЁННЫЙ payload, то есть как сигнал порчи: здоровый резюмнутый прогон отправлял сам себя в карантин. Найдено собственным тестом до всякой интеграции — закрыто: читатель ведёт область (mine), и до хендшейка, который он ПРИЗНАЛ своим, ничего не материализуется и ничего не судится. Пины — TestAnotherAttemptsStreamInTheSameJournalIsSkipped · TestARereadFromTheStartDoesNotMistakeAnotherAttemptForCorruption · TestEventsBeforeAnyHandshakeAreNotJudgedAgainstOurCursor (обе посадки — mine := true и mine := false — падают) fixed(P4, дерево сессии) сессия P4 (собственный тест)
PD-111 bug minor internal/ingest/tail.go seq хендшейка не персистился, поэтому ЛЮБОЙ резюм после него читался как пропасть. hello — это seq 1 потока, но первая редакция обрабатывала его отдельно и курсор не двигала: last_seq оставался нулём при уже сдвинутом байтовом хинте, и следующая же строка (seq 2) давала seq 2 after 0 → карантин на ровном месте — закрыто: хендшейк проходит те же правила, что любая строка, и двигает курсор; эффекта на read-model у него нет, эффект на курсор и есть смысл. Пин — TestAHalfWrittenLineIsLeftForNextTime (сверяет применённые seq 1,2 и продолжение 3,4 после дозаписи) fixed(P4, дерево сессии) сессия P4 (собственный тест)
PD-116 bug minor internal/pgstore/runs.go RecordSpawn, internal/runs/spawn.go Спавн попытки мог прийти ОДНОВРЕМЕННО из воркера очереди и из реконсилятора, и оба видели «не запущено». Воркер получает прогон заданием, реконсилятор находит его неспавненным на своём проходе — обе ветки законны и обе читали unit_name до записи. Дальше их спасала только уникальность ИМЕНИ юнита у systemd: второй systemd-run падал с «unit already exists». Выживание по чужому правилу — не корректность, и оно перестаёт работать в день, когда именование поменяется (например, резюм получит суффикс). Найдено собственным ревью кода на конкурентность, до отчёта — закрыто: RecordSpawn стал compare-and-set (where id = $1 and unit_name is null) и возвращает, досталось ли право; проигравший НЕ стартует и это не ошибка. Пин — runs.TestOnlyOneOfTwoConcurrentSpawnersStartsTheEngine (восемь конкурентных спавнеров, ровно один юнит); посадка «убрать and unit_name is null» падает fixed(P4, дерево сессии) сессия P4 (самопроверка на гонки)
PD-117 bug minor internal/httpapi/v0.go startRun Потолок ниже минимума схемы отвечал 409, а не 400. RunRequest.ceiling_chapters объявлен minimum: 1, и запрос с 0 или отрицательным — МАЛФОРМИРОВАННЫЙ; 409 же определён как «границы сдвинулись между чтением run-options и этим вызовом», поэтому клиент, получивший его, пере-читает run-options и повторяет запрос, который не может пройти НИКОГДА. Замерено ревью: {"ceiling_chapters":0} → 202 у хендлера и 409 после сервиса — закрыто: минимум схемы судится в хендлере, до сервиса. Пин — TestACeilingBelowTheSchemaMinimumIsARejectedRequestAndNotAMovedBound fixed(P4, дерево сессии) адверсариальное ревью (сверка со спекой, исполнением)
PD-118 bug minor internal/pgstore/books.go ReadUsage, runs.go PauseRun Usage.paused_reason был НЕДОСТИЖИМ через собственный путь паузы платформы. ReadUsage требовал finished_at is null, а PauseRun — путь реконсилятора — ставит finished_at тем же запросом, что и паузу. Значит поле заполнялось только когда стоп пришёл событием потока (sink.go, finished_at не трогает) и молчало, когда паузу вызвала платформа. Замерено ревью на живом PG — закрыто: состояние читается по ПОСЛЕДНЕМУ прогону каждой книги (lateral), без условия на finished_at. Пин — TestTheAccountReportsAPauseTheReconcilerCaused fixed(P4, дерево сессии) адверсариальное ревью (исполнением)
PD-119 bug minor internal/httpapi/v0.go getBook Карточка книги несла ревизию ПРОГОНА, которая отстаёт от книжной. Контракт: счётчик ОДИН на книгу и «каждое книго-скоупное чтение и id каждого кадра потока несут одно и то же число». unit_done двигает books.revision и chapters.revision, но не runs.revision, поэтому клиент, применивший кадр id=2, получал в карточке 0 и — по правилу самого контракта — обязан был чтение ОТБРОСИТЬ: карточка не обновлялась всю серию unit-done. Замерено ревью через настоящий RunSink (0→1→2 у книги при 0 у прогона) — закрыто: и BookDetail.revision, и Run.revision проецируются из счётчика КНИГИ. Пин — TestTheCardsRevisionIsTheBooksAndNotTheRuns fixed(P4, дерево сессии) адверсариальное ревью (исполнением)
PD-120 vuln minor internal/pgstore/books.go курсор пагинации Курсор из ЧУЖОЙ библиотеки принимался молча. Контракт прямо возлагает отказ на СЕРВЕР («rejecting a cursor from a dead epoch is the SERVER's duty, MUST, answered 400»), потому что клиенту токен непрозрачен по построению. Курсор нёс только (added_at, id) и не нёс метки коллекции, поэтому токен, построенный на библиотеке другого аккаунта, отдавал окно СВОИХ книг вызывающего вместо 400. Замерено ревью на живом PG (чужой курсор → err=nil, 3 строки). Утечки чужих данных нет — выборка всегда owner_id = $1, — но клиент получает не то окно и обнаружить это не может — закрыто: курсор несёт метку области (sha256("library"+owner), первые 8 байт), чужая метка = ErrBadCursor → 400. Пин — TestACursorFromAnotherLibraryIsRefused (плюс проверка, что свой курсор по-прежнему работает) fixed(P4, дерево сессии) адверсариальное ревью (исполнением)
PD-121 bug minor internal/httpapi/v0.go usageState /usage говорил «exhausted» там, где прогон стартует. Доля округляется ВНИЗ, поэтому $9 остатка от гранта $1000 дают 0%, а состояние выводилось из доли: экран аккаунта показывал «ничего не осталось», пока run-options на той же секунде отдавал шкалу в 300 глав и прогон запускался. Замерено ревью — закрыто: «exhausted» — факт о балансе (Usage.Spendable), а не следствие округления; оба экрана отвечают из одного факта. Пины — TestASmallRemainderIsLowAndNotExhausted, pgstore.TestASmallRemainderOfALargeGrantIsStillSpendable fixed(P4, дерево сессии) адверсариальное ревью (исполнением)
PD-124 bug major, деньги internal/runs/reconcile.go расчёт Расчёт брал ПОЖИЗНЕННУЮ трату КНИГИ и выставлял её как трату прогона. committed_usd из status --json движок считает как SELECT COALESCE(SUM(committed_usd),0) FROM spend WHERE book_id = ? (backend/internal/store/ledger.go в HEAD, «for a book across all days») — сумма по книге за всю историю. Значит каждый следующий прогон книги оплачивал заново всё, что она стоила раньше; перерасход ограничен холдом (Settle каппит), и в леджере он выглядит строкой «capped at the hold», то есть как перерасход ДВИЖКА, а не как арифметика платформы. Замерено двумя независимыми верификаторами на живом PG: прогоны по $1.00 и $0.50 списали $2.50; после того как пожизненная сумма книги перерастает потолок, каждый прогон стоит ровно свой потолок независимо от работы — закрыто: попытка записывает БАЗОВУЮ ЛИНИЮ книги перед стартом (spend_baseline_micro_usd, миграция 00010, читается status --json ДО создания юнита) и платит РАЗНИЦУ; базовая линия не прочиталась = попытка не стартует (платный прогон, который нельзя корректно выставить, хуже прогона, стартующего свипом позже). Пин — runs.TestASecondRunOnABookIsChargedOnlyForWhatItSpent fixed(P4, дерево сессии) адверсариальное ревью ×2, независимо, исполнением
PD-125 bug major, деньги internal/runs/reconcile.go restart Перезапуск при недоступном расчёте открывал ВТОРОЙ холд и терял первый навсегда. settle законно ОТКЛАДЫВАЕТ (движка не спросить) и возвращает nil; restart читал это как успех, брал новый холд на остаток и уходил дальше, а старая резервация оставалась открытой — и не попадала ни в один список: UnsettledRuns фильтровал по ЗАВЕРШЁННОСТИ ПРОГОНА, а ListLiveRuns берёт только попытку с ended_at is null. Замерено: прогон с потолком $3.00 показал $6.00 зарезервированных и закончил с $3.00, навсегда снятыми с баланса, при нуле в списке несведённых — закрыто: перезапуск СПРАШИВАЕТ (AttemptReservationOpen) и откладывается, пока предыдущая попытка не сведена; UnsettledRuns теперь ключуется на ЗАВЕРШЁННОСТИ ПОПЫТКИ, поэтому брошенная резервация видна и при живом прогоне. Пины — TestARestartIsDeferredWhileTheInterruptedAttemptIsUnsettled, TestAnInterruptedAttemptsHoldIsStillFoundWhileItsRunGoesOn fixed(P4, дерево сессии) адверсариальное ревью ×2, независимо, исполнением
PD-126 bug major, деньги internal/runs/spawn.go Юнит, который НЕ удалось создать, съедал бюджет прогона по свипу за раз. Право на спавн записывалось до Runner.Start, и при отказе systemd-run оставалась запись «юнит есть» без юнита и без маркера — то есть в точности форма прерванного прогона. Каждый свип перезапускал прогон: расчёт, новый холд, отказ спавна, снова. Замерено: шесть свипов — попытка 7 и $0.60 списано за движок, который ни разу не стартовал; при SweepEvery=15s весь потолок уходит за минуты — закрыто: неудавшийся Start СНИМАЕТ право (ReleaseSpawnClaim), и следующий свип повторяет ту же попытку вместо перезапуска прогона. Пин — TestAUnitThatCannotBeCreatedDoesNotEatTheRunsBudget (шесть свипов: попытка остаётся первой, баланс не двигается) fixed(P4, дерево сессии) адверсариальное ревью (исполнением)
PD-127 bug major internal/runs/reconcile.go drainJournal Одна нечитаемая строка журнала запирала прогон навсегда. Тейл шёл ДО чтения маркера и возвращал ошибку из всей сверки, а карантинились только пропасть и конфликт payload; малформированная строка, строка длиннее буфера, подменённый файл и битый хендшейк возвращали жёсткую ошибку каждый свип. Замерено: пять свипов — статус translating, finished_at пуст, холд $3.00 держится, при том что маркер на диске и движок давно вышел — закрыто: ЛЮБАЯ неустранимая ошибка журнала = карантин ПРОЕКЦИИ, а жизненный цикл (маркер, живость, расчёт) продолжается; отмена контекста карантином не считается. Пин — TestAnUnreadableJournalDoesNotStopTheRunFromFinishing fixed(P4, дерево сессии) адверсариальное ревью (исполнением)
PD-128 bug minor internal/runs/reconcile.go грация спавна Грация мерилась от старта ПРОГОНА, а не попытки, поэтому у перезапущенной попытки её не было вовсе: она наследует started_at многочасовой давности и признаётся потерянной, как только systemd не успел ответить. Замерено: через секунду после перезапуска — попытка 3 и три созданных юнита — закрыто: run_attempts.started_at читается отдельным полем и грация мерится от него. Пин — TestAnAdmittedRunIsGivenTimeBeforeItIsPresumedLost (снимок с часовым прогоном и пятисекундной попыткой) fixed(P4, дерево сессии) адверсариальное ревью (исполнением)
PD-130 bug minor internal/ingest/tail.go readLine Любая ошибка чтения превращалась в io.EOF, то есть в «догнали, нового нет»: отказ диска читался бы как тишина, материализация вставала бы молча и ни один свип не сказал бы почему — закрыто: только настоящий EOF означает «догнали»; всё прочее возвращается ошибкой и уходит в карантин с причиной fixed(P4, дерево сессии) адверсариальное ревью (чтение кода)
PD-131 bug minor internal/ingest/tail.go Хендшейк не обязан был нести seq 1. На этом транспорте hello ДВИГАЕТ курсор, поэтому hello с seq 0 оставлял курсор нулём, а первое настоящее событие отбрасывалось как его дубль; отрицательный seq уходил в ветку «уже применено». Пайп-декодер это требование имел всегда (decoder.go), файловый читатель — нет — закрыто: seq != 1 у хендшейка = ErrBadHandshake fixed(P4, дерево сессии) адверсариальное ревью (чтение кода)
PD-132 bug minor, деньги internal/runs/reconcile.go settle Холд прогона, который так и не стартовал, не возвращался. После введения базовой линии (PD-124) попытка без неё не сводилась вовсе, а попытка, которую никогда не спавнили, базовой линии и не имеет — её холд оставался зарезервированным навсегда. Найдено собственным тестом при починке PD-124 — закрыто: нет базовой линии И нет имени юнита ⇒ попытка не выполнялась, холд возвращается ЦЕЛИКОМ (Release); нет базовой линии, но юнит был ⇒ расчёт удерживается с ERROR-строкой, а не угадывается. Пин — TestTheHoldOfARunThatNeverStartedComesBackWhole fixed(P4, дерево сессии) самопроверка при починке PD-124
PD-133 bug minor internal/httpapi/v0.go contractRoutes Инстанс без движка не отдавал НИЧЕГО, вопреки собственной строке лога. Маршруты монтировались только когда есть И read-model, И жизненный цикл прогонов, поэтому инстанс с библиотекой и без раннера отвечал 404 на /v0/books, а его же стартовая строка говорила «библиотека отдаётся только на чтение». Замерено ревью — закрыто: ЧТЕНИЯ монтируются при наличии read-model, ручки прогона — при наличии жизненного цикла. Пин — TestAnInstanceWithoutARunnerStillServesTheLibrary fixed(P4, дерево сессии) адверсариальное ревью (исполнением)
PD-134 bug minor internal/pgstore/runs.go, internal/runs/spawn.go Пиннинг версии движка (строка 139) записывался и НИКОГДА не читался: run_attempts.engine_binary не входил в выборку реконсилятора, а спавн и канал ремонта брали путь из ТЕКУЩЕГО конфига. Пин, который никто не читает, — это колонка, а не пин: резюм исполнял бы то, что выкатили сегодня, а status --json спрашивал бы о книге бинарь другой версии — закрыто: EngineBinary читается в LiveRun и используется и резюмом, и каналом ремонта; конфиг остаётся фолбэком только для ещё не спавненной попытки fixed(P4, дерево сессии) адверсариальное ревью (чтение кода)
PD-135 bug minor internal/runs/reconcile.go restart Прерванный прогон без остатка бюджета помечался paused БЕЗ paused_reason (FinishRun его не трогает), тогда как контракт описывает PausedReason как причину паузы, и экрану сказать нечего — закрыто: используется PauseRun, который причину ставит fixed(P4, дерево сессии) адверсариальное ревью (чтение кода)
PD-136 doc minor deploy/tmplatformd.service Юнит нёс ОБЕ диспозиции сразу: старый абзац подавал ProtectHome=yes как «нужную позу на сервере» прямо над строками, ставящими tmpfs+BindPaths, а рассуждение о ресурсных потолках всё ещё исходило из модели «дети живут в cgroup этого юнита», снятой D39.106. Оператор, читающий сверху вниз, получал противоречивые инструкции в одном файле — закрыто: снятые абзацы удалены, потолки прямо названы границей КОНТРОЛ-ПЛЕЙНА, прогоны — своим срезом ⚠ ОСПОРЕНО(PD-136) собственной проверкой 04.09, статус НЕ меняется (норма шапки): ряд стоял fixed с формулировкой «снятые абзацы удалены», а абзац про ресурсные потолки был НА МЕСТЕ и открывался ровно снятой посылкой — «by the argument at the top of this file every tmctl the platform spawns lives in it… the control plane plus every run in flight, together (PD-55)», — противореча и шапке того же файла (⚠ A RUN IS NOT A CHILD OF THIS UNIT, D39.106), и последнему абзацу того же блока (Runs are not in this cgroup at all). То есть один файл держал ТРИ утверждения, из которых два спорят с третьим, 26 дней — противоречие возникло 09.08, когда в юнит въехала шапка D39.106 (git log -S 'A RUN IS NOT A CHILD OF THIS UNIT'd29e30c), и снято 04.09. ⚠ Абзац не снят, а переписан 04.09 (говорить «снят» было бы повторением той же ошибки, за которую ряд и оспорен): посылка заменена на проверенную — cgroup держит демона И его внутрипроцессных движковых детей (manifest/status, сборка экспорта, bank-apply), но НЕ прогоны. ⚠ Первая попытка правки того же дня сказала «control plane and nothing else» — это ложь в противоположную сторону, тоже исправлена. Заодно снято ложное обоснование TimeoutStopSec=90 («грация движка 30 с» при stopGrace = 10 * time.Minute, который к тому же стоит на ТРАНЗИЕНТНОМ юните прогона, а не на этом). Урок ряда, ради которого пометка и стоит: «удалено» в закрывающей формулировке — это утверждение о ДЕРЕВЕ, и его надо проверять грепом, а не памятью автора. fixed(P4, дерево сессии) адверсариальное ревью (чтение)
PD-138 standards info go.mod Прямые зависимости (riverqueue/river, riverdriver/riverpgxv5) стояли помеченными // indirect: make check тидинесс не проверяет, поэтому батарея этого не видела — закрыто: go mod tidy. ⚠ Строка оставлена как заявка: гейта на go mod tidy в батарее по-прежнему нет fixed(P4, дерево сессии) адверсариальное ревью
PD-142 standards minor вся зона, тесты Заявление «22 новых пина, каждый проверен своей посадкой» СНЯТО дофиксом 09.08 как непроверяемое в этом объёме: прогонов посадок было девять (9/10, затем 9/9), то есть «каждый из 22» ими не покрывался, а поимённого списка соответствия пин↔посадка сессия не вела. Проверено исполнением и названо поимённо другое: 33 посадки самопроверки пака и 24 посадки дофикса (список — журнал, раздел «Дофикс P4»). Аудит силы пинов посадками (139 мутаций, четвёртый верификатор): 107 поймано, 32 пережили, из них 8 — не ослабления (эквивалентный код либо страховка DDL-констрейнтом). Пережившие — не дефекты КОДА, а отсутствующие пины на свойства, часть которых объявлена закрытой; по правилу шапки этого файла такое свойство закрытым не считается. Закрыто: написаны 22 новых пина; заявление «каждый проверен собственной посадкой» снято — см. начало строки. Самые весомые: блокировка строки попытки под КОНКУРЕНЦИЕЙ (TestConcurrentDeliveriesOfOneEventCountItOnce — восемь горутин на одно событие; последовательная доставка поглощается одним high-water mark и посадку не ловила) · CSRF на КОНТРАКТНОЙ поверхности (TestACrossSiteRequestCannotStartARun — снятие CSRF из гарда /v0 переживало всё, а это старт платного прогона с амбиентной кукой) · RunSpent считает только свой прогон и не считает открытые холды · обе ветки отложенного расчёта · MarkSettled одноразов · хендшейк второго движка отвергается · монотонность байтового хинта · пустой payload · ETA-ноль · черновой юнит не «сделан» · paused при нехватке баланса · principal падает ЗАКРЫТО · внутренний текст не течёт в Problem. ⚠ Одна посадка («убрать and ended_at is null из RestartRun») пережила и НЕ является ослаблением: unique (run_id, attempt_no) отвергает всех проигравших гонку, так что ровно один перезапуск проходит и без неё — записано, а не подчищено fixed(P4, дерево сессии) адверсариальное ревью (аудит посадками)
PD-143 bug minor internal/runs/spawn.go spec, internal/pgstore/runs.go RestartRun Пиннинг версии движка (строка 139) был закрыт НАПОЛОВИНУ: запиненный путь читался каналом ремонта, но НЕ исполнялся резюмом. spec() брал Cfg.EngineBinary, а RestartRun не переносил engine_binary в новую попытку, поэтому перезапущенный прогон шёл на том бинаре, который выкачен СЕЙЧАС, — то есть перевод продолжала другая программа, и «резюм другой версией только явным флагом» не выполнялось. Найдено собственной пост-сверкой диффа с промтом (не ревью и не батареей: обе половины компилировались и все тесты были зелёными) — закрыто: новая попытка НАСЛЕДУЕТ engine_binary предыдущей, spec() исполняет запиненный путь, а переход на другую сборку требует явного TM_PLATFORM_RESUME_MAY_CHANGE_ENGINE. Пины — runs.TestAResumeStaysOnTheEngineBuildTheRunStartedWith и TestAResumeMovesToANewEngineBuildOnlyWhenItIsAllowed; обе посадки («spec берёт из конфига», «RestartRun не наследует») падают fixed(P4, дерево сессии) пост-сверка диффа с промтом

Закрытые — дофикс P4 и ре-чек V2

ID Класс Серьёзность Где Суть Статус Источник
PD-112 standards minor internal/httpapi/v0.go, контракт 0.2.0 Реализация отдаёт статусы, которых спека у операций НЕ перечисляет — четыре класса, все проверены исполнением адверсариальным ревью: 503 на старте прогона (деплой не может передать потолок/записать конец юнита) · 403 от CSRF-слоя на любом небезопасном запросе (спека описывает требование X-TM-Client в securitySchemes, но статуса ему не даёт) · 500 у любой операции при отказе стора (спека не перечисляет 5xx нигде) · 404 у GET /usage при отсутствующем аккаунте (спека даёт только 200/401; практически недостижимо — сессия ссылается на строку users внешним ключом). Строка ждала решения владельца контракта — закрыто РАТИФИКАЦИЕЙ (оркестратор №15, 09.08): 503 вносится в спеку правкой владельца контракта при лендинге; код зоны не меняется fixed(ратификация 09.08; правка спеки за оркестратором) сессия P4 (самопроверка против спеки)
PD-129 bug minor internal/pgstore/sink.go, runs.go Инверсия порядка блокировок между материализатором и финишером: RunSink берёт books … for update и затем правит runs, а FinishRun/PauseRun правили runs и затем books. Два реконсилятора на одном прогоне (перекрытие поколений деплоя) дают взаимоблокировку в обе стороны — замерено ревью, SQLSTATE 40P01 на обеих формулировках. Порчи нет (Postgres откатывает одну сторону), цена — провалившийся проход свипа и секунда детекта — ⚠ ПЕРЕ-ДИСПОЗИЦИЯ 09.08: строка была закрыта ЛОЖНО. Правка P4 привела к книге-первой только FinishRun/PauseRun; сам материализатор (RunSink.Apply) продолжал брать run_attempts … for update ПЕРВЫМ, а RestartRun — обновлять попытку до всего остального, и приёмка воспроизвела дедлок через реальные API (258 из 300 пар). Закрывающая формулировка описывала половину правки как целое. Действительно закрыто дофиксом — см. PD-145 fixed(дофикс P4, дерево сессии; см. PD-145) адверсариальное ревью (исполнением)
PD-144 bug major, деньги/шов internal/runs/spawn.go, internal/ingest/resync.go Движку передавался ПРИРОСТ там, где его флаг означает НАКОПЛЕННЫЙ книжный потолок. --ceiling-usd переопределяет ceilings.book_usd и сравнивается с committed + reserved книги на КАЖДОЙ резервации (backend/internal/store/ledger.go Reserve, backend/cmd/tmctl/invocation.go — «It caps the book's CUMULATIVE committed+reserved spend, not this run's increment»). Значит второй прогон книги, у которой накоплено ≥ прироста, отвергается первой же резервацией: движок выходит кодом 1, платформа обязана назвать это failed, работа не сделана, ретраи идентичны. Приёмка доказала обе стороны исполнением; сквозная проба пака этого не видела, потому что её фейк кумулятив не моделировал — закрыто: runs.meter.bookCap = committed + прирост (ратифицировано 09.08 ре-чеком V2 после PD-158; reserved в сумму НЕ входит), обе величины читаются ОДНИМ вызовом status --json перед стартом (bookMeter), reserved_usd внесён в аллоулист УКАЗАТЕЛЕМ (отсутствие ≠ ноль, как у committed), рестарт получает свежий отсчёт по тому же пути, фактически ушедшее значение хранится (run_attempts.ceiling_arg_micro_usd, миграция 00011). Пины: runs.TestTheSecondRunOfABookIsGivenTheCumulativeCapAndNotItsOwnIncrement и TestAResumeIsGivenACapComputedFromTheMeterAsItStandsNowоба через фейк ceilingJudge, который отвергает потолок ПО ПРАВИЛУ ДВИЖКА; TestAnAttemptWhoseMeterCannotBeReadIsNotStartedAtAll покрывает отсутствующий reserved_usd. Проба приёмки на этом дереве: --ceiling-usd 6.000000 при committed книги $3 fixed(дофикс P4, дерево сессии) приёмка P4 (F1, двусторонним исполнением)
PD-145 bug major internal/pgstore/sink.go Apply, runs.go RestartRun, internal/runs/reconcile.go Инверсия блокировок из PD-129 была жива, а транзиентный сбой из-за неё уходил в КАРАНТИН. RunSink.Apply брал run_attempts … for update первым, RestartRun правил попытку до всего остального, а FinishRun/PauseRun берут книгу первой — приёмка воспроизвела 258 дедлоков на 300 пар через реальные API. Усилитель хуже самого дедлока: 40P01 из Apply попадал в ветку «любая ошибка журнала = карантин», то есть проекция ЖИВОГО платного прогона слепла навсегда из-за блокировки, которая разрешилась сама — закрыто: порядок написан в одном месте и стал глобальным (pgstore.lockBook: books → runs → run_attempts → account_balances → reservations), книга блокируется первой в Apply, RestartRun, StartRun и DeleteBook (последний найден собственной сверкой всех транзакций пакета: цикла для него нет, но инвариант, у которого есть исключение, перестаёт быть инвариантом); классификация ошибки вынесена в runs.quarantines. ⚠ Формулировка «карантин остаётся только логическим ошибкам» была НЕВЕРНА в части и исправлена ре-чеком V2: первая редакция IsTransient знала только про дедлок и сериализацию, поэтому обрыв соединения с Postgres — то есть ШТАТНЫЙ рестарт управляемой базы (57P01/57P02/57P03, класс 08, сетевой сброс) — по-прежнему карантинил проекцию живого платного прогона НАВСЕГДА (пути снятия карантина в дереве нет) ⚠ «Снятия карантина в дереве нет» — верно на дату ряда и НЕВЕРНО с пака P13: ручка построена (PD-426, tmplatformctl run unquarantine --run <id>); фраза оставлена как часть замера, поправка 04.09.. Доказано исполнением приёмкой. Теперь IsTransient покрывает класс 08, 57P0x, pgconn.SafeToRetry и любой net.Error; ошибки чтения файла — *fs.PathError и net.Error не удовлетворяют, поэтому битый журнал по-прежнему останавливает проекцию, как и должен. Кейсы внесены в таблицу пина. Пины: pgstore.TestTheMaterializerTakesTheBookBeforeTheAttempt и TestARestartTakesTheBookBeforeTheAttempt (порядок утверждается ПРЯМО — блокировка книги удерживается, операция обязана ждать её, а строка попытки обязана остаться свободной под for update nowait), TestAMaterializerAndAReconcilerOnOneRunDoNotDeadlock (конкурентный, 60×3), runs.TestOnlyAJournalWeCannotReadStopsTheProjection fixed(дофикс P4, дерево сессии) приёмка P4 (F2, исполнением)
PD-146 standards major (пин) internal/runs/spawn.go bookMeter Сердце PD-124 не было запинено: посадка «нечитаемый отсчёт → (0, nil)» пережила ПОЛНУЮ батарею. С ней расчёт идёт против базовой линии 0, то есть прогон оплачивает всю пожизненную трату книги — ровно тот дефект, который PD-124 объявил закрытым. Отказ спавну был построен и не проверен ни одним тестом — закрыто: TestAnAttemptWhoseMeterCannotBeReadIsNotStartedAtAll — три формы нечитаемости (вызов упал · нет committed_usd · нет reserved_usd), и в каждой утверждается, что юнит не создан И попытка не заклеймлена (unit_name пуст), значит следующий свип её повторит; хвост теста показывает, что после починки движка та же попытка стартует fixed(дофикс P4, дерево сессии) приёмка P4 (F3, посадка)
PD-147 standards major (пин) internal/runs/spawn.go spawnAttempt Очистка устаревшего exit-маркера перед стартом не была запинена: удаление os.Remove(spec.ExitMarker) пережило полную батарею. Залежавшийся маркер той же попытки читается как её окончание при первом же взгляде реконсилятора: прогон, движок которого только что запущен, будет завершён и рассчитан заживо — закрыто: TestAStaleExitMarkerIsClearedBeforeTheUnitStarts — маркер создаётся ДО спавна, после спавна обязан отсутствовать, а свип обязан оставить прогон живым с открытым холдом fixed(дофикс P4, дерево сессии) приёмка P4 (F4, посадка)
PD-148 bug major, деньги internal/runs/reconcile.go settle, internal/pgstore/credits.go Расчёт по УСТАРЕВШЕМУ снапшоту свипа возвращал холд целиком попытке, которая потратила. Свип реконсилит по списку, прочитанному в начале прохода; быстрый прогон успевает стартовать, потратить и выйти, пока свип идёт по предыдущим — и ветка «попытки не было» судила по l.UnitName == "" из снапшота, хотя в БД спавн уже записан. Приёмка доказала исполнением: charged 0.000000 за попытку, потратившую 0.500000; недоплата безвозвратна и никем не ищется — закрыто: pgstore.ReleaseUnspawned перечитывает run_attempts.unit_name for update В ТОЙ ЖЕ транзакции, что и деньги, и отказывает (ErrAttemptSpawned), если юнит есть; реконсилятор откладывает расчёт до следующего прохода, где снапшот уже содержит юнит и попытка рассчитывается по своей базовой линии. Пин: runs.TestAStaleSnapshotDoesNotGiveBackTheHoldOfAnAttemptThatSpent (холд не возвращается целиком в проходе со старым снапшотом; следующий свип списывает ровно потраченное) fixed(дофикс P4, дерево сессии) приёмка P4 (F5, исполнением)
PD-149 bug minor internal/config/config.go loadRunner Относительный TM_PLATFORM_STATE_DIR не абсолютизировался, а маркер пишется и читается из РАЗНЫХ рабочих каталогов: ExecStopPost исполняется юнитом, у которого WorkingDirectory — каталог книги, а демон читает от своего cwd. Конец прогона становится невидим, реконсилятор перезапускает прогон бесконечно — закрыто: filepath.IsAbs на буте, отказ с именем переменной; пин config.TestARelativeStateDirectoryIsRefusedAtBoot fixed(дофикс P4, дерево сессии) приёмка P4 (F6)
PD-150 standards minor internal/pgstore/sink.go ApplyStatus «Метка давности» ре-синка была обещана промтом («честно, с меткой давности») и не построена: now в ApplyStatus не использовался, поля свежести не было, и у читателя замершей проекции карантинной попытки не было ничего, что сказало бы, насколько старые цифры он видит — закрыто: runs.last_resync_at (миграция 00012) пишется КАЖДЫМ ре-синком; пин pgstore.TestAResyncRecordsWhenItWasTaken (нет метки до первого · метка равна времени вызова · вторая переписывает первую). ⚠ Названная девиация: на провод метка НЕ выходит — в контракте v0 у прогона поля свежести нет; читается оператором и той ручкой, которая появится вместе с полем fixed(дофикс P4, дерево сессии; половина «на провод» — за контрактом) приёмка P4 (F7)
PD-151 doc minor docs/platform-PROGRESS.md раздел «Сессия P4» Числа отчёта расходились с истиной, а одно заявление было внутренне противоречиво: тело журнала давало «105 → 203, +98» (истина на момент приёмки — 242/+137/0), «36 новых строк = 25+10» (истина — 26 закрыто + 10 открыто), и «22 новых пина, каждый проверен своей посадкой (9/10, затем 9/9)» — 22 не покрываются девятью прогонами — закрыто: числа пересчитаны ИСПОЛНЕНИЕМ и приведены с командой (105 → 261, удалённых 0, добавленных 156 — итог дофикса с ре-чеком V2); арифметика 36 = 26 + 10 исправлена; заявление «каждый» снято (см. PD-142). Шапка журнала была права и не тронута fixed(дофикс P4, дерево сессии) приёмка P4 (F8)
PD-155 doc info deploy/README.md Смена TM_PLATFORM_STATE_DIR осиротляет exit-маркеры идущих прогонов: маркер пишется по пути, вычисленному при спавне, а читается по пути из текущей конфигурации, поэтому после смены каталога конец прогона невидим и прогон перезапускается — закрыто: строка в deploy/README.md — менять каталог только при отсутствии живых прогонов fixed(дофикс P4, дерево сессии) приёмка P4 (N4)
PD-156 bug info internal/runs/spawn.go spawnAttempt Отказ ДЕПЛОЯ проверялся после вызова движка. Дофикс перенёс чтение денежного отсчёта в начало спавна и тем поставил его ПЕРЕД дешёвыми отказами «нечем записать конец юнита» / «нечем передать потолок»: инстанс с неполной конфигурацией платил бы секундами CPU движка (status --json пере-нарезает исходник, строка 100) за каждый прогон на каждом свипе, чтобы прийти к ответу, зависящему только от конфигурации — закрыто: runs.runnable() вызывается первым в spawnAttempt, spec и Start; пин TestAMisconfiguredDeploymentIsRefusedWithoutAskingTheEngine (посадка «убрать ранний отказ» падает) fixed(дофикс P4, дерево сессии) собственная сверка диффа дофикса
PD-158 bug minor, деньги internal/runs/spawn.go meter.bookCap Формула потолка из пинга ратификации (committed + reserved (из status --json) + прирост; сам D39.122 §2в ратифицирует ОБЯЗАННОСТЬ платформы пересчитывать «прирост → абсолют», буквальной формулы в решении нет — овер-атрибуцию поправило ревью доков) даёт прогону запас БОЛЬШЕ его холда, когда предыдущий процесс умер с незакрытой резервацией. Найдено сверкой формулы с кодом движка: store.Open — путь ЗАПИСИ, которым идёт каждый translate — выполняет recoverReservations и обнуляет reserved_usd книги ДО первой судимой резервации (backend/internal/store/store.go:110 и :278 — якоря пере-нацелены 04.09, цели уехали с :88/:214); tmctl status читает read-only и этот проход намеренно не делает (store.go:117-132 OpenReadOnly говорит об этом прямо; якорь пере-нацелен 04.09 с :108). Значит цифра, которую видит платформа, — ОСТАТОК мёртвого процесса, и к моменту сравнения её уже нет: движок остановится позже на её величину, расчёт упрётся в потолок холда, а леджер запишет «capped at the hold», как будто перерасходовал движок. Величина ограничена размером остатка (обычно одна оценка вызова), но путь достижим на каждом резюме после падения. Сделано: bookCap считает committed + прирост — отклонение в консервативную сторону (более узкий потолок может только остановить прогон раньше, перерасхода не даёт), названо в коде и запинено TestAResumeIsGivenACapComputedFromTheMeterAsItStandsNow (второе утверждение: остаток НЕ раздул потолок). Сама цифра по-прежнему читается и обязательна (отсутствие ≠ ноль) — она и есть доказательство, что отклонение безопасно: на спавне другого писателя нет (эксклюзивный лок + один живой прогон на книгу), значит любой reserved — остаток. ЗАКРЫТО РАТИФИКАЦИЕЙ (оркестратор №15, 09.08, ре-чек V2): принята формула committed + прирост; аргументация подтверждена оркестратором исполнением обеих формул против гейта движка — ратифицированная переплачивала запасом ровно на leftover-reserved fixed(ратификация 09.08) собственная сверка формулы с кодом движка при F1
PD-159 bug major, деньги internal/runs/reconcile.go settle, internal/pgstore/runs.go SpendBound Отложенный расчёт прогона оплачивал работу СЛЕДУЮЩЕГО прогона той же книги, и тот платил за неё ещё раз. Расчёт читает пожизненный счётчик КНИГИ в момент ПОВТОРА, а откладываться он вправе (движка не спросить). Завершённый-но-нерассчитанный прогон при этом не мешает новому: HasLiveRun смотрит только на finished_at. Замерено: прогон, стоивший $0.10, списан на $2.10 — своя трата плюс всё, что успел потратить преемник, — после чего преемник заплатил ту же сумму снова; переплата ограничена холдом. Найдено ДВУМЯ независимыми верификаторами самопроверки, каждый воспроизвёл исполнением, и третий раз воспроизведено мной перед починкой — закрыто: SpendBound — наименьшая базовая линия среди попыток этой книги, стартовавших ПОЗЖЕ; она снята до того, как та попытка что-либо добавила, и после того, как эта остановилась, поэтому является точной верхней границей. Расчёт берёт минимум из неё и текущего счётчика. Пин: TestADeferredSettlementIsNotChargedForTheNextRunOfTheSameBook (первый платит $0.10, второй — свои $2.00, баланс и леджер сходятся) ⚠ ПАК P8-REVIEW 24.08: пин этой строки доказывает свойство СЛАБЕЕ, чем она гласит. Формула «НАИМЕНЬШАЯ базовая линия среди попыток, стартовавших позже» не исполняется: названный пин кладёт РОВНО ОДНУ более позднюю попытку, а на множестве из одного элемента min и max совпадают, и мутация minmax в internal/pgstore/runs.go AbandonRun (греп column is empty, so the message names it) проходит батарею. Живой пробел вынесен строкой PD-376 с готовым пином; статус этой строки паком НЕ менялся — если приёмка считает, что правило PD-1 требует пере-открытия, это одна правка, улика уже на месте ⚠ ОСПОРЕНО(PD-376) fixed(дофикс P4, дерево сессии) самопроверка дофикса (два верификатора, независимо, исполнением)
PD-160 standards minor (пин) internal/runs/reconcile.go drainJournal Проводка «транзиентный сбой НЕ карантинит» не пинилась: пин стоял на чистой функции quarantines, а удаление ветки, которая её ВЫЗЫВАЕТ, переживало батарею. Регрессия этой формы тихо карантинит проекцию живого платящего прогона — то есть ровно тот дефект, который F2 объявил закрытым — закрыто: TestADeadlockDoesNotStopTheProjection гонит НАСТОЯЩИЙ дедлок через весь путь (транзакция берёт строки в обратном порядке, Postgres рвёт цикл; раунд, где жертвой стала наша сторона, и есть предмет теста) и утверждает на КАЖДОМ раунде, что попытка не в карантине. Посадка «убрать ветку» падает за 1.9 с fixed(дофикс P4, дерево сессии) самопроверка дофикса (посадка)
PD-161 bug minor, деньги internal/pgstore/runs.go RecordSpawn Повторная заявка права на спавн ПЕРЕЗАПИСЫВАЛА базовую линию. «Не удалось создать юнит» — не то же, что «юнит не создан»: systemd-run, убитый по таймауту ПОСЛЕ подачи запроса, рапортует ошибку и оставляет движок работать. Право отдаётся обратно (ReleaseSpawnClaim), следующий свип заявляет его снова и кладёт в базовую линию счётчик, который этот же движок двигает, — попытка потом оплачивает разницу от цифры, уже включающей её собственную работу (недоплата, которую никто не ищет) — закрыто: повторная заявка сохраняет и базовую линию, и записанный аргумент потолка (coalesce / case when), там где юнит действительно не создан значения совпадают. ⚠ Ре-чек V2 показал, что этим строка закрыта НАПОЛОВИНУ: в БД значения сохранялись, а движку на ретрае уходил потолок, пересчитанный по СВЕЖЕМУ счётчику — то есть включающий трату собственного «призрака», — и форензик-колонка 00011 на этом пути лгала (замерено приёмкой: handed 3.400000 против stored 3.000000). Закрыто по-настоящему: на ретрае (SpendBaseline != nil && CeilingArg > 0) спавн ПЕРЕДАЁТ сохранённый аргумент, а не пересчитанный. Пин: TestAReclaimedAttemptKeepsTheBaselineItFirstRecorded — теперь утверждает и базовую линию, и равенство handed == stored fixed(дофикс P4, дерево сессии) самопроверка дофикса (ревью вне карты, чтением)
PD-167 doc info internal/pgstore/migrations/00009_runner.sql:37-39 Комментарий DDL обещает «хинт, не ведущий к seq = last_seq + 1, отбрасывается, и файл перечитывается с начала» — код так не делает: пропасть ведёт к карантину проекции и переходу на ре-синк. Расхождение док↔код, поведение верное — закрыто ре-чеком V2: комментарий приведён к тому, что делает тейлер. ⚠ Ре-чек назвал эту строку «застывшим комментарием миграции 00011»; предмет строки — комментарий 00009, а 00011 нёс отменённую формулу потолка и исправлен вместе с ней (обе миграции этим паком и написаны, нигде не применялись, поэтому их отпечатки в migrations.sha256 обновлены с явной причиной в шапке файла) fixed(ратификация + дофикс V2, дерево сессии) самопроверка дофикса (ревью вне карты)
PD-171 bug minor, деньги internal/runs/reconcile.go settle Счётчик книги НИЖЕ собственной базовой линии попытки списывал $0 молча. Это вырожденный случай — БД проекта подменили или восстановили из копии, — и клампить в ноль правильно (платить аккаунту за подмену файла код решать не вправе), но молчать нельзя: расчёт в ноль обнаруживался бы только по балансу — закрыто: WARN (не INFO: предмет — деньги) с фактом и без цифр (D39.84); пин TestAMeterThatWentBackwardsSettlesAtNothingAndSaysSo проверяет и отсутствие списания, и наличие строки, и что цифры в неё не попали fixed(дофикс V2, дерево сессии) ре-чек V2 (оркестратор №15)

Закрытые — третий раунд P5 (ре-чек оркестратора, 14.08)

ID Класс Вес Где Что и чем закрыто Статус Кем найдено
PD-197 standards major (гейт) Makefile tools-check, go.mod Гейт тулчейна не гейтил, а три дока утверждали обратное. Подъём floor 1.26.5 → 1.26.6 (пять адвизори stdlib) был сделан переменной GO_MIN_VERSION, которую читало только сообщение об ошибке, тогда как проверкой оставался регекс `go1.26.([5-9] [0-9]{2,})— он принимал ровно ту 1.26.5, ради отказа от которой floor и поднимали, и ставил 1.26.10 ниже 1.26.9. Клейм «хост на 1.26.5 получит отказ» стоял в журнале ×2,STACK_DECISIONSи комменте Makefile — **закрыто:** цельversion-check СРАВНИВАЕТ версии (sort -V, пререлизы rc/develотвергаются отдельно) и берёт версию из переменной, чтобы её судили версиями, которых на хосте нет;go.modполучилtoolchain go1.26.6его читает всякая сборка, мимо make тоже (GOTOOLCHAIN=autoскачает,=localостановится). Пиныgates.TestTheToolchainGateComparesVersionsRatherThanMatchingThem(таблица из 11 версий) иgates.TestGoModPinsTheSameToolchainTheBatteryDemands`, обе посадки падают; три клейма переписаны на описание механизма fixed(третий раунд, дерево сессии)
PD-198 doc info internal/pgstore/runs.go PauseRun, internal/books/parse.go Комментарии описывали до-фиксное поведение — в том числе на пути, который удаляет файл пользователя. PauseRun обещал проверку стопа «в том же стейтменте» (стоит отдельный select … for update в той же транзакции); parse.go объявлял отказ источника «терминальным с первого ответа», хотя дофикс провёл КАЖДЫЙ ответ движка через бюджет попыток — закрыто: оба текста приведены к коду; правок поведения не потребовалось. ⚠ Дописка: P6 переписал текст parse.go ещё раз под полосу отказов, где терминален ровно один класс (PD-196). ⚠ испр. оркестратором №16 15.08 при лендинге: переименование этой строки в PD-199 откачено (ID стабилен навсегда, коммит-первоисточник 69d485a), содержимое заведённой рядом второй строки слито сюда fixed(третий раунд, дерево сессии) ре-чек оркестратора (хвосты а, б)

Закрытые — дофикс-2 P5 (кросс-семейное ревью дофикса, 13.08)

ID Класс Вес Где Что и чем закрыто Статус Кем найдено
PD-192 bug major, данные пользователя internal/books/parse.go manifest, Parse Пропавший КОРЕНЬ хранилища читался как вина каждой книги. os.Stat(workdir) даёт ENOENT и на снесённом каталоге книги, и на несмонтированном BooksDir; первое терминально по устройству (FP5-3), значит размонтированный том заставлял ОДИН проход свипа терминально отклонить ВСЕ книги в интейке с source_unreadable — причиной, которая винит файл пользователя и не имеет обратного хода (PD-175). Путь создан моим же фиксом FP5-3 — закрыто: ErrStorageGone отличён от ErrDirectoryGone (корень спрашивается прежде, чем винить книгу), причина storage_unavailable не терминальна, бюджета не тратит и на книге не хранится; предикат waitsForTheDeployment собрал оба «ждущих деплой» случая. Пин books.TestAVanishedStorageRootIsNotEveryBooksFault, посадка падает ⚠ Дополнено ре-чеком (FP5-10): первая редакция закрывала не тот сценарий. Гард спрашивал Stat(BooksDir), а том, смонтированный РОВНО в BooksDir, оставляет после размонтирования пустой mountpoint — Stat успешен, и все книги снова терминально отклонялись; бут безусловным MkdirAll пересоздавал корень и маскировал пропажу. Теперь решение принимается по СЕНТИНЕЛУ провижининга .tmplatform-books, который пишет только первая загрузка (markStorage, O_EXCL) и не пишет бут: ни unmount, ни MkdirAll его не подделывают. Пин books.TestAnUnmountedVolumeLooksLikeAnEmptyRootAndStillIsNotTheBooksFault; первая редакция ПИНА посадку пережила (без сентинела ждёт всё) — добавлено утверждение, что загрузка сентинел пишет, иначе терялась терминальность крэш-окна FP5-3 fixed(третий раунд, дерево сессии) кросс-семейное ревью дофикса (Fable 5, линза интейка)
PD-193 bug major, деньги internal/pgstore/runs.go PauseRun Третий закрывающий путь без гарда живой попытки (после PD-181 и FP5-2): пауза не проверяла, что закрываемая попытка ещё жива и принадлежит этому прогону (апдейт попытки шёл даже без run_id). Проход старого поколения при перекрывающемся деплое паузит прогон, уже рестартованный в живую попытку 2: прогон отвечает paused/credit_exhausted под тратящим движком, а холд попытки 2 не виден ни в ListLiveRuns, ни в UnsettledRunsзакрыто: тот же exists (a.id = $4 and a.run_id = runs.id and a.ended_at is null), что у соседей, плюс run_id в апдейте попытки. Пин pgstore.TestAPauseFromAnOldSnapshotDoesNotCloseARunOverALiveAttempt, посадка падает fixed(дофикс-2, дерево сессии) кросс-семейное ревью дофикса (Fable 5, линза денег), воспроизведено дважды
PD-194 bug minor internal/pgstore/sink.go Begin Синк законченной попытки усыновлял хендшейк следующей. Стоп ДО спавна оставляет попытку закрытой и без engine_run_id — форма, невозможная до P5; устаревший материализатор биндил на неё engine_run_id попытки-заместителя и материализовал тот же журнал второй раз, удваивая счётчики глав и юнитов (деньги не двигались) — закрыто: бинд отказан для законченной попытки. Пин pgstore.TestAMaterializerOfAnEndedAttemptDoesNotAdoptTheNextAttemptsEngine, посадка падает fixed(дофикс-2, дерево сессии) кросс-семейное ревью дофикса (Fable 5, линза денег)
PD-195 bug minor, деньги internal/runs/reconcile.go settle Возврат холда «прогона, который не запускался», ждал ответа движка. Ветка «юнита не было — вернуть холд целиком» стояла ПОСЛЕ обязательного tmctl status, а хост, производящий эту ситуацию, — ровно тот, где движок не запускается: Status падает на каждом проходе, и деньги остаются зарезервированными навсегда (видимыми, но запертыми) — закрыто: ветка идёт до вызова движка; ReleaseUnspawned перепроверяет строку под замком, поэтому снапшот, который успели заспавнить, отвергается там. Пин runs.TestTheHoldOfARunThatNeverStartedComesBackOnAHostWhoseEngineCannotAnswer, посадка падает fixed(дофикс-2, дерево сессии) кросс-семейное ревью дофикса (гипотеза), подтверждено зоной исполнением

Закрытые — эра P5 (загрузка книги · стоп и резюм · наблюдаемость)

ID Класс Серьёзность Где Суть Статус Источник
PD-72 hardening info internal/httpapi/server.go:88 Отсутствие ОБЩЕГО лимита тела над маршрутами не наблюдаемо ничем. Пер-маршрутность (PD-35/PD-53) держится на том, что вложенный MaxBytesReader только УЖЕСТОЧАЕТ: это запинено TestBodyCapIsPerRouteBecauseNestingOnlyTightens. Но возврат внешнего слоя в New батарею переживает, потому что ни один маршрут не просит потолок БОЛЬШЕ дефолтного — наблюдаемым дефект станет ровно тогда, когда появится загрузка книги. — закрыто: маршрут загрузки приехал ВМЕСТЕ со своим потолком и своим тестом. POST /v0/books регистрируется guard(d.Upload.MaxBytes, …), пин — httpapi.TestAnUploadLargerThanTheRouteAllowsIsRefusedAsTooLarge (413 + problem+json на теле выше потолка И 201 на теле ниже него: потолок, который отвергает всё, не потолок), плюс TestTheUploadLimitBelongsToTheUploadRouteAlone — заявка на прогон с телом выше ДЕФОЛТНОГО потолка отвергается, то есть поднятие лимита одного маршрута не подняло его для остальных. Посадка «вернуть DefaultMaxBody на маршрут загрузки» падает fixed(P5, дерево сессии) ревью P2 (линза doc-vs-code)
PD-114 standards minor internal/config/, deploy/ Конфигурация: 27 переменных окружения, НИ ОДНОГО флага у демона и никакой печати эффективной конфигурации при старте (грепнуто). Выбор «только окружение» сам по себе мейнстрим (12-factor) и записан решением в доккомменте config.go; деплой использует systemd-нативные EnvironmentFile=+LoadCredential=. Ниже нормы другое: у оператора нет ни -version, ни «проверить конфиг и выйти», ни строки «вот что реально применилось» — не подхватившийся EnvironmentFile обнаруживается по поведению, а плоское пространство из 27 имён это та точка, где обычно переходят на файл. ⚠ Вопрос ВЛАДЕЛЬЦА (08.08), и он же нашёл этим вопросом реальный дефект в этом паке: дефолт «$/глава» лежал в двух местах (config клал ноль, разрешал вызыватель) — исправлено, дефолт резолвится только в config. Ратифицировано 09.08 (оркестратор №15 по делегации владельца): «только окружение» ОСТАЁТСЯ; строка переформулирована в задачу — печать ЭФФЕКТИВНОЙ конфигурации при старте с редакцией секретов (значение каждой настройки и откуда оно взялось: дефолт · переменная · файл; *_FILE печатается фактом наличия, не содержимым) — закрыто: config.Config.Settings несёт КАЖДУЮ прочитанную переменную с источником (default · environment · file), LogEffective печатает их построчно на старте. Редакция двойная и обе — правила проекта, а не вкус: секрет (DSN несёт пароль) и ЛЮБАЯ денежная сумма (D39.84/PD-99) печатаются фактом наличия и источником, без значения. Пины: TestEverySettingThisServiceReadsIsPrinted (сверка со СПИСКОМ TM_PLATFORM_* из исходника config.go — переменная, добавленная без записи, роняет батарею в том же коммите), TestASecretIsNamedAndNeverPrinted, TestAConfiguredAmountIsNeverPrinted fixed(P5, дерево сессии) вопрос владельца 08.08 + сессия P4
PD-140 bug info internal/runs/reconcile.go Stop, internal/httpapi/ Service.Stop построен и не подключён ни к чему: httpapi.Runs даёт только Bounds/Start, у tmplatformctl команды остановки нет. То есть контрол-плейн не умеет остановить прогон, который сам же запустил. Ручки POST /runs/{id}/stop и /resume в список работ промта не входили, поэтому это НЕ девиация пака, а честно названный хвост: остановить прогон сегодня можно только systemctl --user stop. — закрыто: ручки POST /v0/runs/{runId}/stop и /resume построены по спеке (202 + Run, 404 чужому, 409 на невозможное действие), runs.Service.Stop записывает НАМЕРЕНИЕ стопа в Postgres ДО сигнала и зовёт systemd, Resume переиспользует механику перезапуска реконсилятора (reopen) вместо второй копии денежной арифметики. Пины: TestTheStopIsRecordedBeforeSystemdIsAsked (хук внутри фейка systemd читает БД и требует, чтобы намерение уже было), TestAStopTheUnitDidNotTakeIsAskedAgainByTheSweep, TestResumeContinuesTheRunWithWhatIsLeftOfItsBudget, TestARunOfAnotherAccountCannotBeStoppedOrResumed fixed(P5, дерево сессии) адверсариальное ревью (чтение)
PD-178 standards info internal/runner/systemd_test.go, Makefile Гейт systemd-тестов сверял ТЕКСТ сообщения, а не способность хоста, и перестал гейтить. systemdOrSkip скипал по подстроке «Failed to connect to bus», а systemd 259 отвечает «Failed to connect to user scope bus» — на хосте без пользовательского менеджера три теста ПАДАЛИ вместо скипа. ⚠ Состояние пользовательского менеджера на стенде ПЛАВАЕТ между сессиями — в одной его нет (Linger=no, sudo нет), в следующей он жив (systemctl --user show отвечает Version=259.5) и все три теста проходят; замерено обоими способами в один день — закрыто: гейт спрашивает СПОСОБНОСТЬ (доходит ли процесс до своего менеджера), скип громкий и поимённый, как у БД-тестов; пин runner.TestTheSystemdGateAsksAboutTheCapabilityAndNotAMessage падает, если гейт снова начнёт сверять текст fixed(P5-дофикс, дерево сессии) сессия P5 (батарея на стенде)
PD-181 bug minor, деньги internal/pgstore/sink.go FinishRun, internal/runs/reconcile.go finishStopped Устаревший снапшот свипа мог до-финишировать уже РЕЗЮМИРОВАННЫЙ прогон и оставить холд новой попытки вне всех списков. Маркер завершённой попытки остаётся на диске (их никто не удаляет), а FinishRun гардил только finished_at is null — проход, держащий снапшот попытки 1, закрывал прогон, который стоп-резюм успел вернуть к жизни; попытка 2 с открытой резервацией не попадала ни в ListLiveRuns (там finished_at is null), ни в UnsettledRuns (там ended_at is not null). Достижимо стало ровно с появлением резюма, то есть этим паком — закрыто: FinishRun пишет только если закрываемая попытка ещё живая, и возвращает признак «закрыл», по которому вызыватель решает, считать ли деньги; пин runs.TestAStaleSweepDoesNotReFinishAResumedRunFromTheOldAttemptsMarker, посадка «снять гард» падает fixed(P5, дерево сессии) кросс-семейное ревью P5 (Fable, исполнением)
PD-182 bug minor, деньги internal/runs/reconcile.go finishStopped Стоп закрывал прогон под ЖИВЫМ движком, если заявка на спавн была отдана назад после того, как юнит уже создался. ReleaseSpawnClaim обнуляет unit_name, когда «юнит не удалось создать», а это не то же самое, что «не создался» — systemd-run, убитый после запроса, оставляет движок работать (зона это уже знает: ради этого случая сохраняется базовая линия). Путь стопа читал пустое имя как «процесса не было» и закрывал прогон; движок продолжал тратить, сигнала до него не доходило, следующий прогон книги впитывал его трату в свою базовую линию — закрыто: ненулевая spend_baseline при пустом имени юнита считается надгробием попытки спавна, имя юнита детерминировано, и платформа спрашивает systemd Alive прежде чем закрывать; живой юнит получает стоп. Пин runs.TestAStopDoesNotCloseARunWhoseGivenBackClaimLeftAnEngineRunning fixed(P5, дерево сессии) кросс-семейное ревью P5 (Fable, исполнением)
PD-183 bug minor internal/books/parse.go, internal/jobs/jobs.go Грация клейма разбора (10 мин) была КОРОЧЕ таймаута задания очереди (15 мин): свип воровал клейм у живого парса. В окне 1015 минут на одной проектной директории оказывались два tmctl manifest; проигравший умирал на эксклюзивном локе движка с exit 1, а exit 1 — это то, чем движок говорит «источник не разобрать», и книга отклонялась ТЕРМИНАЛЬНО с удалением исходника — закрыто: грация написана как jobs.JobTimeout + 5m, пара утверждается тестом books.TestTheClaimGraceOutlivesTheQueuesJobTimeout; вдобавок терминальная запись возможна только пока клейм ещё наш (parse_started_at = <наш>), а один exit 1 больше не терминален вовсе fixed(P5, дерево сессии) кросс-семейное ревью P5 (Fable, исполнением)
PD-184 bug info internal/metrics/metrics.go Лейбл method брался из запроса как есть. Для маршрута он свёрнут в (unmatched), а метод — токен, который выбирает вызывающий, и на неразобранном запросе он его же и придумывает: число рядов становится чужим ресурсом — закрыто: закрытый список методов, всё прочее — (other); пин metrics.TestAnUnroutedRequestGetsOneSeriesAndNotOnePerPath fixed(P5, дерево сессии) кросс-семейное ревью P5 (Fable)
PD-186 bug minor, деньги internal/pgstore/sink.go FinishUnspawnedStop Закрытие «стопа до спавна» не имело гарда живой попытки — того самого, что получил FinishRun (PD-181). Устаревший проход мог закрыть уже РЕЗЮМИРОВАННЫЙ прогон, и холд второй попытки выпадал из обоих списков; вдобавок каждый следующий резюм отвечал 409 навсегда, потому что продолжать было бы уже завершённый прогон — закрыто: попытка обязана быть живой (ended_at is null) и принадлежать этому прогону; пин runs.TestAStaleUnspawnedStopDoesNotCloseAResumedRun, посадка «снять гард» падает fixed(P5-дофикс, дерево сессии) приёмка P5 (FP5-2)
PD-187 bug minor internal/books/parse.go reject, manifest Крэш между сносом каталога и записью строки оставлял книгу в parsing НАВСЕГДА. Порядок «каталог, потом строка» был выбран как самоизлечивающийся, и посылка была ложной: снесённый каталог читался как «нет конфигурации», а эта причина терминальной не становится никогда — книга ждала конфигурацию, которую некуда положить — закрыто: пропавший КАТАЛОГ отличён от отсутствующей конфигурации (ErrDirectoryGone) и терминален сразу; пин books.TestABookWhoseDirectoryIsGoneIsRejectedRatherThanLeftWaiting fixed(P5-дофикс, дерево сессии) приёмка P5 (FP5-3)
PD-188 bug minor internal/books/parse.go, internal/pgstore/books.go ClaimParse Ожидание конфигурации ЖГЛО бюджет разбора. ClaimParse считает каждую заявку, а книга без конфигурации заявляется раз в грацию бесконечно — после пяти циклов ожидания бюджет был исчерпан, и ПЕРВЫЙ же ответ движка становился терминальным мгновенно, с удалением исходника; движок отдаёт exit 1 и на опечатку в book.yamlзакрыто: попытка возвращается (RefundParseAttempt), когда движок не был спрошен вовсе; пин books.TestWaitingForAConfigurationDoesNotBringDeletionCloser fixed(P5-дофикс, дерево сессии) приёмка P5 (FP5-4)
PD-189 bug minor cmd/tmplatformd/runner.go, internal/books/parse.go Sweep Бэкстоп гнал разбор под бюджетом прохода (2 мин) против 15 минут очереди. Восстановление большой книги убивалось дедлайном свипа, убийство читалось как «хост не может запустить движок», попытка списывалась — и так каждый проход, пока книга не отклонялась ЗА СВОЙ РАЗМЕР; вдобавок запись отказа шла на просроченном контексте и терялась — закрыто: у прохода интейка свой бюджет (jobs.JobTimeout + 1m), у каждой книги внутри — свой (jobs.JobTimeout), терминальные записи идут на контексте, переживающем дедлайн; пин books.TestOneBookInTheSweepGetsABudgetAParseCanLiveIn ⚠ Дополнено ре-чеком (хвост в): бюджет КНИГИ не равен бюджету ПРОХОДА — вторая книга прохода получала остаток от первой и жгла попытку на обрезанном дедлайне. Проход, которому осталось меньше jobs.JobTimeout, книгу больше не НАЧИНАЕТ (клейм берётся внутри Parse, поэтому отложенная книга не тратит ничего). Пин books.TestAPassTooShortForAParseStartsNoneAtAll fixed(третий раунд, дерево сессии) приёмка P5 (FP5-5)
PD-190 bug info internal/httpapi/v0.go uploadFailed Просроченный дедлайн загрузки уходил 500. Медленный клиент — не сломанный сервис, и 500 говорит клиенту обратное о том, помогает ли повтор — закрыто: 408 problem+json; вопрос о коде вне перечня спеки внесён в пакет PD-180; пин httpapi.TestAnUploadThatOutlivesItsDeadlineIsNotAnInternalError fixed(P5-дофикс, дерево сессии) приёмка P5 (FP5-6)
PD-191 bug minor internal/pgstore/runs.go PauseRun Стоп, пришедший в окно расчёта, отвечал paused/credit_exhausted вместо stopped — то есть «кончились деньги» вместо «владелец остановил» — закрыто: пауза отказывает при висящем интенте (ErrStopRequested), реконсилятор заканчивает прогон стопом; пин runs.TestAStopDuringSettlementOutranksThePause fixed(P5-дофикс, дерево сессии) приёмка P5 (FP5-8а)

Закрытые — эра P6 (потребительская половина шва эмиттера · интейк формы Б · дев-стенд)

ID Класс Серьёзность Где Суть Статус Источник
PD-200 bug minor internal/ingest/tail.go, internal/runs/spawn.go Тейлер УСЫНОВЛЯЛ чужой поток, лежащий на смещении попытки — и это было три дефекта в одной строке, а не наблюдение. Воспроизведено: журнал книги, в котором до нашего hello лежит чужой (tmctl, запущенный оператором руками в каталоге книги), давал (1) adoption чужого engine_run_id через Begin, (2) материализацию его событий на НАШУ попытку — включая ceiling, то есть paused у прогона, которого никто не паузил, и (3) отказ на нашем СОБСТВЕННОМ hello («seq 3, want 1») с карантином проекции платящего прогона навсегда. Закрыто: платформа НАЗЫВАЕТ поток до создания юнита (runs.engineStreamID, отдаётся движку как TM_TRACE_ID, строка 102) и пишет имя в той же транзакции, что claim; mine теперь спрашивает про ПРИМЕНЁННЫЕ строки (LastSeq > 0), а не про байтовый хинт, который у новой попытки ненулевой до первого чтения. Пины: ingest.TestAForeignStreamAtOurOffsetIsNeverAdopted · пин окружения юнита в runs. Живая проба: engine_run_id = tm-stream-run_…-1 в БД стенда до старта движка fixed(P6, дерево сессии) watch-пункт промта P6, воспроизведён
PD-60 bug minor internal/ingest/supervisor.go, шов ПЕРЕ-ДИСПОЗИЦИЯ (эррата №15, 07.08): постановка строки устарела. Она рассуждает про 64 КиБ пайпа, а ратифицированный транспорт (D39.106 п.2) — тейл events.jsonl с курсором: у файла обратного давления в этом смысле нет вовсе, зато появляются свои свойства (fsync-политика, ротация, отставание тейлера, поведение при заполненном диске). ⚠ ЗАКРЫТИЕ ПО ФАКТУ КОДА (P6): политика выбрана ЯВНО и залендена движком (D39.131) — эмиттер ДЕГРАДИРУЕТ ГРОМКО и прогон продолжается: emit не возвращает ошибку никогда, первый отказ пишется ERROR один раз на серию, строки остаются в outbox и повторяются целым префиксом, так что транзиентный ENOSPC/EIO самолечится, а fsync на событие сознательно не делается (журнал — проекция коммитов SQLite при synchronous=NORMAL, проекция не может быть durable сильнее источника). Блокировать прогон отвергнуто явно. У платформы обратного давления в этом смысле нет по построению: она ТЕЙЛИТ файл, а не кормится пайпом; её собственная деградация — карантин проекции с ре-синком (PD-105). Открытого остатка у строки нет fixed(P6, дерево сессии) приёмка P1 (абстрактный разбор двумя агентами, оба независимо)
PD-61 bug info шов, строка 103 Два свойства эмиттера, которые надо задать ДО его постройки, иначе они станут миграцией. (а) Сброс буфера на выходе: bufio.Writer вокруг потока плюс os.Exit/log.Fatal пропускает defer и теряет последние события — ровно те, что сообщают об окончании прогона. (б) Хвост при падении платформы: содержимое непрочитанного пайпа умирает вместе с читателем. Если требование «платформа перезапустилась, прогон продолжается» когда-нибудь появится, ответ — НЕ сокет (он даёт переподключение без возобновления), а журнал файлом: движок дописывает NDJSON в <jobdir>/events.ndjson, платформа тейлит его с чекпойнтом смещения в Postgres. Это переживает и падение платформы, и даёт реплей бесплатно. Оба агента пришли к этому независимо; прецедент — Bazel Build Event Protocol (файл или gRPC, не пайп родителя) ⚠ ЗАКРЫТИЕ ПО ФАКТУ КОДА (P6): оба свойства заданы до постройки и построены. (а) Буфера НЕТ ВОВСЕrunevents.Journal пишет строку одним write(2) без bufio, поэтому os.Exit/log.Fatal/паника не могут потерять ни finished, ни ceiling: терять нечего. (б) Ответ — журнал файлом с курсором (engine_run_id, seq), ровно как строка и предлагала; ратифицирован D39.106 §2 и построен обеими сторонами. Открытого остатка нет fixed(P6, дерево сессии) приёмка P1 (абстрактный разбор)
PD-113 bug major (контрактно видимый) internal/runs/reconcile.go outcome, движок stagerun.go Стоп по потолку сегодня НЕразличим от инфраструктурного отказа, и контракт при этом запрещает называть его failed. Движок возвращает потолок ошибкой (errReserveCeiling, сверено в HEAD), а exitCode мапит всё нераспознанное в 1 — значит по коду выхода «деньги кончились» и «упало» это одно и то же число; события потолка не существует (строка 103). Платформа честно ставит failed, хотя BookStatus требует paused и «никогда не failed», потому что стоп резюмируем. Единственный путь, которым платформа СЕГОДНЯ узнаёт о потолке, — событие потока, которого нет; ветка под него построена и запинена (TestACeilingHaltPausesTheRunWithItsReason, TestWhatTheUnitDidBecomesTheProductStatus, случай «a ceiling halt survives any exit»). — ЗАКРЫТО (P6, потребительская половина шва): потолочный стоп приезжает paused ДВУМЯ независимыми каналами и ни по одному не failed — событием ceiling потока и кодом выхода 4, потому что журнал, который не удалось записать, всё равно оставляет код. Причина пишется вместе со статусом (pgstore.RunEnding.PausedReason), а не вторым оператором, и читается ПОСЛЕ дренажа журнала, а не из снапшота свипа. Пины: runs.TestWhatTheUnitDidBecomesTheProductStatus (таблица с exit 4 и обеими причинами) · runs.TestACeilingHaltWithNoEventIsPausedWithItsReasonAndNotRestarted (сквозь свип: paused, причина, НЕ перезапущен, холд закрыт) · runs.TestACeilingEventArrivingInThisVerySweepStillDecidesTheEnding (стейл-снапшот). Живая проба на стенде с фейком-движком, имеющим правило потолка: ceiling{scope:book} + exit 4 → paused/credit_exhausted, прогресс 4/6 из потока, расчёт $0.08 из $5.10 fixed(P6, дерево сессии) сессия P4 (сверка контракта с кодом движка)
PD-141 bug info internal/pgstore/runs.go PauseRun PauseRun возвращает nil, когда строка прогона уже завершена, поэтому вызывающий рапортует паузу, которой не произошло. Идемпотентность здесь нужна (свип повторяется), но молчаливая — нет: различить «поставил паузу» и «было уже поздно» вызывающий не может — закрыто (P6): PauseRun возвращает paused bool, вызывающий логирует «run paused» только по нему, а отказ называет вслух. Пин — тест стейл-паузы в pgstore утверждает именно paused == false fixed(P6, дерево сессии) адверсариальное ревью (чтение)
PD-152 bug info internal/runs/reconcile.go outcome stopped для остановки, которую мы попросили, на реальном движке недостижим: tmctl ЛОВИТ SIGTERM и выходит кодом 1, поэтому ветка «не вышел сам + $SERVICE_RESULT=success» срабатывает только для процесса, умершего ОТ сигнала. Проба пака показала stopped на фейке, который именно так и умирал. Следствие: пользовательский стоп приедет как failed. ⚠ Дофикс 09.08 расширил строку: то же самое ломает ШТАТНУЮ ПЕРЕЗАГРУЗКУ. При ребуте пользовательский менеджер останавливает юниты корректно, ExecStopPost ОТРАБАТЫВАЕТ и маркер пишется — ревьюер снял живьём на этом хосте (транзиентный юнит, процесс ловит TERM и выходит 1): RESULT=exit-code CODE=exited STATUS=1. То есть на буте свип видит маркер и закрывает все живые прогоны как failed вместо перезапуска, а путь строки 138 покрывает только потерю питания (нет маркера) — и собственный тест TestARunInterruptedByARebootComesBackWithTheBudgetItHasLeft моделирует именно её. ⚠ Половина закрыта паком P5 (D39.128-ветка), половина осталась и это названо: ПОЛЬЗОВАТЕЛЬСКИЙ стоп больше не приезжает как failed — платформа пишет намерение стопа (runs.stop_requested_at, миграция 00014) ДО сигнала и классифицирует маркер по нему (outcome/stoppedOnRequest), с гардом гонки «стоп против самостоятельного финиша» по времени маркера и чистым исходом движка (0/2/3) поверх намерения; пины TestARunTheUserStoppedIsNotReportedAsFailed, TestARunThatEndedBeforeTheStopKeepsItsOwnOutcome, TestACleanExitOutranksAStopThatArrivedTooLate. ⚠ ОСТАТОК ЗАКРЫТ (P6): различимый код пришёл (exit 5 = пойманный сигнал, D39.131), и платформа читает его так: с ЗАПИСАННЫМ намерением — пользовательский стоп, без него — ПРЕРЫВАНИЕ, то есть путь перезапуска строки 138, а не похороны. Решение названо асимметрией: прочитать ребут как стоп пользователя значит оставить все прогоны хоста мёртвыми после штатной перезагрузки, прочитать ручной systemctl stop как прерывание стоит одного перезапуска. Пины: runs.TestAGracefulSignalIsAStopOnlyWhenThisPlatformAskedForOne · TestAGracefulSignalNobodyAskedForBringsTheRunBack · TestAGracefulSignalWeAskedForEndsTheRun. Живая проба на стенде: systemctl --user stop → попытка 1 interrupted, попытка 2 открыта; наш /stopstopped, второй попытки нет fixed(P6, дерево сессии) приёмка P4 (N1)
PD-163 bug info internal/pgstore/books.go ListBooks, GetBook Ревизия области читается ВТОРЫМ запросом после страницы, поэтому под конкурентной материализацией она новее строк: клиент, соблюдающий контрактное «отбрасывай чтение с меньшей ревизией», навсегда потеряет кадры между двумя запросами. Потребителя (SSE) сегодня нет — закрыто (P6): страница и ревизия области читаются ОДНОЙ транзакцией (ListBookslistBooksTx), и то же сделано карточке книги (GetBook + lastRunTx), где ревизия книги читалась до строк прогона fixed(P6, дерево сессии) самопроверка дофикса (ревью вне карты)
PD-164 bug info internal/runner/marker.go, internal/runs/reconcile.go Маркер, который существует, но не разбирается, вечно валит реконсиляцию своего прогона: аналога карантина у этого пути нет, и ошибка чтения маркера возвращается наверх на каждом свипе. Запись атомарна (temp+fsync+rename), так что нужен внешний фактор (правка оператором, битый том). — закрыто (P6): неразбираемый маркер отделён от нечитаемого — runner.ErrBadMarker против ошибки ввода-вывода — и ведёт к терминальному концу прогона с exit_result = marker-unreadable (значение, которого systemd не производит), а не к вечному повтору. Ошибка ЧТЕНИЯ по-прежнему возвращается наверх и повторяется, как и должна. Пин — runs.TestAnUnreadableExitMarkerEndsTheRunInsteadOfWedgingIt (плюс «холд закрыт») fixed(P6, дерево сессии) самопроверка дофикса (ревью вне карты)
PD-165 hardening info cmd/tmplatformd/runner.go markerArgv, internal/runner/runner.go quoteArgv Относительный TM_PLATFORM_CTL_BIN проходит os.Stat, но systemd требует АБСОЛЮТНЫЙ путь в ExecStopPost — каждый старт падает, и виден только цикл заявка-откат. Тот же класс: quoteArgv не экранирует $ (подстановка переменных systemd в Exec-строках), поэтому каталог состояния с таким символом молча ломает командную строку маркера. ⚠ % проверен и БЕЗОПАСЕН — спецификаторы в значениях --property= не раскрываются (замер §17); $ в этой сессии исполнением не проверялся. — закрыто (P6), причём двумя разными ответами: относительный TM_PLATFORM_CTL_BIN теперь отвергается на буте (markerArgv, пин TestARelativeExitMarkerCommandIsRefusedAtBoot) — воспроизведено исполнением, systemd 259 отвечает «neither a valid executable name nor an absolute path» и не стартует юнит целиком. А $ ЗАМЕРЕН и оказался безопасен: через systemd-run --user --property=ExecStopPost=… путь с $dir доехал ЛИТЕРАЛЬНО, и доехал даже при --setenv=dir=EXPANDED (маркер лёг в …/dollar$dir/, не в …/dollarEXPANDED/) — то есть экранирование в $$ было бы ошибкой, а не защитой. Тот же результат, что у % (§17) fixed(P6, дерево сессии) самопроверка дофикса (ревью вне карты)
PD-196 bug minor internal/books/parse.go defer_, движок Опечатка оператора в рукописном book.yaml стоит файла пользователя. Движок отображает ВСЕ свои отказы на exit 1 (его собственный комментарий), поэтому «этот источник нечитаем» неотличимо от «конфиг синтаксически битый»: после пяти циклов книга отклоняется как source_unreadable и исходник удаляется. FP5-4 закрыл только «конфигурации нет вовсе». — ЗАКРЫТО (P6, потребительская половина): движковая половина приехала полосой отказов (D39.131), и интейк судит по КОДУ, а не по типу ошибки: source_unreadable — ТОЛЬКО exit 11, config_invalid (10) — деплойный класс, который ждёт человека и не тратит бюджет попыток, всё прочее (12 · 19 · обычный exit 1 · сигнал · движок не запустился) — parser_unavailable, который отклоняет книгу, но НЕ удаляет её файл. Пины: books.TestOnlyTheOneRefusalClassAboutTheTextEverCostsTheUpload (четыре класса × «файл на месте») · TestAnEngineThatDidNotAnswerIsNeverAVerdictAboutTheBook · ingest.TestOutcomeOfCoversTheWholeExitContract. Живая проба: настоящий tmctl без ключей провайдера вышел 10 на стенде — прогон failed, холд вернулся целиком, файл цел fixed(P6, дерево сессии) кросс-семейное ревью дофикса (Fable 5)
PD-206 vuln minor (условная) internal/config/config.go Load, internal/login/dev.go Имя провайдера дев-входа не было зарезервировано, и TM_PLATFORM_OIDC_PROVIDER — свободный ввод оператора. Ключ личности — (provider, subject); издатель, настроенный под именем dev, кладёт свои sub в то же пространство, куда пишет дев-стенд, и sub, совпавший с дев-субъектом, разрешается в ДЕВ-аккаунт с его засеянным кредитом. Класс PD-32/§10a (тихое связывание чужих аккаунтов), но здесь — одна переменная окружения. Четвёртое из четырёх заявленных свойств недостижимости дев-входа в проде было без этого ЛОЖНЫМ. Закрыто: Load отвергает TM_PLATFORM_OIDC_PROVIDER == login.DevProvider; заодно TM_PLATFORM_DEV_LOGIN тримится, иначе значение из пробелов монтировало вход с личностью «пробел». Пины — config.TestAnIssuerCannotBeConfiguredUnderTheDevelopmentProvidersName и TestAWhitespaceDevelopmentSubjectMountsNothing, обе посадки падают. Плюс второй, ИЗБЫТОЧНЫЙ гард в main.go (ветка дев-входа стоит первой в switch, и без него регресс одной строки конфига дал бы дев-входу перекрыть боевой OIDC) — он по построению недостижим, пока держится первый, поэтому теста не имеет, и это названо fixed(P6, дерево сессии) адверсариальное ревью P6 (кросс-семейное, Fable)
PD-207 bug minor, деньги internal/pgstore/runs.go RestartRun, internal/runs/reconcile.go restart Устаревший снапшот свипа ВОСКРЕШАЛ прогон, который другое поколение уже закрыло. RestartRun отказывал только по намерению стопа на ЖИВОМ прогоне (stopRequested != nil && finished == nil), а на прогоне уже ЗАВЕРШЁННОМ проходил: чистил finished_at, брал новый холд и спавнил движок для прогона, владельцу которого уже сказали, что тот кончился. Воспроизведено: пользователь жмёт стоп, поколение A закрывает прогон как stopped, поколение B со снапшотом ДО стопа видит маркер exit 5 без намерения и перезапускает. Класс PD-181, и эта сессия его РАСШИРИЛА, добавив ветку «exit 5 без намерения = перезапуск». Закрыто: RestartInput.OnlyIfLive — реконсилятор просит гарантию «прогон ещё жив», резюм (который работает по определению с завершённым) не просит. Пин — runs.TestAStalePassDoesNotResurrectARunAnotherPassHasFinished, посадка падает с «stopped → translating» fixed(P6, дерево сессии) самоаудит лендинг-диффа P6
PD-208 bug info cmd/tmplatformctl/seed.go Сид рапортовал «кредит начислен» о начислении, которого не было. Он звал store.Grant напрямую и ВЫБРАСЫВАЛ возвращаемый applied, а ключ идемпотентности детерминирован по аккаунту — значит второй прогон seed на том же стенде печатал «granted», начислив ноль. Тот же класс, ради которого в этом же пакете уже был написан хелпер write (его собственный комментарий: «Applied» и «ключ уже потрачен» — разные исходы, и оператору говорят, какой он получил), плюс вторая половина: неудачное ЧТЕНИЕ баланса после закоммиченной записи роняло весь сид, тогда как write печатает предупреждение и продолжает. Закрыто: сид зовёт write; проверено исполнением на стенде — второй прогон печатает «no-op: key … was already used» fixed(P6, дерево сессии) адверсариальное ревью P6 (линза повторного использования)
PD-209 bug minor, деньги/статус internal/runs/reconcile.go outcome, reconcile, restart; internal/pgstore/books.go Причина daily_ceiling стиралась ДВУМЯ путями, и оба возвращали ложный «кредит кончился». (A) exit 4 БЕЗ материализованного события закрывался как credit_exhaustedа это не редкий угол: карантинная попытка не дренится вовсе, значит КАЖДЫЙ её потолочный стоп шёл этим путём. (B) прогон, чей поток уже сказал ceiling (синк ставит paused без finished_at, то есть прогон остаётся живым), при исчезнувшем без маркера юните уходил в restart, а его ветка исчерпания жёстко зашивала credit_exhausted поверх. Последствия обе: ReadUsage зажигает АККАУНТНЫЙ halted-флаг на аккаунте с деньгами, и гард резюма дневного потолка обходится — резюм разрешён, новая попытка мгновенно упирается в тот же дневной лимит, цикл повторяем пользователем. Закрыто: третье значение ceiling_unknown (миграция 00015) для потолка, чей scope платформа не установила — резюмируемо, но НЕ зажигает аккаунтный флаг; прогон с уже названной причиной закрывается как пауза, а не перезапускается. Пины: runs.TestACeilingHaltWithNoEventIsPausedWithItsReasonAndNotRestarted · TestACeilingThatTheStreamReportedSurvivesAUnitThatVanished · таблица TestWhatTheUnitDidBecomesTheProductStatus; три посадки падают, четвёртая (фолбэк причины в ветке исчерпания) ПЕРЕЖИВАЕТ по построению и это названо в коде fixed(P6, дерево сессии) адверсариальное ревью P6 (линза денег и гонок)
PD-210 bug minor, шов internal/pgstore/runs.go StartRun/RestartRun/RecordSpawn, internal/pgstore/sink.go Begin/effect Окно усыновления чужого потока оставалось между ДОПУСКОМ и спавном. Имя потока писалось при RecordSpawn, а допущенная попытка уже попадает в ListLiveRuns и уже дренится — с want == "", то есть с усыновлением первого встречного hello. Окно — секунды внутри tmctl status или целый интервал свипа; coalesce(engine_run_id, …) в RecordSpawn при этом ОТКАЗЫВАЛСЯ исправить усыновлённое чужое имя на собственное, и прогон слеп на всю жизнь, а чужой ceiling материализовался на него (с новым outcome это перебивает даже чистый exit 0). Закрыто: имя даётся в той же вставке, что создаёт строку попытки (pgstore.EngineStreamID — формат переехал туда, где известен id прогона), RecordSpawn присваивает, а не сохраняет чужое. Побочное следствие, найденное тем же ревью: Begin стал недостижим для новых попыток, и вместе с ним тихо выключилось обновление chunker_version — эффект перенесён в effect, куда hello приезжает обычным событием. Пины: runs.TestAnAttemptIsNamedBeforeAnythingCanBeSpawnedForIt · pgstore.TestTheChunkerVersionOfTheStreamReachesTheBook; обе посадки падают fixed(P6, дерево сессии) адверсариальное ревью P6 (линза денег и гонок)
PD-211 bug info internal/pgstore/sink.go unitDone Апсерт разрешённого юнита был «побеждает последний ПРИШЕДШИЙ», а не последний по времени. Именованный остаток эмиттера — строка, закоммиченная в outbox и не дошедшая до файла, — переанонсируется СЛЕДУЮЩИМ процессом: приезжает со своим seq (то есть проходит high-water mark) и несёт время СТАРОГО события. Юнит, передреденный внутри предыдущей попытки из flagged в shipped, регрессировал обратно. Счётчики не страдают (они считают строки) — страдает ровно та диспозиция, из которой читающая поверхность выводит состояние юнита. Закрыто: where excluded.at >= unit_resolutions.at; пин pgstore.TestAReAnnouncedOlderResolutionDoesNotOverwriteANewerOne, посадка падает fixed(P6, дерево сессии) адверсариальное ревью P6 (линза денег и гонок)

Закрытые — дофикс P6 (приёмка оркестратора №16, 15.08)

ID Класс Серьёзность Где Суть Статус Источник
PD-224 bug major (деньги, несущий путь) internal/runner/engine.go Status, internal/ingest/supervisor.go Status status --json с exit 2 читался как ОТКАЗ, а движок так отвечает про любую книгу с помеченной единицей — отчёт при этом уже напечатан (cmd/tmctl/render.go renderStatusJSON). Следствия на ОБЫЧНОМ пути: расчёт денег такого прогона откладывался вечно (холд висел — класс PD-162), bookMeter отказывал каждому следующему прогону книги, ре-синк умирал; дев-путь имел тот же дефект — закрыто: exit 2 при разбираемом отчёте = ответ, exit 2 с нечитаемым stdout остаётся отказом, знание живёт одним местом (ingest.CompletedWithFlags). Перечень команд, способных выйти 2, снят чтением движка: translate, status, redrive; manifest — нет. Пины runner.TestAFlaggedBookStillAnswersAboutItsMoney и сквозной runs.TestTheMoneyOfAFlaggedBookIsSettledThroughTheRealExitContract (настоящий процесс, деньги сверены балансом и суммой леджера) fixed(дофикс P6, дерево сессии) приёмка P6 (ФП-1)
PD-225 doc minor deploy/README.md Порядок апгрейда движка гонял migrate СТАРЫМ бинарём: шаг миграции стоял ДО установки нового, то есть был no-op, и деадлок v15 оставался; там же «ДВА факта» вместо трёх — закрыто: новый бинарь на версионированный путь ДО миграции, migrate его явным путём, TM_PLATFORM_ENGINE_BIN переключается после; окно между списком и миграцией закрыто остановкой демона на время апгрейда; три факта названы тремя fixed(дофикс P6, дерево сессии) приёмка P6 (ФП-2)
PD-226 standards minor internal/pgstore/books.go BooksForMigration Денежный гейт миграции книг не был запинен: посадка «убрать блокер незакрытого холда» пережила ВСЮ батарею — закрыто: таблица по трём блокерам, каждый по отдельности переводит вердикт книги в «нельзя», и список, который читает цикл деплой-заметки, содержит только свободную книгу; все три посадки падают. Пин pgstore.TestEachOfTheThreeBlockersAloneKeepsABookOutOfTheMigrationList fixed(дофикс P6, дерево сессии) приёмка P6 (ФП-3)
PD-227 bug minor internal/runs/reconcile.go reconcile Ветка нечитаемого маркера хоронила потолочную паузу как failed с пустой причиной: единственная из веток конца попытки, которая не перечитывала причину после дрейна, — а маркер, который не разбирается, не несёт и кода выхода, так что поток остаётся единственным свидетелем (воспроизведено приёмкой на живом PG) — закрыто: обе маркерные ветки слиты, перечит один и общий. Пин runs.TestACeilingSurvivesAMarkerThatCannotBeRead fixed(дофикс P6, дерево сессии) приёмка P6 (ФП-4)
PD-228 vuln minor (условная) internal/config/config.go Load TM_PLATFORM_INSECURE_COOKIES не отвергался рядом с боевым OIDC: гейт ловил только связку с DEV_LOGIN, а дев-рецепт экспортирует обе переменные, так что «стендовое окружение уехало в прод» ловилось наполовину — демон стартовал с боевым Google-входом, куками без Secure и без __Host-, с выключенным HSTS (воспроизведено приёмкой живым демоном) — закрыто: судит не догадка, а собственный адрес деплоя: callback OIDC и есть публичный URL платформы, поэтому http там = стенд (законно, работает), всё прочее — отказ на буте. Пин config.TestCookiesWithoutSecureAreRefusedNextToAProductionProvider fixed(дофикс P6, дерево сессии) приёмка P6 (ФП-5)
PD-229 bug minor internal/pgstore/books.go CeilingPause Незнакомый scope потолка читался как credit_exhausted и зажигал аккаунтный halted-флаг (ReadUsage ключится ровно на это значение) на аккаунте, у которого деньги есть, — при том что этот же пак завёл ceiling_unknown ровно для «потолок был, чей — не установлено» — закрыто: book и day называются, всё остальное = ceiling_unknown; резюмируемость не меняется ни при одном из трёх, а ложного «кредит кончился» больше нет. Пин — таблица pgstore.TestTheCeilingScopeDecidesWhichPauseTheRunGets fixed(дофикс P6, дерево сессии) приёмка P6 (ФП-6)
PD-230 bug info cmd/tmplatformctl/seed.go Сид не тримил --subject, а демон тримит: падённое значение одной переменной окружения давало вход под одной личностью и поиск другой — аккаунт создавался, одноразовый грант сгорал, сид обрывался (воспроизведено приёмкой) — закрыто: одна нормализация с обеих сторон. Пин TestASubjectThatIsOnlyPaddingIsNoSubject fixed(дофикс P6, дерево сессии) приёмка P6 (ФП-6)
PD-231 doc info internal/pgstore/sink.go unitDone Гард at >= обосновывался несуществующим поведением движка: «переанонс несёт СТАРОЕ время события» — по коду движка неверно, at штампует анонсирующий процесс, а непроецированная строка мёртвого прогона стирается ForgetEvents и анонсируется заново со своим временем — закрыто: гард остаётся (потребитель at-least-once обязан быть независимым от порядка доставки), довод приведён к факту здесь и в пинящем тесте fixed(дофикс P6, дерево сессии) приёмка P6 (ФП-6)
PD-232 doc info internal/runs/reconcile.go Resume Комментарий про book-ceiling утверждал «remaining ноль и резюм ничего не меняет» — по арифметике остаток положителен, потому что движок останавливается на невместившемся резервировании, а не на самом потолке — закрыто: комментарий приведён к факту, сам чурн заведён строкой PD-223 fixed(дофикс P6, дерево сессии) приёмка P6 (ФП-6)
PD-233 bug minor internal/runs/reconcile.go outcome Exit 5 со стопом, записанным ПОЗЖЕ маркера, закрывался failed: намерение снимало «прерывание», а сравнение времён отвергало «стоп» — при том что exit 5 недостижим для прогона, кончившегося сам, и два сравниваемых штампа приходят с разных часов (маркер пишет хост юнита, намерение — платформа) — закрыто: exit 5 при записанном намерении = stopped независимо от порядка; чистые концы 0/2/3 отвечены раньше в том же switch и не сдвинулись, что пинится отдельно fixed(дофикс P6, дерево сессии) приёмка P6 (ФП-6)
PD-234 doc info docs/platform-PROGRESS.md Базис «396 → 403» в шапке P6 не воспроизводился (в HEAD 359 ^func Test, откуда 396 — неизвестно) — закрыто: числа пересчитаны, а рядом со строкой написана команда счёта, чтобы следующая сессия сверяла, а не переписывала fixed(дофикс P6, дерево сессии) приёмка P6 (ФП-8)
PD-235 standards info internal/config/config_test.go Подтест-пассажир в пине нового гейта: строка «callback пуст» оставалась зелёной при УДАЛЁННОМ гейте — её отвергает проверка неполного OIDC этажом выше, то есть про сам гейт она не говорила ничего. Класс тот же, что «слепой пин» из P6: покрытие, которого нет — закрыто: строка убрана из цикла (неполный OIDC пинится своим тестом), у гейта осталось два настоящих свидетеля, оба падают на посадке fixed(дофикс P6, дерево сессии) кросс-семейное ревью дофикса P6 (security-линза)
PD-236 bug minor, контрактно видимый internal/runs/reconcile.go freshPausedReason Одна неудачная перечитка причины хоронила потолочную паузу как failed без причины. Перечит падал обратно на снапшот — а он пуст ровно в том случае, ради которого перечит и существует (событие приехало в ЭТОТ дрейн), — и вызывающие закрывали прогон на этой пустоте. На ветке испорченного маркера кода выхода нет, а сама ветка закрывает прогон по построению (PD-164), значит следующего прохода не будет: обычный перезапуск Postgres превращал резюмируемый стоп в failed. Тот же глоток заново вооружал перезапуск, который ветка «поток сказал ceiling» построена предотвращать — закрыто: ошибка чтения возвращается наверх, ничего не решается на неустановленном факте. Пин runs.TestAnEndingIsNeverDecidedFromAReadThatFailed fixed(дофикс P6, дерево сессии) кросс-семейное ревью дофикса P6 (линза автомата состояний)
PD-237 bug minor, деньги internal/runner/engine.go Status Exit 2 принимался по коду и разбору, без согласия самого отчёта. Движок отдаёт этот код из status --json при одном условии — flagged > 0, — и оно не проверялось: измерено ревью, что отчёт с flagged:0, отчёт про ЧУЖУЮ книгу и документ без book_id при exit 2 РАССЧИТЫВАЛИСЬ (2.5 USD против холда) вместо отложенного расчёта. Достижимо не движком, а тем, что стоит на запиненном пути и им не является: обёртка, подменённый на месте каталог версии, недокатанный деплой — закрыто: три условия вместе (код, разбор, согласие отчёта). Пин runner.TestExitTwoIsTakenOnlyFromAReportThatAgreesWithIt fixed(дофикс P6, дерево сессии) кросс-семейное ревью дофикса P6 (денежная линза)
PD-238 bug minor internal/ingest/manifest.go DecodeManifest Документ, который не манифест, приезжал ПУСТЫМ манифестом — а пустой манифест удаляет загрузку пользователя. {}, null и любой объект незнакомых полей декодировались в нули, интейк читал нулевые главы как «движок разрезал источник и книги в нём нет» и сносил каталог. Версия намеренно не гейтится по ЗНАЧЕНИЮ (иначе всякий релиз движка — релиз платформы), и в этом зазоре единственным сторожем деструктивного пути было имя JSON-поля — закрыто: требуется НАЛИЧИЕ manifest_version (не значение), а интейк отдельно различает «глав нет» и «глав нет, но юниты есть» — второе не вина книги. Пины ingest.TestADocumentThatIsNotAManifestIsNotAnEmptyManifest, books.TestAManifestThatContradictsItselfNeverCostsTheUpload fixed(дофикс P6, дерево сессии) кросс-семейное ревью дофикса P6 (линза шва)
PD-239 hardening info internal/runner/engine.go Manifest Интейк держался на непроверяемом инварианте чужой зоны: «manifest не может выйти 2» — верно сегодня (сентинел строится в четырёх местах render.go, ни одно не манифестное), но в движке этого не пинит ничто, а цена ошибки — вся книжная очередь хоста, потратившая бюджет попыток на документ, который движок уже напечатал — закрыто: правило читается, а не предполагается: exit 2 = команда отработала, и на интейковом канале тоже. Пин runner.TestAManifestThatCompletesWithFlagsIsStillAManifest fixed(дофикс P6, дерево сессии) кросс-семейное ревью дофикса P6 (линза шва)
PD-240 hardening info internal/runner/engine.go Status, drain Канал расчёта читал stdout без потолка, тогда как соседний Manifest кап имеет (64 МиБ) — при том что Status зовётся на каждом завершившемся прогоне. Найдено ДВУМЯ линзами независимо. Написание пина вскрыло вторую половину: дренаж, который «нельзя не делать» (иначе EPIPE), на бесконечном писателе не возвращается никогда, то есть кап был во власти того, что ограничивает — закрыто: кап 16 МиБ (замерено: настоящая книга в 2283 главы даёт 1.1 МБ), за капом процесс убивается, а не дренится; обе команды ходят через один drain. Пин runner.TestAnEndlessStatusIsRefusedRatherThanRead (посадка виснет и падает по таймауту) fixed(дофикс P6, дерево сессии) кросс-семейное ревью дофикса P6 (линзы шва и денег)

Закрытые — эра P7 (читающая поверхность контракта 0.3.0)

⚠ Из этой эры ОТКРЫТЫМИ остались PD-281 · PD-297 · PD-298 · PD-299 — их тела живут в секции «Открытые — info», куда перенесены оркестратором №18 22.08. Строка со статусом open под заголовком «Закрытые» невидима тому, кто читает открытые, а именно так этот файл и читают.

ID Класс Серьёзность Где Суть Статус Источник
PD-185 bug info internal/pgstore/sink.go, internal/runs/runs.go readyToTranslate Статус finalizing есть в контракте, в DDL и в обоих аллоулистах — писателя нет ни одного. Тот же класс, что интейк-статусы до этого пака: слово контракта без пути, который его пишет. Сегодня недостижим, поэтому вреда нет; лечится либо писателем (финальная волна движка), либо снятием из аллоулистов, когда станет ясно, что его не будет — закрыто: 0.3.0 снял значение из контракта, P7 снял его из обоих Go-аллоулистов и из обоих CHECK-констрейнтов (миграция 00016), грепом finalizing в internal/ и cmd/ пусто fixed(P7, дерево сессии) кросс-семейное ревью P5 (Fable)
PD-104 bug minor, расхождение док↔код internal/login/login.go:285-288, internal/config/config.go:73 Фри-тир начисляется АВТОМАТИЧЕСКИ, а реестр обещает обратное. Код: дефолт SignupGrantMicroUSD: 5 * 1_000_000 (config.go:73) проведён в демона (main.go:99) и логин отдаёт его в стор на каждой новой подтверждённой паре (provider, subject) — аккаунт создаётся С $5. Строка PD-30 при закрытии утверждает «аккаунт создаётся с нулём, начисление руками из админки» — один из двух текстов лжёт, и это чинится независимо от продуктового решения. Ограничитель у автогранта один — лимитер входа; агрегатного потолка, счётчика и алерта нет (грепнуто). ⚠ ЗАКРЫТО P7 словом владельца 16.08 (D39.138 п.2л): дефолт SignupGrantMicroUSD = 0 (config.go), начисление на бете — руками через tmplatformctl grant; возврат $5 идёт вместе с суточным агрегатным потолком, когда появятся платежи. Пин — config.TestSignupGrantIsParsedNotGuessed (пинит ИМЕННО ноль, чтобы восстановление дефолта мимо ратификации падало); протухшие «$5» вычищены из PLATFORM_DIRECTION.md §2 и BACKLOG.md П-7 fixed(P7, дерево сессии) приёмка P2 (панель; расхождение — оркестратор №15)
PD-172 standards minor docs/architecture/14-api-contract/openapi.yaml createBook Проводное правило POST /books в контракте не записано: часть file ОБЯЗАНА идти последней. Потоковый читатель (r.MultipartReader) отдаёт части в порядке провода, а строка книги — то, что делает загрузку видимой во время приёма и находимой, если она оборвалась, — не может быть записана до языков, которые в ней обязательны. Тем же правилом специфицирована браузерная загрузка S3 («the file or content must be the last field in the form»). Платформа отвечает 400 на форму, приславшую поля после файла, и это поведение спека не описывает. ⚠ ЗАКРЫТО: правило записано в каноне 0.3.0 (§createBook: «The file part MUST come LAST… A part sent after the file is refused, never ignored: 400, invalid_request, с errors[]»), и P7 привёл к нему код — часть после файла ПРОВАЛИВАЕТ чтение, поэтому загрузка откатывается вместе с отказом (иначе пользователь получал 400 И книгу). Пин httpapi.TestAPartAfterTheFileIsRefusedAndTheBookIsNotKept, живая проба на стенде fixed(P7, дерево сессии) сессия P5 (сверка контракта с построенным маршрутом)
PD-173 standards info docs/architecture/14-api-contract/openapi.yaml Book У отклонённой книги нет ПРИЧИНЫ на проводе. BookStatus.rejected описан как «file could not be parsed», а почему именно — не поле контракта. Платформа хранит собственный закрытый словарь причин (books.ReasonSourceUnreadable · ReasonNotConfigured · ReasonParserUnavailable, колонка books.reject_reason, миграция 00013) для оператора и НЕ проецирует его: выдумывать поле не право зоны (прецедент PD-150 — last_resync_at). Следствие для пользователя: экран покажет «не удалось разобрать» без различения «файл не тот» и «наш движок был недоступен» — а это разные советы ⚠ ЗАКРЫТО: канон 0.3.0 завёл Book.reject_reason со словарём RejectReason, P7 проецирует внутреннюю причину на него (httpapi.contractRejectReason) — с одним переименованием по эффекту: parser_unavailableprocessing_failed (ФБ-9: имя значения это то, на что клиент вешает фразу, и оно не должно называть наш компонент) fixed(P7, дерево сессии) сессия P5
PD-174 standards minor internal/httpapi/v0.go contractRoutes POST /books на инстансе без интейка отвечает 404, которого спека для этого маршрута не предусматривает. Форма унаследована от уже принятого решения зоны — непостроенный/несмонтированный маршрут остаётся охраняемым 404 (d.Runs == nil так же), и это честнее 503 на КАЖДЫЙ аплоад, который инстанс всё равно не примет. Но контрактно это тот же класс, что PD-112: код ответа, которого в перечне операции нет. ⚠ Дополнено адверсариальным ревью P5: тот же класс есть и у РЕЗЮМА — 503 на деплое без маркер-команды или без шаблона потолка, тогда как 0.2.1 ратифицировала 503 только для СТАРТА прогона ⚠ ЗАКРЫТО каноном 0.3.0: createBook объявил 404 для деплоя, который книг не принимает (intake_enabled у GET /capabilities), а 503 объявлен ответом и startRun, и resumeRun. Обе формы теперь в перечне операций, вопрос закрыт ратификацией, а не кодом fixed(P7, ратификация 0.3.0) сессия P5 (самопроверка против спеки)
PD-180 standards info docs/architecture/14-api-contract/openapi.yaml createBook Ответ 201 несёт parsing, а не uploading, и «responds immediately» недостижимо как класс. Спека описывает createBook словами «Responds immediately; the book enters uploading», но ответ по HTTP не может уйти раньше, чем прочитано тело: к моменту 201 файл уже принят, и честный статус — parsing. uploading при этом РЕАЛЬНО наблюдаем, но только параллельным чтением библиотеки (пин books.TestABookIsVisibleAsUploadingWhileItsFileIsStillArriving). Сюда же — отказы интейка, которых спека не описывает: больше 16 частей формы и поле длиннее 1 КиБ дают 400, тело сверх потолка — 413 с порогом, которого в контракте нет, а тело медленнее дедлайна маршрута отвечало обрывом соединения без problem-ответа (дофикс это исправил, см. ниже). ⚠ Дополнено дофиксом приёмки: в тот же пакет вопросов входит 408 на просроченный дедлайн загрузки — тело, не успевшее прийти за отведённое маршруту время, это медленный клиент, и 408 по RFC 9110 §15.5.9 говорит именно это, включая то, что лечение — повтор; кода 408 в перечне операции нет ⚠ ЗАКРЫТО каноном 0.3.0: «Responds immediately» снято, 201 описан как несущий parsing дословно; 408, 413 и 400-отказы интейка объявлены ответами операции, перечень помечен НЕисчерпывающим (авторитет — code ответа) fixed(P7, ратификация 0.3.0) адверсариальное ревью P5 (сверка провода со спекой)
PD-199 standards minor internal/httpapi/v0.go contractPausedReason, контракт openapi.yaml §PausedReason У платформы две причины паузы, у контракта одна — и вторая на провод не выходит. PausedReason спеки перечисляет credit_exhausted; с приходом ceiling.scope (D39.131) платформа различает потолок, который поставила САМА, и дневной потолок из book.yaml оператора, который она не выбирала и на котором у аккаунта деньги ЕСТЬ. Проецировать второй как первый нельзя — экран скажет «кончились деньги», и зажжётся аккаунт-флаг ReadUsage; выдумывать слово контракта не право зоны (прецеденты PD-173, PD-150). Поэтому внутренняя причина daily_ceiling хранится и проецируется как null, что спека сама и предписывает клиенту рендерить для незнакомой причины. ⚠ Найдено ЖИВОЙ ПРОБОЙ, а не чтением: до фикса значение уезжало на провод, и клиент, сгенерированный по спеке, его бы отверг. ⚠ ЗАКРЫТО ратификацией D39.132 п.2а («null на проводе подтверждён») — статус приведён P7; проекция осталась той же (httpapi.contractPausedReason), а Usage получил СВОЙ словарь AccountHaltReason, чтобы прогонная причина больше не могла зажечь аккаунтный флаг fixed(P7, ратификация D39.132) сессия P6 (живая проба на стенде)
PD-241 bug minor internal/runs/reconcile.go outcome Стоп, о котором попросил ПОЛЬЗОВАТЕЛЬ, переименовывается в paused/credit_exhausted, если в тот же дрейн приехал потолок: причина паузы проверяется раньше намерения стопа, и аккаунтный halted-флаг (ReadUsage ключится на credit_exhausted) зажигается на аккаунте, у которого 97% баланса на месте (воспроизведено ревью). Денег не теряется и прогон резюмируем, но /usage говорит «кредит кончился» человеку, у которого он есть. Порядок «причина раньше намерения» — ратифицированное решение P6, и молча его переворачивать этим касанием я не стал; денежно-видимая половина — это PD-203 ⚠ ЗАКРЫТО P7 (ратификация D39.132 п.2д «следующим касанием зоны» — это касание): порядок в outcome() перевёрнут — намерение стопа проверяется ПОСЛЕ собственных окончаний движка и ДО причины паузы, так что стоп пользователя приезжает stopped без причины, а аккаунтный флаг не зажигается. Пин runs.TestAStopTheUserAskedForOutranksACeilingThatArrivedWithIt (все три причины паузы + контроль: без намерения решает потолок) fixed(P7, дерево сессии) кросс-семейное ревью дофикса P6 (линза автомата состояний)
PD-223 bug minor internal/runs/reconcile.go Resume, reopen Резюм прогона, остановленного ПЛАТФОРМЕННЫМ потолком, — цикл «холд-спавн-пауза» за клик. Движок останавливается на резервировании, которое не помещается, то есть НЕ доходит до своего потолка: расчёт списывает меньше холда, у прогона остаётся положительный остаток (обычно меньше одного вызова), exhausted не наступает, и резюм открывает попытку, которая упирается в тот же потолок сразу. Денег провайдера не тратится; цена — tmctl status и транзиентный юнит на клик. Порог здесь угадывать нельзя: размер резервирования — число движка, платформе не видное ⚠ ЗАКРЫТО P7 сменой контракта: резюм прогона, остановленного ЛЮБЫМ потолком, теперь отвечает 409 run_not_resumable · cause.code: ceiling_reached (канон 0.3.0 §resumeRun), то есть цикла «холд-спавн-пауза за клик» не существует — лечение это НОВЫЙ прогон с бОльшим потолком. Пин runs.TestAResumeOfARunPausedAtALimitIsRefusedWhoseverLimitItWas fixed(P7, дерево сессии) приёмка P6 (дофикс, ФП-6)
PD-242 bug info internal/runs/reconcile.go Resume ceiling_unknown проходит гейт резюма, который отказывает daily_ceiling: дневной потолок, приехавший ТОЛЬКО кодом выхода (карантин проекции — случай, который собственный комментарий outcome называет не-редким), записывается как «чей потолок, не установлено» и резюмируется свободно, упираясь в тот же лимит того же дня. Не регресс (прежнее credit_exhausted резюмировалось так же) и тот же класс чурна, что PD-223 ⚠ ЗАКРЫТО P7 тем же ходом, что PD-223: гейт больше не различает причины — резюм отказан при ЛЮБОЙ паузе, поэтому ceiling_unknown не может его пройти fixed(P7, дерево сессии) кросс-семейное ревью дофикса P6 (линза автомата состояний)
PD-257 bug major internal/pgstore/readmodel.go writeChapters, migrations/00002:109 Пере-нарезка со сдвигом нумерации валила читающую модель НАВСЕГДА. Id главы у движка — хеш её текста и переживает ре-кат, number позиционный и сдвигается; апсерт по id ставил номер, который ещё держит соседка, и unique (book_id, number) ронял всю транзакцию SaveStructure. Вход детерминирован → дерево замирало молча, structure_version не двигался, клиенту никто не говорил пере-синхронизироваться. Достижимо апгрейдом движка/лангпака (правило заголовков роняет заголовочную главу). Delete-first НЕ лечит: сталкиваются ВЫЖИВШИЕ главы — закрыто: констрейнт стал deferrable initially deferred (00018); пины pgstore.TestARecutThatShiftsChapterNumbersIsMaterialized (вставка в начало И обратная форма) fixed(приёмка P7, дерево сессии) приёмка P7 (воркфлоу 12 осей + кросс-модель)
PD-258 bug major internal/readmodel/readmodel.go refreshStructure, pgstore.writeUnits Упавший tmctl export затирал текст книги пустотой. Дерево и пары — два вызова движка; при отказе второго все пары уходили в SaveStructure с пустыми source/target и pending, а кат не менялся ⇒ те же id ⇒ ветка do update перезаписывала переведённую книгу пустыми строками до следующего успешного экспорта — закрыто: Structure.TextRead несёт факт «канал ответил», апсерт трогает текст только при нём; пин pgstore.TestAFailedExportDoesNotEraseTheTextAlreadyMaterialized fixed(приёмка P7, дерево сессии) приёмка P7 (воркфлоу 12 осей + кросс-модель)
PD-259 bug minor internal/pgstore/idempotency.go ClaimIdempotency Две ОДНОВРЕМЕННЫЕ первые попытки давали 500 вместо 409. select ... for update по несуществующей строке не блокирует ничего, обе доходили до голого insert, проигравшая ловила 23505 — ни один из двух конфликтов контракта, наружу internal_error на том самом случае, ради которого заголовок существует — закрыто: on conflict (user_id, key) do nothing + перечитывание (идиома identities того же пакета); пин pgstore.TestTwoSimultaneousFirstClaimsAnswerInFlightAndNeverAnUnexpectedError (8 гонщиков) fixed(приёмка P7, дерево сессии) приёмка P7 (воркфлоу 12 осей + кросс-модель)
PD-260 bug minor internal/httpapi/v0.go createBook/startRun, httpapi/idempotency.go Оборвавшийся клиент получал ВТОРУЮ книгу. complete/release писали на r.Context(), который отменяет ровно тот клиент, который будет ретраить: ключ висел «в полёте» claimStale=30 мин, потом takeover переделывал работу. Рядом в зоне уже жило решение того же случая (books.writeCtx) — закрыто: settleCtx = WithoutCancel + свой дедлайн fixed(приёмка P7, дерево сессии) приёмка P7 (воркфлоу 12 осей + кросс-модель)
PD-261 bug minor internal/pgstore/migrations/00017:30, httpapi/idempotency.go path в первичном ключе неограничен: длинный адрес переполняет кортеж btree, клейм падает, и ручка отвечает 500 там, где контракт обещает 409 — закрыто: путь КЛЮЧУЕТСЯ дайджестом (path_sha256), сам остаётся читаемой колонкой (00018); пин TestAnOverlongPathDoesNotBreakTheClaim. ⚠ Область ключа — (principal, method, path): «the same key on another operation is another key», пин TestOneKeyOnAnotherOperationIsAnotherKey fixed(приёмка P7, дерево сессии) приёмка P7 (воркфлоу 12 осей + кросс-модель)
PD-263 bug minor internal/pgstore/runs.go StartRun, ReleaseBankStop Полоса прогона и её база считали РАЗНЫЕ проходы. chapters_before снимался по edit-колонке, а первый сегмент прогона со стопом на подпись считает по draft-колонке ⇒ новый прогон над уже начерновленной книгой открывался на полном потолке — ровно тот дефект, ради которого колонку и заводили. Симметрично: при снятии стопа числитель переключается на edit, а база оставалась draft'овой — закрыто: база снимается тем же предикатом, что числитель, и ПЕРЕ-снимается в ReleaseBankStop; пин pgstore.TestTheBarAndItsBaselineCountTheSamePass fixed(приёмка P7, дерево сессии) приёмка P7 (воркфлоу 12 осей + кросс-модель)
PD-264 bug minor internal/pgstore/readmodel.go writeChapters После пере-нарезки глава наследовала прогресс и замечания чужой главы. unit_resolutions адресованы движковыми (номер главы, ординал), которые новый кат пере-указывает, и не чистились — закрыто: ре-кат удаляет резолюции ушедшего ката (переводить их не по чему); пин pgstore.TestARecutDoesNotLetAChapterInheritTheProgressOfTheOneBeforeIt fixed(приёмка P7, дерево сессии) приёмка P7 (воркфлоу 12 осей + кросс-модель)
PD-265 bug minor internal/httpapi/stream.go pump Кадры, подрезанные буфером при ЖИВОМ соединении, пропадали молча — без resync_required, хотя note доставляется однажды и молчаливый пропуск = замечание потеряно навсегда — закрыто: state.Oldest > from+1 в цикле даёт тот же ответ, что и на рукопожатии; пин httpapi.TestFramesPrunedWhileTheConnectionIsOpenAskTheClientToResync (без него мутация проходила батарею) fixed(приёмка P7, дерево сессии) приёмка P7 (воркфлоу 12 осей + кросс-модель)
PD-266 bug minor internal/httpapi/stream.go stream.write На маршруте потока не было дедлайна записи: клиент, открывший поток и переставший читать, держал горутину, соединение и слот сервера вечно — тихий уход не отменяет r.Context()закрыто: SetWriteDeadline на каждый кадр через ResponseController; пин httpapi.TestEveryFrameIsWrittenUnderADeadline fixed(приёмка P7, дерево сессии) приёмка P7 (воркфлоу 12 осей + кросс-модель)
PD-267 bug info internal/httpapi/stream.go streamEvents Last-Event-ID больше позиции книги принимался и отвечал 204. Такого id сервер не выдавал (реальный триггер — восстановление БД из бэкапа двигает event_position назад), и 204 велит клиенту прекратить переподключение по числу, которое нельзя проверить — закрыто: id, с которого сервер продолжить не может, отвечается resync_required — так канон и требует («rather than silently starting from now»); ветка 204 для «at or past на книге в покое» сохранена, она тоже каноническая. Пин httpapi.TestAnEventIDTheServerCannotResumeFromIsToldToResync fixed(приёмка P7, дерево сессии) приёмка P7 (воркфлоу 12 осей + кросс-модель)
PD-268 bug minor internal/runs/reconcile.go finish, Sweep Материализация читающей поверхности голодала свип. refreshReadModel брал 5 минут (WithoutCancel) ВНУТРИ пер-прогонного бюджета в 60 с, то есть уходил из-под withBudget и держал расчёт денег всех прогонов позади себя — ровно та голодовка, ради которой withBudget и заведён (PD-169) — закрыто: работа копится (deferRefresh) и сливается ОТДЕЛЬНЫМ проходом демона (DrainRefresh, pass("readmodel", …)). fixed(приёмка P7, дерево сессии) приёмка P7 (воркфлоу 12 осей + кросс-модель)
PD-269 bug minor internal/runner/artifacts.go projectDB Банк не читался на штатном деплое. Требовался ключ project_db, который движок делает НЕОБЯЗАТЕЛЬНЫМ (дефолт <book_id>.db, backend/internal/config/book.go:163), а канонический шаблон оператора его не содержит — закрыто: тот же дефолт, что у движка; пин runner.TestTheProjectDatabaseDefaultsTheWayTheEngineDefaultsIt fixed(приёмка P7, дерево сессии) приёмка P7 (воркфлоу 12 осей + кросс-модель)
PD-270 bug info internal/httpapi/problem.go WriteProblem Problem без Code уходил как 500 БЕЗ обязательного code. Канон требует код на каждом ответе /v0, 500 включая; omitempty существует ради поверхности, которую канон не описывает, и протекал обратно. Живого вызова не было — латентная ловушка, бьющая по принципу самого файла («расходящаяся пара непредставима») — закрыто: пустой код → internal_error; пин httpapi.TestAProblemWithNoCodeStillAnswersOne fixed(приёмка P7, дерево сессии) приёмка P7 (воркфлоу 12 осей + кросс-модель)
PD-271 bug info internal/httpapi/conditional.go acceptsGzip, writeJSON Договорённость о кодировании решалась по ПЕРВОМУ токену (*, gzip;q=0 читалось как согласие) и HEAD не получал ни ETag, ни 304, хотя маршрутизатор отдаёт ему тот же обработчик (Go 1.22+) — закрыто: именованное кодирование выигрывает у * в любом порядке; валидатор отвечает и на HEAD; пины TestTheNamedCodingOutranksTheWildcardInEitherOrder, TestHeadCarriesTheSameValidatorAsGet fixed(приёмка P7, дерево сессии) приёмка P7 (воркфлоу 12 осей + кросс-модель)
PD-272 bug info internal/pgstore/readmodel.go scope.tag, SubmitBankDecisions Курсор не был привязан к КНИГЕ (только к версии структуры) и принимался на другой книге; решения банка брали блокировки строк в порядке клиентского массива, и два пересекающихся сабмита взаимно блокировались — закрыто: книга входит в тег области; решения применяются в порядке term_id, отказ по-прежнему называет клиентский индекс; пин pgstore.TestACursorMintedForOneBookIsRefusedOnAnother fixed(приёмка P7, дерево сессии) приёмка P7 (воркфлоу 12 осей + кросс-модель)
PD-273 standards info internal/runs/reconcile.go outcome, контракт §Run ЗАКРЫТ решением владельца 17.08: статус остаётся awaiting_bank, лечение — в КОНТРАКТЕ. Разбор: нажать «стоп» на прогоне, УЖЕ стоящем в awaiting_bank, нельзя (RequestStop требует finished_at is null) ⇒ случай ровно один — гонка: стоп запрошен во время перевода, а движок в этом окне домайнил банк и вышел кодом 3. Оба состояния продолжаемы, но ярлык несёт ПРОВОДКУ: ReleaseBankStop вызывается только при l.Status == "awaiting_bank", поэтому переименование в stopped оставило бы bank_released = false, вернуло бы --verify-bank на resume и пере-открыло бы PD-277. Плюс awaiting_bank информативнее: работа стоит на самой дешёвой точке — черновик и майнинг оплачены, редакторская волна не начата. Настоящая дыра — ПРОВОД: Run не умеет сказать «остановлено по вашей просьбе» (status + stop_for_signing = опция запуска, а не состояние), поэтому честную фразу клиент нарисовать не может. Строка бэклога для оркестратора — archive/P7_ACCEPTANCE_HANDOFF_2026-08-17.md §7(и) closed (решение владельца 17.08) приёмка P7
PD-274 doc major deploy/README.md:107=TM_PLATFORM_LANGUAGE_PAIRS, docs/PLATFORM_DIRECTION.md:68 Деплой-нота включала обратно грант, который PD-104 выключил. В копируемом блоке окружения стояло TM_PLATFORM_SIGNUP_GRANT_USD=5, то есть развёртывание по ноте давало каждой новой личности безлимитный самообслуживаемый грант; в PLATFORM_DIRECTION.md §2 «Дефолт $5» пережил правку первого абзаца. Плюс TM_PLATFORM_LANGUAGE_PAIRS не назван вовсе, а пустой список делает /capabilities и интейк противоположными — закрыто: строка снята с объяснением, пары внесены, оба текста актуализированы fixed(приёмка P7, дерево сессии) приёмка P7 (воркфлоу 12 осей + кросс-модель)
PD-275 standards minor internal/pgstore/readmodel_test.go, httpapi/reading_test.go, httpapi/problem_test.go Три пина не проверяли того, что объявляли. TestAMissingBankReadOutSavesNothing падал на шаг раньше (нет book.yaml) и в ветку ErrNoBank не входил; TestALimitIsClampedOrIgnoredButNeverRefused не мог наблюдать подрезку (фейк выбрасывал limit); title(code) == "" недостижимо ни для одного кода. Плюс подсистема Idempotency-Key целиком без тестов — закрыто: три пина переписаны на наблюдаемое свойство, подсистема ключей закрыта семью тестами, карта замечаний (ingest/notes.go) — тремя fixed(приёмка P7, дерево сессии) приёмка P7 (воркфлоу 12 осей + кросс-модель)
PD-276 bug info internal/books/parse.go Parse Материализация на границе интейка шла на контексте задачи, уже потраченном разбором: на большой книге Refresh (ещё два процесса движка) не укладывался, и дерево оставалось пустым, а свип этого не повторяет — закрыто: свой отвязанный бюджет, как на границе прогона. Бэкстоп-свип для книг с пустым деревом был построен доработкой 20.08 и СНЯТ актом 5 (⚠ books.materializeMissingTrees, pgstore.BooksWithNoTree и пин TestABookWhoseTreeNeverLandedIsMaterializedByTheSweep в дереве больше не существуют) вместе с механизмом, который его заменил: долг на материализацию стал колонкой (PD-302), и он покрывает не только «дерева нет вовсе», но и «дерево на границу старше». Свойство закрыто, доказательство переехало: books.TestAParsedBookOwesAReadingSurfaceUntilOneIsMaterialized (плюс проверка, что метка ставится ТОЙ ЖЕ транзакцией, что и конец разбора) fixed(приёмка P7 + доработка 20.08, дерево сессии) приёмка P7 (воркфлоу 12 осей + кросс-модель)
PD-277 bug major internal/runs/spawn.go, шов с движком Экран подписи банка не был подключён к движку: resume перезапускал движок с тем же --verify-bank, и тот немедленно вставал в тот же стоп — подпись не двигала ничего. Отчёт пака помечал пункт «исполнен», сверив НЕ ту ветку движка (if !r.VerifyBank, по которой resume не идёт). ⚠ Нового канала в движок не требовалось: строка единого бэклога 191(в) (слово владельца 16.08, D39.144) уже требовала «проводку resume = снятие стопа до движка», а движок без флага уже делает модель владельца — неподписанное едет авто-строками с пометкой ⟨проверить⟩ (backend/internal/pipeline/mining.go:211-231, D39.42 п.3) — закрыто: на resume флаг не передаётся (l.VerifyBank && !l.BankReleased, bank_released протянут в LiveRun); пин runs.TestAResumedRunIsSpawnedWithoutTheSigningStop. ⚠ ПЕРЕ-ДИСПОЗИЦИЯ 22.08: прежний остаток строки («честность decline»: отклонённый термин уезжал в банк авто-строкой) снят вместе с глаголом — decline больше нет ни в одной ручке зоны (PD-370). Живой остаток другой и он НЕ здесь: ратифицированная модель оставляет пер-термной ПРАВКУ («поправить/добавить термин»), ручки под неё нет ни в зоне, ни в каноне, и это строка 199(а) единого бэклога плюс контрактная половина PD-370. fixed(приёмка P7, дерево сессии) приёмка P7
PD-278 bug minor internal/pgstore/readmodel.go SaveStructure Первая материализация книги трактовалась как ПЕРЕ-нарезка и стирала работу оплаченного прогона. storedKey = '' ≠ ключу манифеста ⇒ ветка ре-ката удаляла ВСЕ unit_resolutions книги. Достижимо штатно: интейк-материализация упала (PD-276), пользователь прогнал книгу, и первый успешный SaveStructure стирал резолюции завершённого прогона — карточка 0 глав, все замечания потеряны, восстановление только новым прогоном за деньги. Регрессия правки PD-264 — закрыто: удаление при storedKey != "" && ключ сменился; пин pgstore.TestTheFirstMaterializationDoesNotDiscardAFinishedRunsWork fixed(приёмка правок P7, дерево сессии) приёмка правок P7 (9 осей + fable-5)
PD-279 bug minor internal/pgstore/migrations/00018_recut_and_key_scope.sql Down Down-путь 00018 не исполнялся на тех данных, которые Up впервые легально принимает: строка с путём длиннее ~2704 байт не влезает в восстанавливаемый старый первичный ключ, и goose DownTo(17) падал детерминированно — то есть откат релиза ломался ровно в аварии, ради которой откат существует. Регрессия правки PD-261 — закрыто: Down снимает такие строки перед пересборкой ключа (таблица — кэш с ретенцией 24 ч); проверено живым PG в цикле UP → данные → DOWN → UP fixed(приёмка правок P7, дерево сессии) приёмка правок P7 (9 осей + fable-5)
PD-280 standards info internal/httpapi/idempotency.go, v0.go intakeFingerprint HTTP-половина Idempotency-Key не имела тестов, и именно там жили три дефекта: что попадает в отпечаток, какая область у клейма и на каком контексте ключ закрывается — из pgstore это не наблюдаемо (там опаковый дайджест) — закрыто: три пина (TestTheSameUploadReFramedIsStillTheSameRequest, TestTheClaimCarriesTheRequestsOwnOperation, TestAnOverlongKeyIsRefusedBeforeAnythingIsClaimed) fixed(приёмка правок P7, дерево сессии) приёмка правок P7 (9 осей + fable-5)
PD-283 bug minor internal/httpapi/stream.go pump Подрезка буфера ДО чтения кадров пропускалась молча: сверка state.Oldest > from+1 делалась ПОСЛЕ того, как водяной знак перепрыгнул через дыру, поэтому Oldest оказывался ПОЗАДИ него и условие не срабатывало никогда — то есть правка PD-265 не закрывала собственный сценарий, а кадры note, которые канон запрещает терять, терялись. Батарея не видела: единственный пин стоял на подрезке МЕЖДУ чтениями — закрыто: дыра опознаётся по голове пачки (frames[0].Position > from+1; позиции непрерывны — единственный писатель emitFrame), кадры этой пачки не отправляются; пин TestAHoleAtTheHeadOfTheBatchAsksTheClientToResync fixed(доработка 20.08, дерево сессии) доработка 20.08 (сверка находок против дерева)
PD-284 bug minor internal/pgstore/idempotency.go CompleteIdempotency Квитанция писалась в строку, которая этой попытке уже не принадлежит. where не различал ни живую строку от исчезнувшей, ни свой клейм от перехваченного: воспроизведено живым PG — попытка, у которой клейм забрали по протуханию, дописывала свой ответ, и клиент получал квитанцию ЧУЖОГО запроса (status=201 location=/v0/books/bk_old). Тег команды при этом выбрасывался в _, поэтому «квитанция не записана» было ненаблюдаемо — закрыто: and finished_at is null + отдельная ErrClaimLost; окно перехвата у ЖИВОЙ попытки закрыто загрузочной проверкой TM_PLATFORM_UPLOAD_DEADLINE < pgstore.ClaimStale (та же форма, что уже стояла для UploadGrace); пин TestAnUploadDeadlineLongerThanEitherWindowIsRefusedЭРРАТА (акт 5): закрытие было ПРЕЖДЕВРЕМЕННЫМ. and finished_at is null закрывает только «загрузка дольше окна» и ничего не говорит про ВЛАДЕЛЬЦА клейма: строка перехвачена, а finished_at у преемника законно пуст. Целиком закрыто токеном клейма — PD-300 fixed(доработка 20.08, дерево сессии) доработка 20.08 (сверка находок против дерева)
PD-285 standards minor internal/runs/reconcile_test.go, internal/httpapi/idempotency_test.go Два пина утверждали свойство, которого у кода нет. TestAResumedRunIsSpawnedWithoutTheSigningStop гонял фикстуру, стартующую прогон БЕЗ stop_for_signing, поэтому подмена l.VerifyBank && !l.BankReleasedl.VerifyBank его не роняла — пин PD-277 был пустым. TestTheSameUploadReFramedIsStillTheSameRequest объявлял «тот же файл в другой оболочке — тот же запрос», тогда как боевой вызов кладёт в отпечаток r.ContentLength, который оболочку считает — закрыто: первый проходит весь путь (первая попытка обязана НЕСТИ флаг, вторая — нет), второй переименован и утверждает то, что код делает, с честной границей PD-262 fixed(доработка 20.08, дерево сессии) доработка 20.08 (сверка находок против дерева)
PD-286 bug info internal/httpapi/stream.go write, streamEvents Ошибка Flush выбрасывалась, а на маленьком кадре это единственный вызов, который видит залипший сокет: Fprintf пишет в bufio и возвращает nil, поэтому поток рапортовал «кадр ушёл» о кадре, который не ушёл, и залипший клиент получал writeTimeout на каждый heartbeat вместо одного. Плюс HEAD на маршрут потока проходил в насос и держал горутину до конца прогона (RFC 9110 §9.3.2: HEAD отдаёт те же заголовки и не тело) — закрыто: ошибка Flush убивает поток, HEAD отвечает заголовками; пины TestAFrameThatCouldNotBeFlushedEndsTheStream, TestHeadOnTheStreamAnswersTheHeadersAndNothingElse fixed(доработка 20.08, дерево сессии) доработка 20.08 (сверка находок против дерева)
PD-287 bug minor internal/runs/runs.go Bounds, Start blocked называл чужую книгу всякий раз, когда у аккаунта есть открытый холд, независимо от того, укорачивает ли он шкалу, — а канон §RunOptions обещает, что поле объясняет ИМЕННО укорачивание («why the scale is smaller than the account could otherwise afford»). Пользователь книги из трёх глав видел «другая книга держит кредит» и шёл останавливать прогон впустую. Симметрично CreditHeldError в Start срабатывал на ЛЮБОМ выходе за шкалу, включая выход по размеру книги. Bounds не имел ни одного теста — закрыто: CreditHeldBy возвращает и СУММУ чужих холдов, поле заполняется только если возврат этой суммы удлинил бы шкалу (в Start — только если без неё запрос бы прошёл); пин TestBlockedNamesAnotherBookOnlyWhenItsHoldShortensTheScale fixed(доработка 20.08, дерево сессии) доработка 20.08 (сверка находок против дерева)
PD-288 bug info internal/pgstore/readmodel.go noteCount vs ListNotes Book.note_count и GET /notes описывали разные множества: счётчик считал все флагнутые резолюции книги без джойна, список — только те, чья глава существует (INNER JOIN, иначе chapter_id нечем заполнить). Карточка обещала N замечаний, список отдавал меньше с next_cursor: null. Достижимо штатно, пока дерево не материализовано (PD-276) — закрыто: счётчик считается тем же джойном fixed(доработка 20.08, дерево сессии) доработка 20.08 (сверка находок против дерева)
PD-289 bug info internal/pgstore/sink.go unitDone Кадры строились из ОТВЕРГНУТОЙ доставки: апсерт резолюции отбрасывает событие старше сохранённого (where excluded.at >= unit_resolutions.at), но тег команды не проверялся — счётчики главы пересчитывались и emitChapter слал кадр note, собранный из полей устаревшего события, под тем же id замечания. Кадр хранится wire-ready и реплеится дословно, поэтому расхождение пережило бы правку — закрыто: RowsAffected() == 0 ⇒ ничего не изменилось, ничего не объявляется fixed(доработка 20.08, дерево сессии) доработка 20.08 (сверка находок против дерева)
PD-290 bug info internal/pgstore/credits.go inTxinReadTx, гейты ListNotes/ListBank Гейт дельта-чтения и строки, которыми он управляет, читались в РАЗНЫХ снапшотах: READ COMMITTED берёт снапшот на каждый стейтмент, поэтому замена банка/дерева между проверкой version_too_old и запросом строк не отвергалась и не отражалась — короткий список, который выглядит полным (класс PD-163, объявленный закрытым «одной транзакцией») — закрыто: пути чтения открываются repeatable read + read only (одна функция inReadTx; read-only повторяемое чтение не может прерваться, в отличие от serializable) fixed(доработка 20.08, дерево сессии) доработка 20.08 (сверка находок против дерева)
PD-291 bug info internal/readmodel/readmodel.go, internal/pgstore/readmodel.go writeUnits Юнит, который есть в манифесте и выпал из экспорта (кат сдвинулся между двумя вызовами движка), перезаписывал сохранённый текст пустым pending: флаг «текст прочитан» был на всей структуре, а не на паре. Правка PD-266 закрывала только полный отказ экспорта — закрыто: флаг переехал на ПАРУ (StructureUnit.TextKnown), структурный снят как избыточный; пин TestAPairTheExportDidNotCarryDoesNotSpeakAboutItsText fixed(доработка 20.08, дерево сессии) доработка 20.08 (сверка находок против дерева)
PD-292 standards info internal/pgstore/runs.go RestartRun, internal/runs/reconcile.go Resume Снятие стопа банка было ОТДЕЛЬНЫМ писателем после переоткрытия прогона: отказ между двумя записями оставлял возобновлённый прогон с bank_released = false — весь второй сегмент бар считал draft-колонку против купленного потолка, и канала починки не было (свип это поле не трогает, повторный resume отказал бы: прогон уже translating) — закрыто: снятие уехало ВНУТРЬ транзакции RestartRun (LiftBankStop), метод ReleaseBankStop снят, пин переписан на реальный путь fixed(доработка 20.08, дерево сессии) доработка 20.08 (сверка находок против дерева)
PD-293 bug info internal/runs/reconcile.go DrainRefresh Проход материализации не мог быть подрезан своим бюджетом: DrainRefresh не смотрел ctx между книгами, а каждая книга берёт отвязанные 5 минут, поэтому K медленных книг переезжали объявленные 10 минут прохода и задерживали интейк, телеметрию и СЛЕДУЮЩИЙ такт свипа — то есть голодание PD-169 переехало уровнем выше — закрыто: проверка между книгами, недоделанное возвращается в очередь (deferRefresh дедуплицирует по книге) ⚠ ЭРРАТА (акт 5): механизм заменён, свойство сохранено. DrainRefresh/deferRefresh удалены вместе с очередью в памяти; проверка бюджета между книгами живёт в readmodel.Drain, а «недоделанное возвращается» стало долговечным долгом с арендой (PD-302, PD-320, PD-322) ⚠ ПАК P8-REVIEW 24.08: живой носитель, который эррата этой строки прямо называет (проверка бюджета между книгами в readmodel.Drain), не покрыт НИ ОДНИМ тестом — во всём дереве нет теста, который вообще даёт Drain дедлайн, поэтому гейт можно выключить целиком и батарея останется зелёной; точный близнец у интейка при этом запинен своим тестом. Вынесено строкой PD-388; статус паком не менялся ⚠ ОСПОРЕНО(PD-388) fixed(доработка 20.08, дерево сессии) доработка 20.08 (сверка находок против дерева)
PD-294 standards info internal/pgstore/readmodel.go SubmitBankDecisions Транзакция решений банка не брала блокировку книги первой — против глобального порядка зоны (credits.go lockBook: books → runs → run_attempts → account_balances → reservations). Она берёт блокировки bank_decisions и, через внешний ключ, bank_terms, которые SaveBank держит, уже взяв книгу; ретрая нет, IsTransient сюда не подключён, поэтому взаимоблокировка ушла бы клиенту как 500 — закрыто: lockBook в начале транзакции fixed(доработка 20.08, дерево сессии) доработка 20.08 (сверка находок против дерева)
PD-295 bug info internal/pgstore/migrations/00016_read_surface.sql Индекс unit_resolutions_book_idx (00015) стал строгим префиксом-дубликатом индекса, который добавляет 00016, и не снимался: второй индекс на самом горячем пути записи (строка на юнит на волну) — закрыто: drop index в Up, воссоздание в Down; заодно снято утверждение о «пине 18.4», которого нет в ратифицированной таблице стека (пол — 16). Перефингерпринт 00016 объяснён в шапке migrations.sha256 fixed(доработка 20.08, дерево сессии) доработка 20.08 (сверка находок против дерева)
PD-296 standards info internal/httpapi/v0.go contractSurface, internal/httpapi/problem.go codes Два места, которые надо править парой, правились по одному. Маршруты регистрировались дюжиной вызовов mux.Handle, а тест «каждый контрактный маршрут требует сессии» ходил по литеральному списку из четырёх — семь новых маршрутов пака не покрывал никто. Коды ошибок несли статус и заголовок в двух отдельных switch, а тест — в третьем, рукописном: новая константа не покрывалась ничем — закрыто: маршруты и коды стали по ОДНОЙ таблице, тесты ходят по ней; счётчик кодов пинит закрытый словарь 0.3.0 в 16 значений fixed(доработка 20.08, дерево сессии) доработка 20.08 (сверка находок против дерева)

Закрытые — акт 5 P7 (ревью акта 4 + ответ контрактной сессии, 20.08)

Находки адверсариального ревью собственного диффа акта 4 (9 линз × 2 рефутера) и работа, которую принёс ответ контрактной сессии на записку зоны. Каждая строка закрыта посадкой мутации: код испорчен названным образом, пин обязан упасть, код возвращён.

ID Класс Серьёзность Где Суть Статус Источник
PD-202 bug info internal/pgstore/readmodel.go finishedUnits, sink.go unitDone chapters.units_done не двигался вовсе у пайплайна без волны редактора. Колонка считала ТОЛЬКО волну edit, поэтому на деплое без редактора ни одна глава не была «сделана» никогда: полоса книги стояла на нуле, а ChaptersLeft не подрезался — сервис бесконечно предлагал купить уже переведённые главы. ⚠ Это ДЕНЬГИ, а не полоса. Закрыто формой, которую задала контрактная сессия: «сделано» = ПОСЛЕДНИЙ проход, который книга на ЭТОМ деплое реально получает, и форму движок объявляет сам — у пайплайна без редактора знаменатель волны edit равен нулю (beginWaves), значит runs.edit_total = 0 при ненулевом draft_total и есть «редактора нет». Читают одно выражение все пять мест (chaptersDone, runProgress, emitChapter, ReadBookForRun, bookScope.wave); до первого отчёта прогона форма НЕИЗВЕСТНА и ответ остаётся волной edit — неизвестное не должно читаться как «сделано». Колонка-дубль units_done снесена (00021, PD-314). Пин: pgstore.TestADraftOnlyDeploymentStillClampsTheScale fixed(акт 5) вопрос зоны P7 → ответ контрактной сессии 20.08 (E-G)
PD-253 standards info internal/pgstore/readmodel.go ListUnits, канон §listUnits 410 Gone отвечал и на главу, которой никогда не было. Расхождение снято НЕ кодом: контрактная сессия приняла довод зоны — различать «была и больше нет» от «не было никогда» невыполнимо без надгробий, которых у платформы нет и заводить которые дороже вопроса, — и изменила канон в нашу сторону. Расхождение перестало быть расхождением fixed(канон 0.4.0) приёмка P7 (акт 1) → ответ контрактной сессии 20.08 (L)
PD-262 bug minor internal/httpapi/v0.go intakeFingerprint, internal/httpapi/idempotency.go Тождество интейка решалось по ОБЪЯВЛЕННОМУ, и объявленное его не несёт. Content-Length стоял вместо размера файла и не закрывал ни одной половины: он считает и multipart-обвязку (повтор из другой клиентской библиотеки — «другой запрос»), а chunked-тело не объявляет ничего вовсе — две РАЗНЫЕ книги под одним ключом с одинаковым объявлением были неразличимы, и вторая получала Location первой. Закрыто нормой 0.4.0 («метаданные + имя файла + СОДЕРЖИМОЕ»): Content-Length из отпечатка убран, файл дайджестится на лету (io.TeeReader), дайджест хранится с квитанцией (00023), а повтор ЧИТАЕТСЯ и сверяется до того, как ему что-то ответят — совпал, реплей; не совпал, 409 key_reused; не прочитан — не реплей. Пины: httpapi.TestTheFingerprintIsExactlyTheDeclaredParts, httpapi.TestTheClaimsFourAnswersReachTheClient/another_FILE…; живая проба 20.08: тот же файл → 201 с тем же id, другой файл → 409 key_reused, книг в библиотеке одна fixed(акт 5) приёмка P7 (акт 1) → норма 0.4.0 (E)
PD-282 standards minor internal/runs/reconcile.go Resume, канон §resumeRun resume прогона, потратившего весь купленный потолок, отвечал 202 и НИЧЕГО не менял — молчаливый no-op, который тот же раздел канона запрещает в соседней строке. Контрактная сессия ратифицировала обратное обещание: 202 означает, что работа реально переоткрыта, и такой вызов — 409 run_not_resumable, cause: ceiling_reached, в ЛЮБОМ статусе. Закрыто вместе с разведением двух exhausted (PD-312). Пин: runs.TestResumeOfARunWithNothingLeftIsRefusedWithTheCeilingReached fixed(акт 5) триаж акта 4 → ответ контрактной сессии 20.08 (A)
PD-300 bug minor internal/pgstore/idempotency.go, internal/httpapi/idempotency.go Ключ идемпотентности не знал своего владельца, и обе половины стоили пользователю книги. Строка несёт (user, method, path, key) и не несёт, КАКАЯ попытка её держит: (1) попытка, застрявшая дольше ClaimStale, наконец падала и её defer release УДАЛЯЛ живую строку преемника, который уже создавал книгу — третья попытка получала свежий клейм и делала работу второй раз; (2) у попытки забрали клейм, а она дописывала СВОЙ ответ, и клиент реплеил Location книги, которой у него нет. and finished_at is null не сужает ни одну: строка преемника законно не завершена. Обе воспроизведены ревьюером на живом PG. Закрыто токеном клейма (00020): ClaimIdempotency минтит его и ПЕРЕ-минтит при перехвате, complete и release его предъявляют, несовпадение → ErrClaimLost. ⚠ Сравнивать вместо токена claimed_at нельзя — Go даёт наносекунды, Postgres хранит микросекунды. Пины: pgstore.TestAnAttemptThatLostItsClaimCanNeitherAnswerForItNorTakeItAway, httpapi.TestEveryEndingPresentsTheClaimItWasGranted fixed(акт 5) ревью акта 4 (2 линзы, воспроизведено на живом PG)
PD-301 bug major internal/pgstore/events.go ReadStream, internal/httpapi/stream.go Поток говорил end раньше, чем появлялось дерево — то есть ровно Ф-56, ради которой он и строился. books.Parse коммитит FinishParse (parsing → not_started) и только ПОТОМ зовёт материализацию — два процесса движка на собственном бюджете. В этом окне «в покое» было истинно, насос слал end, а автоматический реконнект браузера получал 204 «не переподключайся»: клиент переставал смотреть ровно тогда, когда главы вот-вот появятся. Та же дыра на границе ПРОГОНА: прогон закрыт, текст ещё не материализован, готовый ОПЛАЧЕННЫЙ перевод до читателя не доезжает. Закрыто долгом (PD-302): «в покое» теперь означает «и материализация не должна». Пин: pgstore.TestABookThatOwesAReadingSurfaceIsNotAtRest; живая проба 20.08: при непогашенном долге поток НЕ шлёт end, реконнект отвечает 200 вместо 204 fixed(акт 5) ревью акта 4 (замерено на живом PG)
PD-302 bug minor internal/runs/reconcile.go (deferRefresh/DrainRefresh), internal/books/parse.go Долг на материализацию жил только в памяти процесса, и терялся двумя достижимыми путями: Refresh вернул ошибку — ветка логировала и НИЧЕГО не ставила обратно; демон перезапустился — слайс умер с процессом. В обоих случаях дерево от прошлой границы ЕСТЬ, поэтому единственный бэкстоп («дерева нет вовсе») книгу не видел, и текст оплаченного прогона не доезжал до читателя никогда. Закрыто ДОЛГОВЕЧНЫМ долгом: колонка books.read_model_owed_at (00019) ставится в транзакциях FinishParse и FinishRun, гасится по РАВЕНСТВУ метки (долг, поставленный позже, переживает материализацию), очередь берётся из БД. Механизмов стало на два меньше: runs.pendingRefresh/DrainRefresh/deferRefresh/Reader и books.materializeMissingTrees/BooksWithNoTree удалены, дрейн переехал в readmodel.Drain — пакет, чья это работа. Пины: runs.TestAFinishedRunLeavesItsBookOwingAReadingSurface, books.TestAParsedBookOwesAReadingSurfaceUntilOneIsMaterialized, pgstore.TestADebtStampedDuringAMaterializationSurvivesIt, readmodel.TestTheDrainMaterializesEveryOwedBookAndKeepsWhatFailed fixed(акт 5) ревью акта 4 (2 линзы)
PD-303 bug minor internal/readmodel/readmodel.go refreshStructure Упавшее чтение пар отчитывалось УСПЕХОМ: отказ Export только логировался, refreshStructure возвращал nil, и интейк считал материализацию состоявшейся — книга получала полное дерево пустых пар, а читатель не видел даже собственного исходника. Закрыто возвратом отказа вызывающему (errors.Join): неполная материализация НЕ гасит долг, и книга остаётся в очереди дрейна, пока один проход не ответит по всем трём каналам. Пин: readmodel.TestAPartialMaterializationDoesNotDischargeTheDebt fixed(акт 5) ревью акта 4
PD-304 bug minor internal/pgstore/books.go ReadUsage Usage зажигал остановку АККАУНТА от потолка ОДНОГО прогона. Предикат сканировал paused_reason последнего прогона каждой книги, а credit_exhausted там значит «этот прогон потратил своё». Замерено: счёт $10, прогон на 10 глав ($0.30) — провод отвечал {"state":"ok","remaining_percent":97,…,"halt_reason":"credit_exhausted"}, то есть пользователю с деньгами говорили, что денег нет, рядом с процентом, говорящим обратное. Канон предупреждает об этом дословно (§AccountHaltReason). Закрыто чтением остановки С АККАУНТА: нечего тратить — остановлен, и ничем иным. Пины: pgstore.TestUsageIsAShareAndAnAccountWithNoGrantsIsExhausted, pgstore.TestAPauseTheReconcilerCausedIsVisibleOnTheRun; живая проба 20.08: $10 → halt_reason: null fixed(акт 5) ревью акта 4 (замерено на проводе)
PD-305 standards minor httpapi.TestTheClaimCarriesTheRequestsOwnOperation, pgstore.TestAnOverlongPathDoesNotBreakTheClaim, pgstore.TestASecondRunsBarStartsAtZeroOverAHalfFinishedBook, config.TestAnUploadDeadlineLongerThanEitherWindowIsRefused, httpapi.TestEveryCodeNamesExactlyOneStatus Пять пинов проходили под мутацией, которую сами называют — каждый проверен ревьюером исполнением. Причины разные и все поучительные: единственный гоняемый маршрут имел путь, совпадающий с паттерном · 4000 повторяющихся байт СЖИМАЮТСЯ в индексном кортеже и до предела btree не доходят · фикстура заканчивала ровно одну главу и покупала ровно одну, поэтому кламп не связывал · оба значения цикла нарушали ВТОРУЮ проверку, поэтому снятие первой ничего не меняло · statusOf(c) это буквально codes[c].status, то есть сравнение таблицы с собой (регрессия акта 4: слияние статуса и заголовка сделало пин тавтологией). Закрыты по-разному, но каждый — так, чтобы названная мутация падала: новый пин на маршрут, где путь ≠ паттерн · несжимаемый путь (падает index row size 4040 exceeds btree maximum 2704) · новый пин, где прогон покупает МЕНЬШЕ, чем книга успевает закончить · одна проверка против ТЕСНЕЙШЕГО окна вместо двух · сверка с ТРАНСКРИПЦИЕЙ канона §ErrorCode, а не с той же таблицей fixed(акт 5) ревью акта 4 (все пять проверены исполнением)
PD-306 bug minor internal/pgstore/readmodel.go noteCount note_count стоил 83% времени GET /v0/books — регрессия акта 4. Джойн на chapters добавили, чтобы счётчик и список замечаний описывали одно множество (PD-288), и решение не было замерено. Замер зоны на корпусе 40 книг × 500 глав (по 1000 замечаний, после vacuum analyze): 16.8 мс на страницу против 3.4 мс со счётчиком, заменённым литералом, из них 9 мс — сам джойн. Стоимость неустранима по форме: счёт идёт по КАЖДОМУ замечанию каждой книги страницы. Закрыто счётчиком на главе (00022, возвращает колонку, снесённую 00016 — вместе с писателем, которого ей не хватало): фолд unitDone и пере-расчёт writeChapters ведут его в тех же операторах, что и волновые счётчики, из тех же строк, а согласие счётчика со СПИСКОМ становится конструктивным — замечание, у главы которого нет строки, не имеет ни адреса на проводе, ни счётчика. Замер после: 6.6 мс на страницу. Пины: pgstore.TestTheCardsNoteCountAndTheNotesListDescribeOneSet, pgstore.TestAFlaggedUnitMovesTheChaptersNoteCounter, бенчмарк pgstore.BenchmarkLibraryPage fixed(акт 5) ревью акта 4 (замерено)
PD-307 bug minor internal/pgstore/credits.go CreditHeldBy blocked называл СТАРЕЙШИЙ холд, а не тот, что укоротил шкалу — регрессия акта 4. Решение «укорачивает ли» стало приниматься по СУММЕ чужих холдов (верно), а книга по-прежнему выбиралась order by opened_at limit 1. Замерено: $10, на книге A держится $0.03 (открыт первым), на книге B — $9.60; blocked называл A, и пользователь отменял прогон, который ничего не освободит. Закрыто выбором книги с НАИБОЛЬШЕЙ суммой холдов (group by book_id order by sum(...) desc, min(opened_at)) — ничьи решаются старшинством, чтобы ответ был устойчив между двумя чтениями. Пин: pgstore.TestTheHoldThatIsNamedIsTheOneThatWouldFreeTheMost fixed(акт 5) ревью акта 4 (замерено)
PD-308 standards minor internal/httpapi/v0.go startRun startRun отвечал 413 — кодом, которого канон у этой операции не объявляет (400/401/403/404/409/503), а §ErrorCode определяет payload_too_large как «свыше intake_max_bytes», то есть про ЗАГРУЗКУ. Соседняя JSON-запись на то же условие отвечала 400, значит одна из двух врала о том, что клиент сделал не так. Закрыто приведением к 400 invalid_request. Пин: httpapi.TestAnOverlongRunRequestIsInvalidAndNotTooLarge; живая проба 20.08 fixed(акт 5) ревью акта 4 (2 линзы)
PD-309 standards minor internal/httpapi/stream.go pump Границы дыры в потоке не были запинены на ОДИН кадр. emitFrame минтит по кадру за раз, поэтому обычная дыра у живого соединения шириной ровно в один кадр, а пин PD-283 гоняет дыру в 189 кадров — обе границы отвечают на такую одинаково, и сдвиг любой из них на единицу батарея не замечала. Закрыто табличным пином на четыре случая по обеим границам. Пин: httpapi.TestTheGapBoundariesAreExactAtOneFrame fixed(акт 5) ревью акта 4
PD-310 doc info internal/pgstore/runs.go, internal/pgstore/books.go, internal/httpapi/problem.go, internal/books/render.go Проход по болтливости обрубил четыре комментария на середине — осиротевшая строка без подлежащего у EngineStreamID, непарная скобка и склеенное надвое предложение в WriteStatusProblem, удвоенная клауза в render.go, док-блок StuckIntake, приклеенный к чужой функции. Тот же дефект был в акте 3. Закрыто починкой всех пяти; правило записано там же, где ошиблись: при таком проходе читать РЕЗУЛЬТАТ целиком, а не только удаляемое fixed(акт 5) ревью акта 4 (2 линзы)
PD-311 hardening minor internal/config/config.go loadIntake Загрузочная граница дедлайна оставляла секунду. Проверка сравнивала дедлайн с окном и не оставляла ничего на работу ПОСЛЕ чтения тела: принимался 29m59s, а терминальные записи интейка идут на собственный бюджет и квитанция ключа пишется после них — клейм становился перехватываемым за мгновение до того, как его завершили. Плюс проверок было ДВЕ, и та, что слабее, не могла сработать никогда (ClaimStale теснее UploadGrace), а пин её «покрывал» вторым значением, которое ловила соседняя. Закрыто одной проверкой против ТЕСНЕЙШЕГО окна с явным запасом books.UploadSettle. Пин: config.TestAnUploadDeadlineIsRefusedUnlessTheWholeUploadFitsTheTighterWindow fixed(акт 5) ревью акта 4
PD-312 bug minor internal/runs/reconcile.go reopen, internal/httpapi/v0.go Два РАЗНЫХ факта возвращались одним вердиктом exhausted: «прогон потратил свой потолок» и «счёт не тянет холд». Лечения у них противоположные — новый прогон против пополнения, — а ответ был один, поэтому пользователя с непотраченными главами отправляли покупать прогон, который ему не нужен. Разведено на ceilingSpent и creditUnavailable; второму контрактная сессия завела cause.code: credit_unavailable (второй уровень открыт, типов не двигает). Реконсилятор по-прежнему паузит оба одинаково — это не конфляция, а его работа: состояние, в котором владелец может действовать. Пин: runs.TestResumeOverAnEmptyAccountSaysTheCreditIsUnavailableAndNotTheCeiling (включая «пополнил — тот же вызов продолжает тот же прогон») fixed(акт 5) вопрос владельца 20.08 → норма 0.4.0 (A)
PD-313 standards minor internal/pgstore/books.go Run, internal/httpapi/project.go Клик пользователя «стоп» не доезжал до клиента. Столбец runs.stop_requested_at был, на проводе его не было, и никакой status его не заменяет: стоп не мгновенен, поэтому стоп, попросленный во время перевода, встречается с собственной остановкой прогона на подписи банка — прогон приезжает awaiting_bank, а этот статус предлагает ПРОДОЛЖИТЬ на клик, который значил «останови». Закрыто ОБЯЗАТЕЛЬНЫМ булевым Run.stop_requested (0.4.0, единственная ломающая правка), снимается при resume вместе с намерением. Побочно: три места читали Run тремя одинаковыми списками из четырнадцати колонок — сведены в runRow+scanRun. Пины: pgstore.TestTheStopIntentTravelsOnEveryRunAndAResumeClearsIt, httpapi (обязательность поля); живая проба 20.08 fixed(акт 5) норма 0.4.0 (J)
PD-314 standards info internal/pgstore/migrations/00021_drop_units_done.sql chapters.units_done был байт-в-байт дублем units_edit_done и писался двумя местами. После PD-202 читателей у него не осталось вовсе, а имя, читающееся как «сделано», над значением «отредактировано» — ровно та ловушка, из которой вырос PD-202. Колонка снесена вместе с обоими писателями fixed(акт 5) уборка зоны при PD-202

Закрытые — самопроверка акта 5 (адверсариальное ревью собственного диффа, 20.08)

Ревью зоны против СОБСТВЕННОЙ работы акта 5: 9 линз × 2 рефутера + критик полноты, 84 агента, 0 ошибок. 37 находок, 18 пережили хотя бы одного рефутера, и часть из них — дефекты, которые внесли правки самого акта 5. Три из восемнадцати воспроизведены агентами исполнением на живом Postgres, две — посадкой мутации в дерево. Отдельно ценно то, что ревью поймало КЛАСС, ради которого этот пак его и гоняет: два пина, написанные в акте 5, проходили под мутацией, которую сами называют.

ID Класс Серьёзность Где Суть Статус Источник
PD-315 bug major internal/pgstore/sink.go FinishUnspawnedStop, internal/pgstore/runs.go PauseRun Транзакций, которые ЗАКАНЧИВАЮТ прогон, три, а долг на материализацию ставила одна. Собственная посылка акта («обе границы ставят его в транзакции, которая границу закрывает») оказалась ложной: пауза на потолке и стоп, приехавший на попытку без юнита, оставляли книгу finished_at-нутой и НЕ должной. Оба случая с текстом: прогон, вставший на потолке, перевёл всё до него; стоп на попытке, у которой сняли клейм спавна, ловит движок, который реконсилятор сам же и рассчитывает (спенд-базлайн там держится намеренно). Читателя нет — очередь дрейна ключуется этой колонкой, и на закрытую книгу больше не смотрит никто. Закрыто фрагментом owesAReadingSurface, который несут все три, и пином по ТАБЛИЦЕ из трёх концовок. Пин: pgstore.TestEveryEndingOfARunLeavesTheBookOwingASurface fixed(акт 5, самопроверка) ревью акта 5, линза «долг» (2/2 рефутера не опровергли)
PD-316 bug major internal/pgstore/readmodel.go finishedUnits Форма пайплайна читалась с ПОСЛЕДНЕГО прогона, а последний — самый НОВЫЙ, и только что допущенный не объявил ничего. На деплое без редактора chapters_done каждой книги падал в ноль в момент создания прогона и поднимался обратно на первом progress-событии — счётчик, которому канон запрещает ходить назад, и вместе с ним шкала покупки, которая в этот момент снова предлагает уже переведённое. Закрыто переносом факта на КНИГУ (миграция 00024, books.edit_wave): пишется объявлением движка через оба канала (поток и resync), монотонно — книга, прошедшая редактирующий пайплайн, остаётся такой. Пины: pgstore.TestAdmittingARunDoesNotWalkTheBooksProgressBackwards, pgstore.TestTheWaveShapeIsWrittenByTheEngineAndOnlyGrows fixed(акт 5, самопроверка) ревью акта 5, линза «волны» (2/2)
PD-317 bug major internal/pgstore/runs.go StartRun, RestartRun Базлайн полосы и её числитель считали РАЗНЫЕ проходы. Числитель научился про деплой без редактора, а chapters_before в обоих местах по-прежнему называл units_edit_done — второй прогон над уже начерновленной книгой открывался на своём потолке, и снятие стопа подписи открывало второй сегмент там же. Воспроизведено ревьюером исполнением. Закрыто одним выражением на оба места. ⚠ Первый написанный на это пин ПРОШЁЛ под своей мутацией — фикстура останавливалась на шаг раньше того места, где перекос виден. Пины: pgstore.TestASecondRunOnADraftOnlyDeploymentStillOpensAtZero, pgstore.TestLiftingTheStopOnADraftOnlyDeploymentRetakesTheRightBaseline fixed(акт 5, самопроверка) ревью акта 5, линзы «волны» и «доки»
PD-318 standards major internal/books/books_test.go, internal/runs/reconcile_test.go Два пина акта 5 проходили под мутацией, которую сами называют. Оба утверждали КОНЕЧНОЕ состояние («книга должна поверхность»), а названная мутация — вынос метки из транзакции границы во второй оператор — конечное состояние не меняет: меняется окно, в котором книга разобрана и не должна ничего, а колонка — единственный ретрай. Ревьюер посадил обе мутации и обе прошли всю батарею. Закрыто утверждением про АТОМАРНОСТЬ: xmin (транзакция, последней писавшая строку) у книги и у кадра, который та же транзакция выпустила, обязан совпадать. ⚠ Первая редакция этого пина в прогонах сравнивала книгу с самим ПРОГОНОМ и падала на зелёном дереве: расчёт штампует прогон ещё раз мгновением позже fixed(акт 5, самопроверка) ревью акта 5, линза «пины» (исполнением, 2/2)
PD-319 bug major internal/pgstore/idempotency.go ClaimIdempotency Клейм, проигравший гонку ОСВОБОЖДЕНИЮ, отвечал 500 — на том самом случае, ради которого заголовок существует. Две попытки приходят вместе; победитель вставки падает быстро (любой 4xx возвращает ключ немедленно), и пере-чтение проигравшего на свежем READ COMMITTED снапшоте не находит ничего. Это тот же 500, который двумя строками выше закрывал on conflict do nothing. Воспроизведено ревьюером на живом PG (6 из 12 гонщиков). Закрыто повтором клейма с начала: пропавшая строка — не ответ никому, ключ просто снова свободен. Пин: pgstore.TestAClaimThatLostARaceToAReleaseIsRetriedAndNotAnError fixed(акт 5, самопроверка) ревью акта 5, линза «идемпотентность» (2/2)
PD-320 bug minor internal/pgstore/books.go BooksOwedReadModel, internal/books/parse.go Дрейн получал книгу, которую интейк в этот момент материализует. Долг становится виден в момент коммита разбора, а его собственный плательщик только входит в чтение движка (до 5 минут); свип идёт каждые 15 секунд в том же процессе и клейма у списка не было. Любая книга, материализующаяся дольше интервала свипа, читалась движком ДВАЖДЫ одновременно — ровно та цена, которую этот же пак экономил RefreshCut-ом (PD-248). Закрыто арендой: ClaimReadModelDebt отодвигает срок долга, очередь берёт только то, что <= now(), интейк клеймит свой долг перед материализацией. Пины: pgstore.TestAClaimedDebtLeavesTheQueueUntilItsWindowLapses, readmodel.TestTheDrainSkipsABookSomebodyElseIsAlreadyMaterializing, books.TestTheIntakeClaimsItsOwnDebtBeforeMaterializing fixed(акт 5, самопроверка) ревью акта 5, линза «SQL»
PD-321 bug minor internal/readmodel/readmodel.go refresh Погашение долга шло на том же контексте, что и чтения движка, которые его исчерпали. На большой книге чтения законно съедают весь бюджет, и запись «сделано» после них не доезжает — материализация СЛУЧИЛАСЬ и не записана, поэтому следующий проход делает две полных ре-нарезки заново, и так каждый проход. Закрыто отвязанным коротким бюджетом — то же правило, по которому живут терминальные записи интейка. Пин: readmodel.TestTheDischargeSurvivesAContextTheReadsUsedUp fixed(акт 5, самопроверка) ревью акта 5, зонд линзы «долг»
PD-322 bug minor internal/readmodel/readmodel.go Drain Книга, про которую движок не может ответить никогда, держала голову очереди вечно. Очередь берётся старейшими долгами, поэтому четырёх таких книг (каталог, который оператор перенёс; проектная база, которую эта сборка не читает) хватало, чтобы всё, что за ними, не получило оплаченный текст вовсе. Закрыто переносом неоплаченного долга в КОНЕЦ очереди — с той же сверкой метки, что и погашение. Пины: pgstore.TestADeferredDebtGoesToTheBackAndNeverOverwritesANewerOne, readmodel.TestTheDrainMaterializesEveryOwedBookAndKeepsWhatFailed fixed(акт 5, самопроверка) собственная сверка зоны при чтении своего же дрейна
PD-323 standards minor internal/httpapi/v0.go createBook Повтор под завершённым ключом ИГНОРИРОВАЛ часть, присланную после файла, и отвечал 201, тогда как то же тело под свежим ключом — 400 missing_or_late. Одни и те же байты были законны или нет в зависимости от того, какой ключ на них надет, и это противоречило собственному док-комментарию маршрута. Закрыто проверкой хвоста и на пути реплея. Пин: httpapi.TestTheClaimsFourAnswersReachTheClient/another_FILE…; живая проба 20.08 fixed(акт 5, самопроверка) ревью акта 5, линза «идемпотентность»
PD-324 bug minor internal/pgstore/books.go ReadUsage Доля считалась только по строкам kind = 'grant', а на бете грант при регистрации НОЛЬ и оператор пополняет счёт через adjust. Замерено на проводе: счёт с $20, с которого можно стартовать прогоны, отвечал {"state":"low","remaining_percent":0} — та же «у вас нет денег» пользователю с деньгами, что и PD-304, с другого конца. Знаменатель стал «всё, что когда-либо ДОБАВИЛИ» (гранты и положительные корректировки); отрицательная корректировка остаётся на стороне трат. Пин: pgstore.TestTheShareCountsEveryWayCreditWasAdded; живая проба 20.08 fixed(акт 5, самопроверка) ревью акта 5, критик полноты (взаимодействие двух правок, которого не видела ни одна линза)
PD-325 doc major deploy/README.md §дев-стенд Загрузочный гейт пар, который завёл этот же акт, отвергал собственный рецепт стенда: копипаст-блок «Рецепт целиком» ставит BOOKS_DIR и ENGINE_BIN, но не TM_PLATFORM_LANGUAGE_PAIRS — демон не поднимался вовсе, и следующий шаг рецепта (tmplatformctl seed) бил в мёртвый порт. Боевой блок в том же файле пары получил, стендовый — нет. Это рецепт, которым поднимается ФРОНТ. Закрыто fixed(акт 5, самопроверка) ревью акта 5, критик полноты
PD-326 doc info internal/pgstore/events.go, internal/books/books.go, internal/httpapi/v0.go, internal/runs/reconcile.go Четыре комментария, лгущих о коде под ними — тот же класс, что PD-310, найденный тем же ревью в собственных правках акта: nothingIsRunning описывал материализатор как читающий его «по отрицанию» (полярность одна и та же, и инженер, действующий по комментарию, отправил бы дрейн читать движок ровно во время живого прогона) · BooksOwedReadModel утверждал, что чтение движка берёт проект эксклюзивно (ревьюер проверил ДВИЖОК: NewReadOnlyRunner открывает БЕЗ flock, намеренно) · canTranslate нёс две первых строки док-комментария · блок проекций объявлял себя «канон 0.3.0, поле в поле», уже содержа stop_requested, которого в 0.3.0 нет · ветка стопа банка всё ещё обещала, что резюм ждёт ПОЛНОГО набора решений — гейт, снятый D39.144 и удалённый из кода этим же паком fixed(акт 5, самопроверка) ревью акта 5, линзы «доки» и «SQL»
PD-327 standards minor internal/httpapi/idempotency.go, internal/httpapi/v0.go intakeFingerprint Тождество интейка теперь решается СОДЕРЖИМЫМ файла, а ратифицированный 0.3.0 сравнение по байтам запрещает («compared over the DECLARED parts — the metadata and the file's name and size — and never over the bytes themselves»). Новое поведение — это норма 0.4.0 (§E ответа контрактной сессии) и безопасная сторона: правило 0.3.0 и есть то, что позволяло двум разным книгам делить один ключ (PD-262). Строка заведена не как дефект, а как зависимость от ратификации: до лендинга 0.4.0 оркестратором зона отвечает 409 key_reused там, где действующий канон обещает реплей. Закрывается лендингом канона ⚠ ЗАКРЫТО P8-FIX: условие закрытия наступило — канон 0.4.0 РАТИФИЦИРОВАН (D39.152), а константа, которой деплой объявляет версию клиенту, поднята 0.3.0 → 0.4.0 (internal/httpapi/capabilities.go). Чтобы это не повторилось, поднятие ГЕЙЧЕНО: gates.TestTheAnnouncedContractVersionIsTheOneTheCanonRatified читает info.version из САМОГО канона, а не из копии числа — прежние тесты сверяли провод с константой и потому проходили при любом её значении (посадка «вернуть 0.3.0» гейтом ловится) fixed(P8-FIX) ревью акта 5, линза «провод»

Закрытые — эра P8-FIX (фикс-лист приёмки P7, 2122.08)

Пак фикс-листа приёмки P7 (пинг оркестратора №18) плюс врезанный первым блокер выката, который в паке P7 не рождался и прожил два пака помеченным закрытым.

ID Класс Серьёзность Где Суть Статус Источник
PD-169 bug BLOCKER cmd/tmplatformd/runner.go свип, internal/runs/reconcile.go, internal/pgstore/runs.go ЭТА СТРОКА СОЛГАЛА ДВУМ ПРИЁМКАМ. Она стояла fixed(P5), а её пин (TestOneSlowRunDoesNotEatThePassOfTheWholeSweep) гоняет Sweep ВООБЩЕ БЕЗ дедлайна прохода — то есть доказывает пер-прогонный бюджет и по построению не может увидеть проход. Живое свойство: sweepBudget 2 мин на проход против defaultRunBudget 60 с на прогон = двух медленных прогонов хватает, чтобы съесть проход целиком, после чего ctx.Err() обрывает цикл и UnsettledRuns — единственный ретрай отложенного расчёта — не вызывается ВООБЩЕ. Не для этих двух книг: для ВСЕЙ инсталляции, каждый проход, потому что order by r.started_at ставит заклиненный прогон (по построению самый старый) в голову списка детерминированно. Цена: холды чужих аккаунтов не возвращаются, а книга с незакрытым прогоном не пускает новый (runs_one_live_per_book) — деньги заморожены, книга заморожена, пользователю видно «идёт». Ручки не было: runs.Config.RunBudget объявлен и НИКЕМ не присваивался, переменной окружения нет ни для одного из двух чисел, tmplatformctl не умеет ни закрыть прогон, ни вернуть холд — наблюдаемость (tm_platform_sweep_unfinished_total) росла, а сделать было нельзя ничего — ЗАКРЫТО P8-FIX четырьмя механизмами, а не подъёмом константы: (1) проход РАЗДЕЛЁН на фазы, реконсиляция получает половину и не может съесть расчёт (runs.phaseBudget); (2) прогон, ВЫБРАВШИЙ свой бюджет или упавший, получает отсрочку с растущим бэкоффом (run_attempts.reconcile_after, миграция 00025) и перестаёт держать голову списка — свип читает RunsToReconcile, а не ListLiveRuns; (3) после runs.StalledAfter неудач подряд прогон считается ЗАСТРЯВШИМ: гейдж tm_platform_runs_stalled и tmplatformctl runs --stalled называют строку, число неудач, последнюю ошибку и что она держит; (4) терминальный вердикт — ОПЕРАТОРА: tmplatformctl run abandon --run <id> [--release-hold], который отказывается закрывать прогон, чья попытка ещё называет юнит. Оба числа стали ручками (TM_PLATFORM_SWEEP_BUDGET, TM_PLATFORM_RUN_BUDGET — второе присваивается впервые). Пины: TestTheSettlementPhaseIsReachedWhenTheRunPhaseSpendsThePass (проход с НАСТОЯЩИМ дедлайном и ДВУМЯ зависшими прогонами — та самая арифметика) · TestARunTheSweepCannotFinishStopsHoldingTheHeadOfTheList · TestARunThatKeepsFailingBecomesTheOperatorsProblem · TestAbandoningAStalledRunIsRefusedOverALiveUnitAndAlwaysGivesTheHoldBack. Прежний пин P5 НЕ удалён: он доказывает другое, настоящее свойство fixed(P8-FIX) второй рубеж приёмки P7 (пинг №18, врезан первым)
PD-328 bug minor internal/httpapi/capabilities.go, internal/httpapi/stream.go Деплой объявлял клиенту версию контракта, которой не отдаёт: ContractVersion = 0.3.0 при формах 0.4.0 и ратифицированном каноне (D39.152). Пока мажор 0, различие МИНОРА несёт ломающие правки по замыслу, и клиент, сгенерированный под объявленную версию, ОТКАЗЫВАЕТСЯ работать. Практического вреда не было (фронт заморожен на 0.2.3), но это ровно тот класс, что живёт до первого выката. Тесты, которые «покрывали» константу, сверяли ПРОВОД С НЕЙ ЖЕ — self-consistent, проходят при любом значении (класс PD-1) — починено подъёмом до 0.4.0 плюс гейт против САМОГО канона (internal/gates/contract_test.go читает info.version из docs/architecture/14-api-contract/openapi.yaml); отсутствие канона = падение, а не скип fixed(P8-FIX) фикс-лист приёмки, п. 1
PD-329 hardening minor internal/pgstore/events.go emitFrame Два поля, которые канон требует на КАЖДОМ кадре (EventBase), не были запинены ничем: снятие revision и structure_version проходило ВСЮ батарею (воспроизведено посадкой обеих мутаций 21.08 — 18 пакетов, exit 0). Асимметрия и есть причина: поля штампуются в ОДНОМ месте, поэтому каждый кадр получает их даром, и батарея, читающая кадры ради их собственных payload'ов, от этого не выигрывает ничего. Цена на проводе: клиент применяет кадр, только если его ревизия не ниже удерживаемой, — кадр без ревизии либо отбрасывается всяким читателем, либо применяется вне порядка — закрыто pgstore.TestEveryFrameCarriesTheBooksRevisionAndStructureVersion, написанным над БУФЕРОМ, а не над одним эмиттером: кадр нового вида покрыт в день, когда его заводят. Обе мутации ловятся fixed(P8-FIX) фикс-лист приёмки, п. 2 (дыра найдена посадкой оркестратора)
PD-330 bug minor internal/readmodel/readmodel.go Drain, internal/pgstore/books.go У долга материализации не было ни предела попыток, ни канала «признать безнадёжным»: claim → fail → DeferReadModelDebt пишет now() → свип через 15 с → вечно. «Конец очереди» был очередью из одного. Три следствия, каждое непрерывное: до 5 минут движковых процессов на проход навсегда · поток такой книги НЕ заканчивается НИКОГДА (AtRest требует read_model_owed_at is null) — браузер переподключается к ней вечно · при живом манифесте и падающем экспорте SaveStructure коммитится каждый проход ⇒ revision++ и кадр каждые 15 с о том, что ничего не изменилось. У интейка предел был всегда (parse_attempts=5) — закрыто бэкоффом с потолком плюс списанием после readmodel.maxAttempts (то же число 5 и по той же причине: один вопрос — один ответ). Сдаться здесь БЕЗОПАСНО в отличие от прогона: деньги той границы уже рассчитаны, теряется свежесть текста; и это не окончательно — следующая граница работы ставит свежий долг и обнуляет бюджет (owesAReadingSurface). Оператору: гейдж tm_platform_reading_surfaces_abandoned, tmplatformctl books --abandoned, tmplatformctl book refresh --book <id>. Миграция 00025 fixed(P8-FIX) фикс-лист приёмки, п. 3
PD-331 bug info internal/runs/runs.go:62-65, cmd/tmplatformd/runner.go Ручка, которая существовала в структуре, была задокументирована и ничего не делала: runs.Config.RunBudget объявлен с доккоммментом про голодание, а startRunner его НЕ присваивал — единственным ограничителем одного прогона внутри прохода был пакетный дефолт, и ни один деплой не мог его изменить. Класс «мёртвое поле, читающееся как механизм» — присвоено, плюс TM_PLATFORM_RUN_BUDGET и TM_PLATFORM_SWEEP_BUDGET (оба печатаются на буте с источником, PD-114) fixed(P8-FIX) фикс-лист приёмки, врезка про блокер свипа
PD-332 bug minor internal/readmodel/readmodel.go refreshStructure, internal/ingest/manifest.go У материализатора не было ПОЛА на пустой манифест — латентная потеря всего текста книги. in.Chapters строился только из manifest.Chapters и ни разу не сверялся со счётчиками того же документа, лежащими рядом; пустой список едет в SaveStructure, где delete from chapters where book_id = $1 and not (id = any($2)) на пустом массиве истинен для ВСЕХ глав, каскад сносит текст, а при пустом Key уходят ещё и все unit_resolutions. Сегодняшним движком недостижимо, и именно поэтому опасно молча: ловится не сломанный движок, а документ, который ЭТА сборка прочла неверно (переименованный ключ декодируется аллоулистом в пустой список). Асимметрия и была находкой: на ИНТЕЙКЕ ровно этот случай отловлен и прибит мутацией с P6 — там «нет глав» это вердикт, УДАЛЯЮЩИЙ загрузку — закрыто ingest.Manifest.Whole(), зеркалом собственного правила движка (BookManifest.selfConsistent, backend/internal/pipeline/manifest.go:460), плюс отказ на нулевом счёте глав; оба входа (Refresh и RefreshCut интейка) под одним полом. Пины — readmodel.TestAManifestThatDoesNotDescribeItselfNeverReachesTheTree (четыре формы) и TestTheIntakesOwnCutIsHeldToTheSameFloor fixed(P8-FIX) фикс-лист приёмки, п. 10
PD-333 hardening minor internal/pgstore/credits.go inReadTx Изоляция читающих чтений не была запинена ничем: снятие RepeatableRead+ReadOnly проходит ВСЮ батарею (воспроизведено посадкой 21.08), при том что под инвариантом лежат шесть ручек выдачи, а носитель прямо называет цену — «every frame in that window was lost for good» (PD-163): READ COMMITTED берёт снапшот НА ОПЕРАТОР, страница и ревизия, которой она подписана, приезжают из двух миров, и клиент, соблюдающий контрактное «отбрасывай чтение с меньшей ревизией», теряет всё, что материализовалось между ними — закрыто двумя пинами, потому что свойств два и падают они независимо: TestAReadingTransactionIsOneSnapshotAndRefusesToWrite проверяет ПОВЕДЕНИЕ против Postgres (чужой коммит невидим второму чтению; запись внутри = SQLSTATE 25006), а TestEveryPagedReadingHandleTakesTheReadingTransaction — что шесть ручек через него и ходят (ручка, переведённая на inTx, в тесте без конкурентного писателя читает столь же правильно). Четыре посадки, четыре поймано fixed(P8-FIX) аудит 21.08, п. 12 фикс-листа
PD-334 bug minor, деньги internal/pgstore/credits.go CreditHeldBy Оговорка and book_id <> $2 не исполнялась ни одним тестом: пин, который выглядит покрывающим, в своей фикстуре даёт исключаемой книге НОЛЬ холдов, поэтому мутант, снимающий оговорку, проходит батарею (воспроизведено 21.08). Сумма кормит контрактный blocked, то есть ответ на вопрос «почему шкала короче, чем аккаунт может себе позволить»: с собственным холдом книги в сумме экран приглашает пользователя погасить прогон ЭТОЙ ЖЕ книги ради ЭТОЙ ЖЕ книги — а холд уже вычтен из баланса, так что совет не может оказаться верным даже случайно — закрыто TestABooksOwnHoldIsNotWhatIsHoldingItDown с фикстурой, где у спрашиваемой книги холд САМЫЙ БОЛЬШОЙ, поэтому забывчивый ответ называет её же fixed(P8-FIX) аудит 21.08, п. 13 фикс-листа
PD-335 doc info internal/books/books.go canTranslate Задвоенная первая строка доккомментария («reports whether this deployment declares the pair» + «judges an upload against what this deployment declared») — класс PD-310/326, след прохода по болтливости. Снята одна из двух, вторая отделена пустой строкой от абзаца, который к ней и относится ⚠ Якорь фикс-листа указывал на pgstore/books.go:234-235; в дереве это internal/books/books.go:234-235 — файлов с этим именем два fixed(P8-FIX) фикс-лист приёмки, п. 7
PD-336 doc info internal/pgstore/perf_test.go, internal/pgstore/migrations/00022_chapter_note_count.sql Один замер рассказан четырьмя носителями тремя разными числами, и ни одно не несло УСЛОВИЙ: «83%» (журнал, регистр PD-306), «96% of the page» (perf_test.go), «16.8 против 3.4, джойн сам 9 мс» (миграция 00022). Числа честны и меряют РАЗНОЕ: 96% — холодный, невакуумированный корпус акта 4 (636 мс против 24), 8083% — вакуумированный корпус 40×500 — сведено указанием условий при каждом числе; нормативным для вакуумированных цифр остаётся миграция 00022, которая их и несёт вместе со своими условиями, остальные на неё указывают. Удалять «96%» как фантом НЕЛЬЗЯ — оно выводится из своего замера. ⚠ Расхождение с буквой пинга названо: пинг просил свести условия В миграцию 00022, но она РЕЛИЗНАЯ и гейт неизменности (migrations.sha256) её правку запрещает; правка гейта ради этого была бы подгонкой под зелень. Поэтому условия дописаны в носители, которые править законно fixed(P8-FIX) фикс-лист приёмки, п. 9
PD-337 bug info internal/runs/reconcile.go phaseBudget Дефект СОБСТВЕННОЙ правки этого пака, найденный её же пином: доля фазы считалась абсолютным дедлайном от s.now() — инъектируемых часов сервиса, — тогда как контекст истекает по РЕАЛЬНОМУ времени. Фаза получала не половину прохода, а разницу между двумя часами: в пине это дало проход вдвое длиннее заказанного, на деплое с замороженных часов не бывает — но пин, замерявший длительность, поймал это до лендинга. Урок записан там же, где ошиблись fixed(P8-FIX) самопроверка P8-FIX
PD-338 bug info internal/pgstore/sqlgate_test.go Первая редакция гейта SQL проверяла не тот SQL: сбор именованных констант спускался ast.Inspect внутрь тел функций, а q — имя дюжины разных операторов в пакете, поэтому запрос мог быть просверен против ЧУЖОГО текста и пройти. Тот же класс, ради которого гейт и написан (PD-169: проверка, доказывающая свойство слабее объявленного). Поймано первым же прогоном самого гейта; область собрана из ТОП-УРОВНЕВЫХ деклараций плюс локальных констант функции fixed(P8-FIX) самопроверка P8-FIX
PD-339 bug minor internal/runs/reconcile.go reconcileOne Дефект собственной правки, менявший смысл всего механизма: отсрочка ключевалась на ОШИБКЕ реконсиляции, а самый частый клин — зависший tmctl status живого прогона — ошибки НЕ возвращает: maybeResync её глотает намеренно и правильно («a status call that fails is not a run that failed»). То есть прогон, выедающий бюджет молча, остался бы в голове списка навсегда — ровно случай, ради которого механизм строится. Найдено пином, не чтением. Условие теперь «ошибка ИЛИ выбран собственный бюджет», причём ctx.Err() == nil отделяет «прогон потратил своё» от «проход кончился», чтобы прогон не наказывался за чужую занятость fixed(P8-FIX) самопроверка P8-FIX
PD-340 bug major internal/pgstore/books.go truncateReason, internal/pgstore/runs.go DeferRun Дефект СОБСТВЕННОГО кода пака, и самоподрывной: обрезка причины отказа шла ПОБАЙТОВО. В эти колонки попадает первая строка stderr ДВИЖКА дословно и без ограничения длины (runner.firstLine), а продукт переводит с китайского и японского — сообщение, цитирующее текст книги, длинное и многобайтовое по природе. Срез через середину руны даёт невалидный UTF-8, Postgres такой text отвергает (SQLSTATE 22021) — и отказывает та самая запись, которая фиксирует неудачу: срок не сдвинут, счётчик не вырос, элемент снова в голове очереди, навсегда. То есть механизм пака воссоздавал бы ровно то голодание, которое чинит, своей же бухгалтерией. Плюс сырой stderr вообще не обязан быть валидным UTF-8 — поэтому починка это strings.ToValidUTF8 ПЕРЕД срезом и срез по границе руны. Пин — TestAnEnginesOwnErrorTextSurvivesBeingRecorded, и он утверждает не про строку, а про то, что Postgres её принимает: правило чужое, и тест, меряющий только строку, прошёл бы при любой кодировке. Обе мутации ловятся fixed(P8-FIX) самопроверка P8-FIX, ревьюер контракт-конформности
PD-341 bug info internal/pgstore/books.go AbandonReadModelDebt Два числа об одном факте: books --abandoned печатал на единицу меньше, чем лог рядом. Списание не инкрементило read_model_attempts, а строка лога берёт Attempts+1 — оператор видел «4 попытки» под сообщением «attempts=5». Попытка, исчерпавшая бюджет, — тоже попытка; счётчик теперь считает её. Пин — TestAWrittenOffDebtLetsTheStreamEndAndTheNextBoundaryBringsItBack (сверяет ПЯТЬ) fixed(P8-FIX) самопроверка P8-FIX
PD-342 hardening minor internal/pgstore/runs.go AbandonRun Контрактно видимый исход операторского вердикта не был закреплён ничем: мутант, кладущий status='stopped', failure_reason=null, проходил всю батарею. Канон требует, чтобы статус книги был статусом её текущего или последнего прогона («a client holding both never has to decide which wins»), так что расхождение — не косметика, а два ответа без правила выбора. Закрыто ассертом внутри TestAbandoningAStalledRunIsRefusedOverALiveUnitAndAlwaysGivesTheHoldBack: сверяются и статус прогона, и причина, и равенство статусу книги fixed(P8-FIX) ревьюер контракт-конформности
PD-343 hardening minor internal/httpapi/stream.go base Вторая половина EventBase не была закреплена ничем — и это ДРУГОЕ место, чем то, что чинил пункт 2 фикс-листа. Поля штампуются двумя независимыми писателями: исторические кадры — pgstore.emitFrame (запинен PD-329), СВЯЗНЫЕ (hello, resync_required, end) — httpapi.base. У второго покрытие было тоньше: единственный тест смотрел у hello только contract и structure_version, а end и resync_required не смотрел никто. Канон требует оба поля на КАЖДОМ кадре. Закрыто TestEveryConnectionFrameCarriesTheBooksRevisionAndStructureVersion по двум формам связи; посадка «base без revision» ловится fixed(P8-FIX) ревьюер контракт-конформности
PD-344 standards info internal/readmodel/readmodel_test.go, internal/ingest/manifest.go, internal/pgstore/sqlgate_test.go Три пина этого же пака измеряли МЕНЬШЕ, чем заявляли — тот самый класс, ради которого пак и существует: (а) рост бэкоффа материализации проверялся у ФУНКЦИИ, поэтому плоский retryIn(1) в её ВЫЗОВЕ переживал батарею; (б) книжная сверка UnitsTotal в Manifest.Whole() не проверялась — её случай неотличим от по-главной проверки только на первый взгляд: каждая глава может описывать себя верно, пока документ в целом называет другое число пар, а это ровно то, что делает переименованное поле; (в) таблица исключений гейта SQL ключевалась НОМЕРОМ СТРОКИ, то есть переехавший на неё вызов был бы проверен чужим текстом. Всё три закрыты; у (в) добавлен гейт «исключение, которое больше не нужно, — ошибка», чтобы мёртвая строка таблицы не прикрывала то, что под неё переедет fixed(P8-FIX) ревьюер контракт-конформности
PD-346 bug BLOCKER internal/runs/reconcile.go reconcileOne Механизм, ради которого написан весь пак, НЕ ВКЛЮЧАЛСЯ НА ЕГО СОБСТВЕННЫХ ДЕФОЛТАХ — и это было бы вторым PD-169 подряд. Отсрочка ставилась только если item.Err() != nil && ctx.Err() == nil, то есть «элемент выбрал СВОЙ бюджет, пока у фазы время ещё было». Но контекст элемента — ПОТОМОК фазового и создаётся ПОЗЖЕ, поэтому дедлайн фазы никогда не позже; на залендённых числах (проход 2 мин ⇒ фаза 60 с против бюджета прогона 60 с) это один и тот же миг. Квалификатор был ложен ровно тогда, когда прогон завис, срабатывала другая ветка — и зависший прогон записывался как УСПЕШНО сверенный, обнуляя накопленный счётчик. Механизмы 2, 3 и 4 (отсрочка · гейдж runs_stalled · tmplatformctl runs/run abandon) на любом дефолтном деплое были недостижимы, при том что ВСЕ их пины проходили: каждый из них выставлял бюджет прогона много меньше прохода. Найдено ревьюером, написавшим пин на РЕАЛЬНОМ соотношении — починено снятием квалификатора: истечение бюджета И ЕСТЬ неудача. То, ради чего квалификатор стоял (не наказывать прогон, оказавшийся последним в занятом тике), отдано осознанно по асимметрии: незаслуженная отсрочка стоит минуту и стирается первым же успехом, пропущенная — вечность; а прогон, обрезанный пять проходов подряд, это и есть голодание, о котором оператор обязан услышать. Пин — TestTheDeferralEngagesAtTheRatioThisZoneShips, написанный на shipped-соотношении и только на нём fixed(P8-FIX) ревьюер «шов и деньги», волна 1
PD-347 bug major internal/pgstore/runs.go AbandonRun Новая операторская ручка брала блокировки ПРОТИВ глобального порядка (§22 books → runs → run_attempts → …): for update of r, a и только потом lockBook — ровно инверсия, которую lockBook называет причиной «258 дедлоков из 300». Замерено ревьюером: 7 сорванных транзакций на 60 конкурентных пар с FinishRun и 3 с PauseRunобе на пути расчёта денег. Деньги не бились (кэш сходился с леджером), цена — сорванный проход свипа или отказ операторской команды сырым текстом Postgres. Починено: book_id читается без блокировки, книга блокируется ПЕРВОЙ, прогон и попытка пере-читаются под ней (та же форма, что у FinishRun) fixed(P8-FIX) ревьюер «шов и деньги»
PD-348 bug major, деньги internal/pgstore/runs.go AbandonRun Ручка могла закрыть прогон, чей движок ЖИВ, и отдать его холд целиком. Гейд читал только unit_name, а ReleaseSpawnClaim обнуляет имя, СОХРАНЯЯ spend_baseline_micro_usd — и RecordSpawn прямо пишет, что это надгробие возможно живого процесса («could not be created is not was not created»): systemd-run, убитый после того, как уже попросил, оставляет движок работать. Реконсилятор этот случай отрабатывает (finishStopped спрашивает systemd), новая ручка — нет. Итог по замеру ревьюера: status=failed, холд возвращён, прогон вне ListLiveRuns и UnsettledRuns — движок, если жив, тратит против книжного потолка без резервации и вне всех списков. Починено: гейд читает ОБА свидетельства, а сообщение называет выводимое имя юнита, потому что оператору идти к systemctl fixed(P8-FIX) ревьюер «шов и деньги»
PD-349 bug minor internal/pgstore/runs.go AbandonRun Протухший paused_reason оставался на failed-прогоне и уезжал на провод — канон 0.4.0 разрешает машинную причину только при status: paused («null otherwise»). Живой прогон получает её из журнала (событие ceiling), а ручка меняла статус и колонку не трогала; FinishRun ровно для этого держит nullif($5,''). Замерено ревьюером на проводе: status=failed paused_reason=credit_exhausted. Починено обнулением в том же операторе fixed(P8-FIX) ревьюер «шов и деньги»
PD-350 bug major internal/ingest/manifest.go Whole У нового пола не было свидетеля для полей-ИДЕНТИЧНОСТЕЙ — и это тот самый класс, ради которого пол написан. Пол сверял только списки, у которых рядом напечатан счётчик; у chapters[].id и units[].id счётчика нет, а derivedID берёт строку дословно. Переименование любого из двух ключей декодируется аллоулистом в "" для ВСЕХ строк, все они схлопываются в один производный id, и replacement-запись сносит остальную книгу — при документе, проходящем каждый объявленный им счёт. Замерено ревьюером: 3 пары → 1 строка, err=nil; 2 главы → 1 глава. Починено отказом на пустом id: движок пустого не выдаёт никогда (buildManifest), так что цена нулевая, и это единственная проверка здесь, которую счётчики сделать не могут fixed(P8-FIX) ревьюер «шов и деньги»
PD-351 bug minor internal/runs/reconcile.go, cmd/tmplatformd/runner.go pass Пак чинил класс и по дороге снёс единственный сигнал, которым этот класс виден. tm_platform_sweep_unfinished_total поднимается вызывающим по errors.Is(err, context.DeadlineExceeded), а разделённый на фазы Sweep начал возвращать nil в обеих фазах — счётчик, который PD-169 называет наблюдаемостью голодания, перестал мочь вырасти вообще. Вместе с PD-346 (гейдж застрявших оставался нулём) инсталляция теряла ОБА сигнала. Починено: фаза, не дошедшая до конца списка, возвращает обёрнутый DeadlineExceeded с числами «дошли до N из M»; расчётная фаза не дублирует счёт. Пин — TestAPassThatRanOutOfTimeSaysSo fixed(P8-FIX) ревьюер «шов и деньги»
PD-352 bug info cmd/tmplatformctl/runs.go listRuns, internal/pgstore/runs.go StalledRuns tmplatformctl runs без флага не показывал ни одного здорового живого прогона, хотя строка usage обещает «live runs and what the reconciler cannot finish»: пол листинга был 1 неудача. То есть на работающем деплое команда отвечала «no run is failing to reconcile» — и тот же ответ давала, пока прогон УЖЕ клинил, а отсрочка его ещё не посчитала. Плюс в строке не было spend_micro_usd: решение «вернуть холд ЦЕЛИКОМ» принималось вслепую к тому, сколько работы списывается (колонка писалась и не читалась нигде). Починено обоим; пин — TestTheRunsListingShowsLiveRunsAndNarrowsToStalledOnes на уровне КОМАНДЫ fixed(P8-FIX) ревьюер «шов и деньги»
PD-353 bug BLOCKER internal/runs/reconcile.go settlePhase, internal/pgstore/sink.go UnsettledRuns Блокер пака был починен на ПОЛОВИНЕ прохода. Разделение на фазы не даёт одной фазе съесть другую и ничего не делает с голоданием ВНУТРИ фазы, а список расчёта денег упорядочен так же — order by a.ended_at, старейший первым. Одна завершённая попытка, чей tmctl status не отвечает (проект на отвалившемся маунте — и ровно то состояние, которое оставляет run abandon), съедала весь бюджет расчёта каждый проход: холды ЧУЖИХ аккаунтов по уже законченным книгам не возвращались, пока тот проект недоступен. По ошибке это не ловилось: settle при нечитаемой трате возвращает nil намеренно и правильно, то есть клин рапортовал УСПЕХ, потратив проход — закрыто тем же механизмом, что и первая фаза: сигналом стало ИСТЕЧЕНИЕ БЮДЖЕТА, UnsettledRuns фильтруется отсрочкой, DeferRun перестал требовать живую попытку (множества фаз не пересекаются по ended_at). Пин TestASettlementNobodyCanFinishStopsHoldingTheHeadOfTheMoneyList; пин волны 1, закреплявший ОБРАТНОЕ («асимметрия списков»), развёрнут вместе с механизмом и это названо в нём вслух fixed(P8-FIX) воркфлоу-ревью волны 2: ПЯТЬ независимых линз, включая Fable 5
PD-354 bug major internal/runs/reconcile.go reconcileOne, maybeResync Счётчик неудач не мог дорасти до собственного порога. Дорогой вопрос движку задаётся не чаще ResyncEvery (дефолт 5 мин) и пропускается совсем, пока движется поток, — поэтому прогон с зависшим status падал, получал отсрочку, возвращался через минуту на проход, который движка НЕ СПРАШИВАЛ, и обнулял счётчик этой тишиной. 1,0,1,0 навсегда: StalledAfter недостижим, гейдж пуст, бэкофф не растёт, оператору не говорят — то есть механизмы 3 и 4 пака были мертвы на любой инсталляции — закрыто правилом «счётчик снимается УЛИКОЙ, а не её отсутствием»: reconcile сообщает, установил ли проход хоть что-нибудь (журнал сдвинулся · движок ответил · попытка закончилась), и только тогда зовётся ClearRunDeferral. Пин TestTheFailureCountSurvivesThePassesThatAskTheEngineNothing fixed(P8-FIX) воркфлоу-ревью волны 2
PD-355 bug major, деньги internal/pgstore/runs.go StalledRuns, cmd/tmplatformctl/runs.go Колонка SPENT операторской таблицы показывала пожизненную трату КНИГИ, а не этой попытки. В run_attempts.spend_micro_usd лежит цифра движка дословно, а она кумулятивна по книге за все прогоны (ingest.Spend), — то есть на второй книге оператор, решающий --release-hold, видел всё, что книга стоила с загрузки. Таблица заведена ровно ради этого решения — закрыто разностью с базовой линией (та же арифметика, что у attemptSpend, с тем же клампом), а попытка БЕЗ базовой линии печатает ?, а не ноль: это ровно тот случай, где settle отказывается считать деньги вообще fixed(P8-FIX) воркфлоу-ревью волны 2
PD-356 bug major internal/pgstore/readmodel.go SaveStructure, internal/readmodel/readmodel.go Провалившийся экспорт на СМЕНЕ КАДРА стирал весь оплаченный текст книги. TextKnown бережёт пару только там, где её строка ВЫЖИВАЕТ: при смене кадра меняются все идентичности, старые строки удаляются каскадом, новые вставляются с тем, что принёс вызывающий, — то есть с пустотой, если манифест прочёлся, а экспорт нет. Дальше списание долга (PD-330) замораживало пустую поверхность навсегда, опровергая собственное обоснование «книга сохраняет прежнюю поверхность». Замерено ревью: тот же кадр — текст цел, новый кадр — source="", target="" — закрыто отказом: Structure.TextRead отличает «экспорт ответил пусто» от «экспорт не ответил», и разрушительная замена на второй не делается (ErrTextUnknownForANewCut), долг остаётся следующему дрейну fixed(P8-FIX) воркфлоу-ревью волны 2
PD-357 bug minor internal/readmodel/readmodel.go giveUpOrRetry, internal/ingest/exit.go Бюджет попыток книги тратился на общехостовые отказы: ни одной ветки по ПРИЧИНЕ — подменённый бинарь движка, чужой лок проекта, немигрированная схема стоили книге попытку из пяти, а применимы они ко всем книгам хоста разом, так что несколько проходов списали бы всю библиотеку. Соседний пакет держит ровно эту норму и называет её вслух (books.waitsForTheDeployment) — закрыто предикатом ingest.DeploymentFault (там, где живёт контракт кодов выхода) и pgstore.AttemptCost: такой отказ откладывается с бэкоффом, но попытки не стоит. Пин TestABrokenDeploymentDoesNotSpendABooksAttempts (три формы) fixed(P8-FIX) воркфлоу-ревью волны 2
PD-358 bug minor cmd/tmplatformctl/runs.go oneLine Заголовок книги из загрузки пользователя подделывал строки и колонки операторских таблиц, а stderr движка резался ПО БАЙТУ. Интейк вычищает управляющие символы только из заголовка, выведенного из ИМЕНИ ФАЙЛА, поэтому названный руками приезжает в таблицу с табами и переводами строк; продукт переводит zh/ja, так что срез по фиксированному смещению попадает внутрь трёхбайтовой руны примерно в двух случаях из трёх — закрыто заменой управляющих символов и срезом по границе руны. Пин TestTheOperatorsTableCannotBeForgedByABooksOwnText; ESC пинится на самой функции, потому что tabwriter съедает таб в отступ и подделку в выводе не видно. ⚠ Две линзы ревью разошлись во ВЕСЕ этой находки (одна сочла микрополировкой, вторая довела цепочку до многобайтового stderr движка); починено как дешёвое и бесспорное fixed(P8-FIX) воркфлоу-ревью волны 2
PD-359 hardening minor internal/pgstore/books.go truncateReason NUL обходил починку PD-340: strings.ToValidUTF8 чинит невалидный UTF-8, а U+0000 валиден — и Postgres отвергает его тем же SQLSTATE 22021, то есть падает ровно та запись, которая фиксирует отказ. ⚠ Живого входа не найдено и это сказано прямо: движок держит NUL-гейт на всех ветках декода и не цитирует байты файла в сообщении об отказе; закрыта дыра в защите, а не воспроизведённый дефект fixed(P8-FIX) воркфлоу-ревью волны 2
PD-360 bug minor internal/pgstore/sqlgate_test.go Собственный гейт пака давал ложную зелень: таблица исключений unresolvable ключёвана функцией, поэтому ВТОРОЙ несворачиваемый запрос в той же функции молча проверялся текстом первого и ещё и увеличивал отчёт «N statements planned» — подделка выглядела как рост покрытия. Бьёт по объявленному свойству самого гейта («запись пишется руками, значит второй такой сайт — чьё-то решение, а не тихая дыра») — закрыто счётом сайтов: запись покрывает ровно один, второй сообщается fixed(P8-FIX) воркфлоу-ревью волны 2
PD-361 bug minor internal/pgstore/runs.go AbandonRun, cmd/tmplatformctl/runs.go --release-hold не мог изменить исход по деньгам, а его ветка оставляла прогон нерассчитанным навсегда. Гейд живости допускает к abandon только попытку, не дошедшую до движка, — значит она ничего не потратила и холд возвращается ЦЕЛИКОМ обычным расчётом на следующем проходе; флаг решает только КОГДА. При этом доккоммент обещал «холд остаётся открытым, пока есть шанс, что движок ответит» — то, что гейд уже сделал невозможным, — а ветка флага закрывала резервацию, минуя список расчёта, и settled_at не ставил никто — закрыто: обещание исправлено, ветка сама штампует settled_at, флаг оставлен и назван тем, чем является (нужен, когда демон остановлен — состояние, ради которого этот CLI и существует). Пин переименован по факту: TestAbandoningAStalledRunIsRefusedOverALiveUnitAndAlwaysGivesTheHoldBackПАК P8-REVIEW 24.08: пин этой строки доказывает возврат холда на прогоне, которого команда не обслуживает — он берёт прогон, созданный Start и брошенный сразу, у которого reconcile_failures 0 и reconcile_after NULL. У всей популяции, ради которой построен run abandon, отсрочка стоит, и холд ждёт до 30 минут. Вынесено строкой PD-391; статус паком не менялся ⚠ ОСПОРЕНО(PD-391) fixed(P8-FIX) воркфлоу-ревью волны 2
PD-362 bug minor internal/runs/reconcile.go ranOut, reconcileOne Штатная остановка демона читалась как голодание инсталляции: обе фазы сообщали об исчерпании через DeadlineExceeded, а отмена контекста — тот же ctx.Done(), поэтому обычный рестарт поднимал tm_platform_sweep_unfinished_total и, этажом ниже, писал каждому недообработанному прогону отказ, которого у него не было — закрыто различением: голодание — только истечение СОБСТВЕННЫХ часов фазы. Пин TestAPassTheDaemonStoppedIsNotReportedAsStarvation fixed(P8-FIX) воркфлоу-ревью волны 2
PD-363 bug minor internal/pgstore/runs.go RunsToReconcile Отсрочка не снималась НОВОЙ уликой: пользователь жал стоп, движок умирал, маркер лежал на диске — а прогон висел translating с зарезервированным холдом до конца бэкоффа (минуты на прогоне, отложенном пару раз), потому что свип — единственный читатель живых прогонов, а отсрочка есть утверждение о ПРОШЛОМ. Регрессия этого же пака: до него Sweep читал ListLiveRuns, и стоп отрабатывал на ближайшем тике — закрыто одним предикатом: интент на стоп перевешивает отсрочку fixed(P8-FIX) воркфлоу-ревью волны 2
PD-364 bug info internal/pgstore/migrations/00025_stalled_work.sql Индекс, который не мог обслужить запрос, о котором его комментарий говорил «зеркалит». Замерено ревью: планировщик берёт существующий run_attempts_live_idx (ended_at is null), новый индекс не сканируется ни разу, стоит одну пустую страницу, а стоимость свипа ограничена числом ОДНОВРЕМЕННО живых прогонов, а не длиной истории — индекс УДАЛЁН, а не переописан; обе фазы свипа упираются в уже существующие индексы. ⚠ Миграция правится в СВОЁМ ЖЕ незакоммиченном паке (не выкачена нигде), отпечаток в migrations.sha256 пере-записан — это НЕ правка релизной миграции fixed(P8-FIX) воркфлоу-ревью волны 2
PD-365 hardening info cmd/tmplatformd/runner.go Присвоение операторских ручек в демоне не было засвидетельствовано ничем: startRunner требует БД, шину systemd и очередь, поэтому его конфиг-литерал наблюдаем только прогоном всего демона — именно так RunBudget и прожил целый пак объявленным, задокументированным и равным нулю (PD-331). Снятие обеих строк оставляло батарею зелёной — закрыто выносом отображения в чистую runsConfig и пином TestTheOperatorsRunnerKnobsReachTheReconciler; посадка ловится fixed(P8-FIX) воркфлоу-ревью волны 2
PD-366 bug info internal/pgstore/perf_test.go Комментарий лгал о коде под ним (класс PD-310/PD-326): условия замера объявляли, что бенчмарк этого файла воспроизводит ХОЛОДНЫЙ корпус, «потому что сеет и меряет без вакуума», — а corpus заканчивается vacuum analyze, то есть меряет ровно вакуумированный мир и его нормативные 16.8/3.4 — формулировка исправлена по факту fixed(P8-FIX) воркфлоу-ревью волны 2

Закрытые — эра P11 (отзыв сессии на открытом потоке · застрявший расчёт · денежные пины)

| PD-379 | vuln | major | internal/httpapi/stream.go:160=h.pump(r.Context(), s, who, bookID, state, last, resuming) ⚠ якорь пере-нацелен паком P11: сигнатуру pump изменил он сам (принципал вместо голого id — в этом и лечение), internal/httpapi/server.go guard, internal/auth/session.go SessionStore | Открытый поток событий переживает и отзыв сессии, и оба её потолка: «выйти везде» не выключает уже установленный канал. Аутентификация происходит РОВНО ОДИН РАЗ, в auth.Authenticator.Require внутри guard; дальше streamEvents уходит в pump, и цикл до конца соединения читает только ReadFrames/ReadStream, строку сессии не смотрит ни разу. Значит POST /auth/logout, POST /auth/logout-all, tmplatformctl revoke и оба потолка (idle и абсолютный) уже открытый GET /v0/books/{bookId}/events не прекращают. Пере-проверено трижды независимо (финдер, рефутер в отдельной копии, координатор); на демоне с SESSION_IDLE=5s/SESSION_MAX_AGE=10s поток жил +40 с после отзыва. Бьёт по объявленной норме: ASVS 5.0 7.4.1 — требование УРОВНЯ 1 при объявленном зоной L2, и STACK_DECISIONS §13 отказывается от лимита одновременных сессий ИМЕННО в обмен на мгновенный отзыв. ⚠ Побочно, тем же прогоном: /auth/logout-all на ДЕВ-профиле не смонтирован вовсе (404) — из пары ручек, которой §13 обосновывает свою политику, на стенде доступна одна. Воспроизведение: docs/p8-review/axis2-auth/sse-outlives-revocation.sh и sse-outlives-absolute-ceiling.sh, снимок координатора docs/p8-review/sse-outlives-revocation.txtВЕС ПОДНЯТ minor → major ПОСЛЕ РЕВЬЮ СТАРШЕЙ МОДЕЛЬЮ (fable-5), и поднят по трём доводам, которых сужение не учло. (1) Граница «соединение само закрывается, когда книга приходит в покой» — не гарантия кода: у книги, чей долг материализации списан как неоплатный, поток НЕ КОНЧАЕТСЯ НИКОГДА, и это собственный комментарий зоны — internal/pgstore/books.go:629=a book whose event stream can NEVER end. То есть окно утечки не ограничено прогоном. (2) Вес отказавшего КОМПЕНСИРУЮЩЕГО контроля наследуется от рисков, которые он компенсирует, а не от схемы кадра: STACK_DECISIONS §13 отказывается и от лимита одновременных сессий, и от собственной границы федеративной сессии ИМЕННО в обмен на мгновенный отзыв и два срока — а открытый поток ускользает от всех трёх разом, и у §13 не остаётся содержания. (3) Провалено требование УРОВНЯ 1 при объявленном зоной L2 — это дыра ниже собственного пола, а не отклонение от лучших практик. ⚠ ЗАКРЫТО ПАКОМ P11 (29.08). auth.Principal несёт непубличную способность пере-спросить свою сессию, pump зовёт её ПЕРВЫМ ДЕЛОМ на каждом тике, отказ даёт терминальный кадр session_ended (канон 0.8.0) с watermark СОЕДИНЕНИЯ, а не головой истории. Запрос стора StillLive намеренно БЕЗ клаузы idle: окно бездействия скользит на запросе, а поток — один запрос на всю жизнь, поэтому гашение по idle рвало бы связь активному читателю. Доказано ДВУМЯ раздельными живыми сценариями: (а) длинные потолки + tmplatformctl revoke → поток кончился через 1 с (~/tm-p11/probes/a-revocation-ends-the-stream.txt); (б) idle 10 с / абсолютный 40 с, БЕЗ отзыва → поток пережил окно бездействия и кончился ровно на потолке (b-the-ceiling-ends-the-stream.txt). Посадки r_nocheck, r_idle, r_head, r_open, r_wirename — пойманы. Эррата STACK_DECISIONS §13 снята, галочка ASVS 7.4.1 в архиве восстановлена. ⚠ Остаток отдельной строкой: строку сессии удаляет часовой свип и по бездействию тоже, поэтому «строки нет» обязано значить «мертва» | fixed(пак P11 — лендинг оркестратора; статус проставлен зоной по норме §3.6 «закрытие дефекта — коммит + пинящий тест») | ревью-пак P8-REVIEW, ось 2 (живая проба, подтверждено рефутером и координатором) | | PD-385 | bug | major | internal/pgstore/runs.go:541=where a.ended_at is null and a.reconcile_failures >= $1 ⚠ якорь пере-нацелен паком P11: прежняя строка (where r.finished_at is null and …) была ОДНИМ предикатом на обе половины и её больше нет — выборка разложена на две ветви, и это ровно лечение, internal/pgstore/observe.go (гейдж), internal/pgstore/runs.go AbandonRun | Прогон, чей РАСЧЁТ доведён до StalledAfter, не виден операторским поверхностям порога, а лог-строка на пересечении порога шлёт оператора именно туда. Обе фазы делят один счётчик через общий deferItem, но операторская половина построена только для ЖИВЫХ прогонов: StalledRuns джойнит a.ended_at is null и фильтрует r.finished_at is null, гейдж tm_platform_runs_stalled считает по тому же предикату, а AbandonRun читает where id = $1 and finished_at is null и отвечает ErrNoRun. Живая проба на состоянии, выращенном штатными путями (интейк, HTTP-старт, отказ спавна, run abandon): runs --stalled отвечает «no run is failing to reconcile», runs — «no run is live», run abandon — «is not a live run», гейдж 0, при этом в базе settled_at NULL, reconcile_failures 5 и открытая резервация на 90000 микро. Тот же слепой угол закрывает прогон, который ЖИВ, но чья ПРЕДЫДУЩАЯ попытка не рассчиталась после рестарта. ⚠ Рефутер опроверг заголовочный абсолют «невидим ВСЕМ поверхностям»: tmplatformctl balance --user печатает этот холд строкой, tm_platform_oldest_open_hold_seconds растёт без потолка, а tmplatformctl books показывает «WHY NOT: unsettled hold»; ноль в улике финдера был артефактом его фикстуры. Остаётся то, ради чего строка заведена: три поверхности ПОРОГА слепы, терминальной ручки для такого прогона нет, а ERROR на пороге называет команду, которая на нём молчит. Воспроизведение: docs/p8-review/axis3-queue/live-stalled-settlement.shВЕС ПОДНЯТ minor → major ЗАКРЫВАЮЩИМ РЕВЬЮ СТАРШЕЙ МОДЕЛИ, и довод не про эту строку в одиночку, а про КРУГОВОЕ сужение четырёх строк пака. PD-384 сужен до minor тем, что холд «виден» гейджу tm_platform_oldest_open_hold_seconds и команде balance --user. Но PD-392 доказывает ЖИВОЙ ПРОБОЙ, что у этого гейджа ручки НЕТ: идентификатора он не даёт, balance --user требует аккаунт, которого гейдж не называет, глобального списка открытых холдов в CLI нет, а документированный случай самого гейджа это ровно данная популяция — при oldest_open_hold_seconds 10813 все три команды отвечают «no run is live», «no run is failing to reconcile», «no book has been given up on». PD-390 доказывает, что тот же гейдж умеет ЗАМИРАТЬ и отдавать нули как здоровье. PD-389 — что его сеттеры не запинены ничем. То есть каждое из четырёх сужений держится поверхностью, несостоятельность которой доказывает соседняя строка ТОГО ЖЕ пака, и по кругу. А терминальной ручки для этой популяции нет ПО ПОСТРОЕНИЮ: internal/pgstore/runs.go:689=select finished_at from runs where id = $1 ⚠ якорь пере-нацелен паком P11: прежняя строка отвечала ErrNoRun законченному прогону — она и была дефектом; теперь это ветвление, читаемое ПОД блокировкой книги. Следствие, названное прямо: для всей популяции «закончен, но не рассчитан» деньги пользователя заморожены бессрочно, поверхности ПОРОГА слепы, ERROR на пороге называет команду, которая откажет, и единственный выход — сырой SQL в проде. Составной инвариант, на котором принят пак P8-FIX (D39.154: гейдж плюс runs --stalled плюс run abandon как ответ на PD-169), для этой популяции ЛОЖЕН ЦЕЛИКОМ — а «решается до следующего пака» есть определение major-секции самого регистра. Носителем major сделана ЭТА строка как самая полная по улике (живая проба на состоянии из штатных путей плюс отказ ручки); PD-384 и PD-392 несут ссылку сюда, чтобы не плодить второй major на тот же корень ⚠ ЗАКРЫТО ПАКОМ P11 (29.08). Популяция «кончился, а деньги нет» вошла в StalledRuns (колонка PHASE), в гейдж tm_platform_runs_stalled и получила терминальную ручку: run abandon закрывает КАЖДЫЙ осиротевший холд прогона, снимает отсрочку и штампует settled_at; допуск сужен до reconcile_failures >= 1, иначе команда отдавала бы целиком холд расчёта, который просто ещё не закрылся. Доказано до/после на состоянии из ШТАТНЫХ путей (интейк → HTTP-старт → спавн → выход движка → снят запиненный бинарь): было «no run is live» / «is not a live run» / гейдж 0 при открытой резервации 90000 микро, стало строка settling с холдом и возврат денег целиком (~/tm-p11/probes/pd385-before.txt, pd385-after.txt). ⚠ Вторая названная строкой популяция — ЖИВОЙ прогон с нерассчитанной ПРЕДЫДУЩЕЙ попыткой — получила обе поверхности видимости, но не ручку: отдельной строкой | fixed(пак P11 — лендинг оркестратора; статус проставлен зоной) | ревью-пак P8-REVIEW, ось 3 (живая проба, сужено рефутером) | | PD-376 | bug | minor, деньги | internal/pgstore/runs.go:1227=select min(a.spend_baseline_micro_usd), пин internal/runs/sweep_test.go TestADeferredSettlementIsNotChargedForTheNextRunOfTheSameBook | PD-159 стоит fixed, а её пин доказывает свойство СЛАБЕЕ, чем читается: слово min не исполняет никто, и мутация minmax проходит батарею. Строка PD-159 закрывает двойную оплату формулой «SpendBound — НАИМЕНЬШАЯ базовая линия среди попыток этой книги, стартовавших ПОЗЖЕ», а названный ею пин кладёт в книгу РОВНО ОДНУ более позднюю попытку — на множестве из одного элемента min и max совпадают. Прогон ./internal/pgstore/ и ./internal/runs/ под мутацией зелёный; независимый пин на той же мутации падает (the bound is 0.500000, want 0.200000), то есть мутация поведенческая, а не эквивалентная. Достижимость: при монотонном росте книжного счётчика min и max расходятся уже при ДВУХ более поздних попытках, а две даёт один преемник, переживший рестарт или резюм; тогда границей становится базовая линия, УЖЕ содержащая трату предыдущего прогона — это ровно PD-159 на одну попытку дальше. Переплата ограничена холдом. Класс — «реестр умеет врать», тот же разбор, каким был найден PD-169. Готовый пин: docs/p8-review/axis1-money/a1_spendbound_test.go.txt и независимый r1_spendbound_test.go.txt. ⚠ Пере-проверено на ПОЛНОЙ батарее координатором пака (мутация M11): улика пере-снята на полной батарее — прогон go test ./... -count=1 со всеми тремя гейтами под той же мутацией даёт ПУСТУЮ дельту против чистой базовой линии той же копии — то есть мутацию не ловит ни один из 18 пакетов. Лог — docs/p8-review/mutations-round2.logЗАКРЫТО ПАКОМ P11 (29.08). Взят готовый пин пака — вариант r1_ как более сильный (ходит настоящими дверями StartRun/RecordSpawn, наименьшая базовая линия стоит В СЕРЕДИНЕ, так что «первая поздняя» и «последняя поздняя» тоже падают) — и усилен ВТОРОЙ книгой того же аккаунта, чтобы исполнялся и фильтр r.book_id. Посадка minmax: чистая копия EXIT=0, с мутацией EXIT=1, единственный красный — этот пин. ⚠ ДИСПОЗИЦИЯ, которую строка оставляла приёмке: PD-159 НЕ пере-открывается. Основание прежнее (D39.159 §5): дефект из кода ушёл, недоставало ПИНА, — а теперь пин есть, то есть пробел закрыт там, где он был. Двусторонняя ссылка сохраняется: PD-159 несёт ОСПОРЕНО(PD-376), эта строка называет PD-159 | fixed(пак P11 — лендинг оркестратора; статус проставлен зоной) | ревью-пак P8-REVIEW, ось 1 (посадка мутации, подтверждено рефутером собственным пином) | | PD-384 | bug | minor | internal/runs/reconcile.go:278=case overran: (settleOne) ⚠ якорь пере-нацелен паком P11: прежнее if !overran { было САМИМ дефектом — судить по цене вместо вердикта — и заменено свитчем по вердикту, internal/runs/reconcile.go settle (три тихих return nil) | Расчёт денег, упавший ДЁШЕВО, не считается никогда: порог StalledAfter для него недостижим. Вторая фаза считает неудачу ТОЛЬКО по исчерпанию бюджета (overran := errors.Is(item.Err(), context.DeadlineExceeded), дальше if !overran { return }), а settle возвращает nil БЫСТРО в трёх случаях: движок не ответил, в отчёте нет committed, попытка без базовой линии. Каждый может быть ПОСТОЯННЫМ — запиненный бинарь движка снесён при выкате, проект заменён под платформой, попытка старой схемы. Тогда цикл вечен: reconcile_failures остаётся 0, reconcile_after NULL, гейдж и tmplatformctl runs --stalled пусты, холд заморожен. Замерено пробой: пять проходов одного нерассчитываемого прогона дали 5 вызовов движка, reconcile_failures=0, StalledRuns(5)=0. Плюс цена: settle зовёт tmctl status НА КАЖДОМ проходе без рейт-лимита, тогда как соседний maybeResync имеет dueForResync ровно из-за этой цены. ⚠ Рефутер сузил вес major → minor: холд ВИДЕН двум поверхностям, которых финдер не спросил — гейдж tm_platform_oldest_open_hold_seconds и tmplatformctl balance --user, печатающий каждый открытый холд суммой, книгой и id прогона; плюс каждый проход пишет WARN с id прогона. Воспроизведение: docs/p8-review/axis3-queue/probe_settlement_surface_test.go.txtОбщий корень с PD-385, и там же он взвешен: сужение ЭТОЙ строки опирается на операторскую поверхность, несостоятельность которой доказывает соседняя строка того же пака — круговое сужение разобрано в PD-385, поднятой до major ⚠ ЗАКРЫТО ПАКОМ P11 (29.08). Неудачей считается ВЕРДИКТ расчёта, а не только исчерпание бюджета: settle вернул три состояния (закрыт · гонка · не вычислим), settleOne судит по ним, поэтому все три тихих return nil теперь доходят до порога. Цена, названная строкой, закрыта тем же ходом: отсрочка ограничивает tmctl status вместо вызова каждым проходом. ⚠ Первая неудача НЕ откладывается — открытая резервация это ворота РЕЗЮМА пользователя (reopen отказывает, пока холд предыдущей попытки открыт), и минута ожидания после секундной аварии была бы регрессом; бэкофф идёт со второй и капнут пятью минутами, а не тридцатью. Посадка r_firstfast | fixed(пак P11 — лендинг оркестратора; статус проставлен зоной) | ревью-пак P8-REVIEW, ось 3 (проба на реальном сторе, сужено рефутером) | | PD-391 | bug | minor | internal/pgstore/sink.go:724=where a.reconcile_after is null or a.reconcile_after <= $1, internal/pgstore/runs.go AbandonRun, cmd/tmplatformctl/runs.go (сообщение), deploy/README.md | run abandon не возвращает холд «на ближайшем свипе»: отсрочка застрявшей попытки остаётся, и деньги ждут до 30 минут. AbandonRun завершает прогон и попытку, но run_attempts.reconcile_after не трогает, а UnsettledRuns по нему фильтрует. Застрявший прогон по построению всегда отсрочен: deferItem ставит now + backoff(failures+1), а backoff при пяти неудачах упирается в потолок 30 минут. Значит для ВСЕЙ популяции, ради которой команда построена, обещание CLI «its hold comes back whole on the next sweep» и та же фраза рантбука ложны: кредит остаётся вычтенным, tm_platform_oldest_open_hold_seconds продолжает расти ПОСЛЕ действия оператора, и оператор читает это как «я сделал, не помогло». Пин PD-361 доказывает свойство слабее: он берёт прогон, созданный Start и брошенный СРАЗУ, у которого reconcile_failures 0 и reconcile_after NULL. Живая проба на состоянии, выращенном штатным механизмом: через 45 секунд и три свипа холд открыт, balance печатает «reserved 3.000000», гейдж 2439 с; ручной update run_attempts set reconcile_after = now() закрывает холд в тот же свип. Воспроизведение: docs/p8-review/axis4-metrics/20-abandon-keeps-the-hold.sh и r3-abandon-hold.shЗАКРЫТО ПАКОМ P11 (29.08). AbandonRun снимает reconcile_after в ОБЕИХ ветках, живой и расчётной. Пин растит прогон до пяти неудач штатными путями и сверяет холд ПОСЛЕ свипа на НЕДВИНУТЫХ часах — то есть исполняет ровно то обещание CLI, которое было ложным. Названные строкой носители обещания исправлены: deploy/README.md и сообщение команды. Посадка m391_defer — поймана топично | fixed(пак P11 — лендинг оркестратора; статус проставлен зоной) | ревью-пак P8-REVIEW, ось 4 (живая проба, подтверждено рефутером на состоянии из штатного пути) | | PD-397 | hardening | info | internal/pgstore/credits.go:52-53=A ledger row is never edited: the correction is another row, internal/pgstore/credits.go:398=The two are never written apart, миграция internal/pgstore/migrations/00007_credits.sql | Два самых сильных денежных инварианта объявлены ПРОЗОЙ и держатся ТОЛЬКО кодом — схема их не навязывает. Adjust обещает «леджер не правится, коррекция это ещё одна строка, и именно это делает сумму воспроизводимой»; appendLedger обещает «кэш и леджер никогда не пишутся врозь, потому что отстающий кэш — это второй ответ про деньги». Проба прямым SQL по стенду показывает, что DDL допускает нарушение обоих: UPDATE и DELETE строки леджера ПРИНЯТЫ, кэш баланса выставляется ЛОЖЬЮ и ОТРИЦАТЕЛЬНЫМ тоже. Пере-проверено координатором пака независимо от агента — все четыре приняты, и откат пробы сам же оставил расхождение кэша с леджером в 1 микро-доллар, которое поймало только сведение двумя путями, а не база. ⚠ Что схема при этом ДЕРЖИТ и что находкой НЕ является (иначе строка читается как «денежных констрейнтов нет»): знак по каждому виду строки, обязательная нота у коррекции, закрытый словарь видов, непустые source/source_id, уникальность ключа идемпотентности в пределах аккаунта, положительность сумм резервации, согласованность состояния и времени закрытия, владение книгой через композитный внешний ключ, и переполнение bigint в кэше. То есть DDL закрывает ФОРМУ строки и не закрывает ИСТОРИЮ. Цена названа и она не про сегодняшний код: пути правки леджера в Go нет, поэтому эксплуатации нет — опасны миграция данных, операторский psql и будущий инструмент, каждый из которых по построению идёт мимо кода, а прозу в доккомментарии не читает. Лечится либо триггером на update/delete по credit_ledger, либо явной записью «append-only — дисциплина кода, не схемы» рядом с обещанием. Воспроизведение: docs/p8-review/axis1-money/constraint-probe.shЗАКРЫТО ПАКОМ P11 (29.08), и закрыто ВТОРЫМ из двух предложенных строкой способов. Триггер на update/delete по credit_ledger ОТКЛОНЁН с двумя основаниями, проверенными в дереве: он сработает на КАСКАДНОМ удалении пользователя, которое миграция 00007 объявляет границей append-only, и сломает законную фикстуру TestAReleaseWhoseKeyWasSpentIsRefusedRatherThanSilent, которая правит леджер намеренно. Вместо него: проза Adjust и appendLedger сделана честной («держит КОД, а не схема», с перечнем того, что схема ДЕРЖИТ), плюс ГЕЙТ TestTheLedgerIsAppendOnlyInTheCodeThatWritesIt — ни один update/delete по credit_ledger (в том числе схемо-квалифицированный) не написан ни в одном из 172 SQL пакета. Посадка r_ledgeredit настоящей формой (tx.Exec внутри appendLedger). ⚠ Что осталось НЕзакрытым и названо: миграция данных, операторский psql и будущий инструмент идут мимо пакета по построению | fixed(пак P11 — лендинг оркестратора; статус проставлен зоной; ⚠ закрыт РАЗБОРОМ с отказом от триггера, см. тело) | ревью-пак P8-REVIEW, ось 1 (проба агента, пере-проверена координатором; строка заведена по аудиту полноты) | | PD-394 | hardening | info | internal/pgstore/credits.go:227=if spent < 0 {, констрейнт credit_ledger_sign в internal/pgstore/migrations/00007_credits.sql | Гард отрицательного расхода в Settle не запинен: снятие проходит ПОЛНУЮ батарею (18 пакетов). Класс тот же, что у PD-333/PD-334 — оговорка денежного пути, которую ни один тест не исполняет. Цена НАЗВАНА и она ограничена схемой, а не кодом: отрицательный spent дал бы settlement с положительной суммой, а это ловит констрейнт credit_ledger_sign — проверено прямым INSERT на стенде, Postgres отвечает violates check constraint "credit_ledger_sign". То есть сегодня вреда нет, и защита ТРАНЗИТИВНА: держит её схема, а не гард, который для этого написан. Родня PD-86 (там потолок сессии держится через соседнюю функцию). Достижимость самого отрицательного значения сегодня нулевая — единственный источник attemptSpend клампит в ноль, и этот кламп запинен. Воспроизведение: docs/p8-review/plant.py (мутация M4) и mutations-full.logЗАКРЫТО ПАКОМ P11 (29.08). Гард получил ИМЯ (ErrNegativeSpend) и пин на errors.Is. ⚠ Имя понадобилось не для красоты: ПЕРВАЯ редакция пина проверяла лишь «вернулась ошибка» — и посаженная мутация её прошла, потому что ошибку вернул констрейнт credit_ledger_sign, то есть ровно та транзитивная защита, о которой строка и написана. Посадка r_negative на исправленном пине — поймана | fixed(пак P11 — лендинг оркестратора; статус проставлен зоной) | ревью-пак P8-REVIEW, ось 1 (посадка мутации координатора) | | PD-414 | bug | minor, деньги | internal/pgstore/credits.go Settle (appendLedger для run_settle) | Settle ВЫБРАСЫВАЛ флаг applied своей леджер-записи — единственный из трёх вызовов appendLedger, который его не читал. Соседи проверяют: holdTx отвечает ErrDuplicateHold, releaseHoldErrReleaseKeySpent С ОТКАТОМ (PD-97). Здесь потраченный ключ run_settle НЕ СПИСЫВАЛ НИЧЕГО, при том что холд уже возвращён целиком, а вызывающему возвращался nil: аккаунт получает работу даром. ⚠ Достижимость сегодня НУЛЕВАЯ, и это записано, чтобы приёмка не искала траекторию: резервация закрывается под state = 'open', поэтому второй Settle получает ErrNoReservation и сюда не доходит, а потратить ключ можно только пере-открыв резервацию на той же попытке — что holdTx отказывает ровно по этой причине. Класс — ровно PD-394: неисполняемая сегодня оговорка денежного пути. Найдено самопроходом пака P11 (линза денег), не строкой заказа ⚠ ЗАКРЫТО ПАКОМ P11 (29.08): флаг читается, новый сентинел ErrSettlementKeySpent, откат как у releaseHold; пин TestASettlementWhoseKeyWasSpentIsRefusedRatherThanSilent, посадка r_applied | fixed(пак P11 — лендинг оркестратора; статус проставлен зоной) | самопроход пака P11 (§4.5, линза денег под гонкой) | | PD-415 | doc | minor | docs/STACK_DECISIONS.md «Стенд разработчика», internal/runner/translate_resnapshot_live_test.go | Рецепт стенда давал КРАСНУЮ батарею, а не скип, и красноту эту следующая сессия принимала за свою поломку. Рецепт рендерит шаблон книги из backend/example/book.yaml, а тот указывает на configs/pipeline-c1.yaml — ПЛАТНЫЙ DeepSeek. Живой тест P10 TestTheSnapshotGuardIsLoudWithoutTheFlagsAndPassesWithThem, включаемый вторым гейтом, гоняет настоящий движок и падает tmctl: missing API keys (fill in backend/.env) — при том что его собственный комментарий обещает «free of provider keys and of paid calls». Тест прав: у стенда есть $0-пара (local-qwen3-8b, провайдер на 127.0.0.1:11434, заглушку тест поднимает сам), но в репо НЕТ пайплайна, который бы её называл. ⚠ Цена не только во времени: гейт гоняет НАСТОЯЩИЙ движок, то есть платный пайплайн в шаблоне — ещё и риск оплаченных вызовов ⚠ ЗАКРЫТО ПАКОМ P11 (29.08): раздел переписан — три гейта вместо двух, замеренные числа (0 скипов с гейтами, 287 без на HEAD и 304 на дереве пака) и рецепт $0-шаблона, который обязан лежать РЯДОМ с prompts/ (промпты резолвятся от каталога пайплайна). Пере-проверено чужими руками: оркестратор при приёмке наступил на ту же граблю и вышел по предупреждению за минуту | fixed(пак P11 — лендинг оркестратора; статус проставлен зоной) | пак P11, сборка стенда | | PD-416 | bug | minor | internal/pgstore/sqlgate_test.go usedException | Дефект, который пак P11 ВНЁС в чужой гейт, и который поймала его же мутационная обвязка. Счётчик исполнения исключений unresolvable был ПАКЕТНОГО уровня, а collectSQL с приходом второго гейта (TestTheLedgerIsAppendOnlyInTheCodeThatWritesIt) стал вызываться дважды за прогон: общий счётчик читал первый визит ВТОРОГО вызывающего как второй визит ПЕРВОГО и падал с «Ready holds a second statement this gate cannot read» о функции, которая держит ровно одно. Проявлялось как КРАСНЫЕ ЧИСТЫЕ копии в мутационной кампании там, где копируемое дерево было зелёным, — то есть маскировало дельту, что для кампании худший исход. ⚠ ЗАКРЫТО ПАКОМ P11 (29.08): счётчик стал per-extraction (newExceptionCounter), инвариант «одно исключение — один сайт, каждое исключение исполнено» сохранён полностью | fixed(пак P11 — лендинг оркестратора; статус проставлен зоной) | мутационная обвязка пака P11 | | PD-417 | bug | minor | internal/pgstore/runs.go StalledRuns, internal/pgstore/observe.go, миграция 00028_settling_attempts.sql | Расширение операторских выборок на вторую популяцию (PD-385) оставило их без индекса: последовательный проход по всем попыткам на запросе, который рантбук советует для cron, и на гейдже, который демон гоняет каждые 15 секунд вечно. У живой половины индекс был с 00009 (run_attempts_live_idx, частичный по ended_at is null), у settling-половины — никакого. Замер на 200 000 попыток, explain (analyze, buffers), таблица 2×2: без индекса плохи ОБЕ формы (3647 буферов дизъюнкцией, 3653 через UNION ALL), с индексом хороши обе (10 и 18). То есть катастрофу снимает ИНДЕКС, а не форма запроса. UNION ALL оставлен по другому доводу: каждая ветвь несёт путь доступа СТРУКТУРНО, тогда как BitmapOr — выбор планировщика по статистике, а статистика ЗДОРОВОГО деплоя (пустая застрявшая популяция) ровно обратна той, на которой мерилось. Класс — PD-364 с другой стороны: там индекс не мог обслужить свой запрос, тут запрос перерос свои индексы ⚠ ЗАКРЫТО ПАКОМ P11 (29.08) миграцией 00028 (частичный индекс по ended_at is not null and reconcile_failures >= 1) плюс двумя ветвями | fixed(пак P11 — лендинг оркестратора; статус проставлен зоной) | самопроход пака P11 (линза цены в БД), пере-замерено по требованию артефакта |

Закрытые — перенос по секциям (аудит 29.08: статус был переведён раньше, а строка осталась под открытым заголовком)

Ни одна строка здесь не меняла СТАТУС — каждая уже несла свой fixed(...); двигалась только позиция. Читающий открытый список по весу видел закрытую работу и мог пойти чинить построенное — класс, который ENGINEERING_STANDARDS §3.8 называет стоившим зоне порядка десятой доли строк. Счёт по статусам от переноса не изменился (counts.py ключуется формой строки, а не секцией).

| PD-370 | bug | major | internal/httpapi/reading.go, internal/httpapi/v0.go, internal/pgstore/readmodel.go, канон 14-api-contract §BankDecision | Была построена и стоит в каноне модель, которую владелец отменил 16.08. D39.144 ратифицировал: подписывается ВЕСЬ банк ОДНИМ ОК, «пер-термная подпись = сотни кликов — НЕ модель продукта»; пер-термно существует не подпись, а ПРАВКА термина (до подписи) и правка с пере-генерацией задетых глав (после прочтения, гейчено 192). Гейт полноты нота сняла, а САМ ГЛАГОЛ остался — чистка дошла до resume и счётчиков и не дошла до словаря. Колонки были write-only: их не читал ни один SELECT, то есть реализации не существовало — ЗОННАЯ ПОЛОВИНА ЗАКРЫТА 22.08 по слову владельца: снят write-путь целиком (маршрут POST /books/{bookId}/bank/decisions, хендлер, wire-типы, метод интерфейса Library, SubmitBankDecisions, типы BankDecision/BankReceipt, UnknownTermError), на его месте — пометка для будущей сессии с ратифицированной моделью и с тем, что уже есть в схеме. Пол размера поверхности 15 → 14 сдвинут ЯВНО и с причиной в комментарии: гейт сработал, а не был подогнан. КОНТРАКТНАЯ ПОЛОВИНА ЗАКРЫТА 27.08 минором 0.5.0 (D39.161): путь POST /books/{bookId}/bank/decisions, глагол submitBankDecisions и три схемы снесены из канона — ноль вхождений, замер в docs/CONTRACT_MINOR_REPORT.md и пере-мер приёмки D39.161 п.2; счётчики pending_decisions/complete сняты со всех трёх носителей (BankPage, EventBank, квитанция). Преемник — дверь POST …/bank/corrections (выведена из tmctl bank-apply), СМОНТИРОВАНА паком P9 (D39.162); Capabilities.bank_corrections_enabled следует cfg.RunsEnabled(). Итог: зонная половина снята 22.08 (слово владельца), контрактная — 27.08 (D39.161); ⚠ НЕ наследовать этой строкой заказ на счётчики в wireBankPage — он жил отдельной строкой PD-399 (закрыта паком P9 своим пином). | fixed(D39.161 — контрактная половина; 22.08 слово владельца — зонная; перевод по аудиту 28.08) | поправка владельца 22.08; ратификация D39.144; аудит 31 агента 22.08 · минор D39.161 · аудит документации 28.08 | | PD-406 | bug | major | internal/httpapi/stream.go:137=helloID := state.Position | Служебный кадр hello на ПЕРЕПОДКЛЮЧЕНИИ несёт id = state.Position (голова истории книги) вместо предъявленного клиентом last — и переводит Last-Event-ID браузера ВПЕРЁД через кадры, которые это соединение ещё не отдало. По WHATWG поле id фиксируется в last event ID buffer на диспатче; обрыв сразу после hello (прокси, вывод инстанса из ротации, ошибка первого ReadFrames) теряет кадры last+1..Position НАВСЕГДА — включая note, которые контракт запрещает терять («MUST NOT be coalesced or dropped»), без resync_required; усилитель — revision в data того же hello выше потерянных строк, так что предписанный контрактом дельта-догон ?after_version= возвращает пусто, обезоружены обе починки сразу. Это ровно вариант, который 14-api-contract/README.md (§Last-Event-ID) называет отвергнутым. ⚠ ЗАКРЫТА фикс-раундом P9 (28.08, акт D39.162): при resuming hello несёт last клиента (свежий коннект — голову, как и from ниже); пин TestAResumingHelloCarriesTheClientsOwnWatermark (+ посадка «hello обратно на голову» поймана) | fixed(фикс-раунд P9, D39.162) | воркфлоу-ревью P9 28.08 (линза race:flip-read-consistency), лечение по слову оркестратора 28.08 | | PD-401 | bug | minor | internal/pgstore/runs.go StartRun (снятие баз), internal/pgstore/sink.go recordWaveShape, internal/pgstore/readmodel.go runDone/runTotal | Прогон, стартовавший при edit_wave = false и получивший переворот флага ПОСЛЕ старта, делает всю купленную работу с полосой на 0/N — и это навсегда, а не «окном». Механика: движок объявляет форму волн первым progress-событием ПОСЛЕ старта (recordWaveShape; флаг только растёт, STACK_DECISIONS §35), а базы полосы снимались в StartRun через флаг-зависимый предикат МОМЕНТА СТАРТА и не пере-базируются никогда — сидят на черновой колонке, числитель после переворота уезжает на редакторскую, разрыв не закрывается. Достижимо штатно: книга прочерчена на пайплайне без редактора, оператор ставит редактора, куплено продолжение; воспроизведено ревьюером приёмки заменой одной строки в пине пака (edit_wave = false до старта, переворот после → 0/4 при всей сделанной работе). Тот же симптом «полоса не достигает единицы», что у PD-281, — на том «промежуточном классе», которым диспозиция PD-281 и объясняется. ⚠ Остаток и ПОСЛЕ лечения: на смешанном заделе (купленный диапазон шире начернённого) переворот двигает знаменатель вверх в окне первого progress-события — транзиентный провал доли ⚠ ЗАКРЫТА лендингом P9 (58bae30, акт D39.162): лечение (фиксированные базы + пара живым флагом + миграция 00026 + пин TestAFlagThatFlipsAfterStartDoesNotStrandTheBar) заленжено и зелено. Транзиентный остаток окна первого progress-события остаётся named-границей (PD-401-остаток в словаре зоны) | fixed(лендинг 58bae30, акт D39.162; перевод по аудиту 28.08) | пак P9: опровергатель формы полосы (первая редакция) · блокер ревьюера приёмки 27.08 (пере-формулировка) · аудит документации 28.08 | | PD-409 | hardening | minor | internal/runs/bank.go decisionsFile (bank-corrections-*.json в StateDir, удаление только defer cleanup()) | Отрендеренный документ решений переживает любую нечистую смерть демона и остаётся в StateDir навсегда: ничто в дереве каталог не подметает. SIGKILL/OOM/паника/истёкший грейс деплоя посреди вызова двери — и файл до 1 МиБ с полными src/dst правленых терминов пользователя лежит под 0600 бессрочно в каталоге, где оператор считает маркеры прогонов; единственная существующая уборка (os.Remove exit-маркера в spawn.go) эти файлы не видит. Накапливается через деплои: N нечистых смертей = N сирот. ⚠ ЗАКРЫТА фикс-раундом P9 (28.08, акт D39.162): Service.SweepCorrectionScratch() подметает по маске на буте (вызов в композиционном корне tmplatformd до подъёма HTTP, файлы вне маски не трогаются); пин TestBootSweepsOrphanedCorrectionDocuments (посадка «маска мимо» поймана). Честная оговорка: сам ВЫЗОВ из main юнитом не запинен — ловится чтением | fixed(фикс-раунд P9, D39.162) | воркфлоу-ревью P9 28.08 (линзы door:crash-windows · door:lock-lifecycle), лечение по слову оркестратора 28.08 | | PD-398 | standards | info | docs/scripts/counts.py malformed, строки PD-99 и PD-197 этого файла | Гейт формы регистра ловит только НЕДОСТАЧУ ячеек, а не избыток — и в файле уже есть две строки, которые он пропускает. malformed() сравнивает if n < shape, поэтому строка с лишним символом вертикальной черты внутри инлайн-кода проходит молча: счёт ячеек через awk с разделителем-чертой даёт PD-99 (11 ячеек) и PD-197 (10), а counts.py --check печатает битая форма: []. Обе строки допаковые, обе сломаны греп-альтернацией и регекспом в тексте. ⚠ Что СМЯГЧАЕТ и почему это info, а не выше: col() считает колонки С КОНЦА, поэтому статус и вес таких строк читаются ВЕРНО, и сегодняшние числа зоны не врут — пере-проверено, PD-99 и PD-197 попадают в свои корзины правильно. Дыра латентная: лишняя черта в одной из ТРЁХ последних колонок сдвинет уже их, и гейт снова промолчит. ⚠⚠ ВТОРАЯ ПОЛОВИНА ПОСТРОЕНА 27.08 (оркестратор №19), смягчение строки ОПРОВЕРГНУТО. Довод «читаем с конца, значит лишняя черта безвредна» неверен: он защищает СТАТУС (третья с конца), но не ВЕС — тот читался col(l,6), то есть тоже с конца, а от конца он далеко, и любая лишняя черта в «сути» сдвигала его молча. Живой пример — сама PD-197: вес читался из ячейки «где». Эмпирика, принесённая зоной: три случая за двое суток, все у тех, кто в этот момент про этот класс ПИСАЛ (правка PD-396 · первая редакция этой строки · пере-формулировка её приёмкой), и во всех трёх «битая форма» оставалась пустой. Построено три вещи, каждая проверена исполнением: (1) парсер уважает markdown-экранирование — PD-99 несла корректно экранированные черты, и ломался парсер, а не строка; (2) ВЕС регистра читается с НАЧАЛА (cells(l)[3]), потому что ведущие ячейки коротки и черт не несут, хвостовые тоже, а свободный текст живёт в СЕРЕДИНЕ — оба конца безопасны, середина нет; (3) новый гейт tail_vocab судит СЛОВАРЬ хвоста (статус и вес), а не счёт ячеек, с одним грандфазерным исключением PD-59. Проверено подсадкой: черта в статусную ячейку краснит гейт немедленно; на чистом дереве EXIT=0, числа не сдвинулись (398/96, 3/27/66). ⚠ Правка n != shape по-прежнему ОТКЛОНЕНА и теперь на точном основании: после уважения экранирования в бэклоге остаются пять строк с законной сырой чертой внутри инлайн-кода, и их хвост чист. ⚠ ЗАКРЫТА 27.08: пин появился. selftest_tail_vocab() в docs/scripts/counts.py гоняется на КАЖДОМ --check и несёт четыре утверждения — чистая строка молчит · сырая черта в СТАТУСНОЙ ячейке краснит · экранирование законно и лишней ячейкой не считается · черта в СЕРЕДИНЕ не сдвигает вес. Проверен ПОСАДКОЙ, а не заявлением: три мутации гейта (снять уважение к экранированию · вернуть вес на чтение с конца · обезвредить словарь статуса) — пин ловит все три, базовая линия молчит. ⚠ Первая редакция пина МОЛЧАЛА на второй мутации: утверждение про вес проверяло cells(), а мутация меняет то, чем пользуется register(), — пин смотрел не туда. Переписано на настоящий путь; называю, потому что пин, проверенный одним прогоном вместо посадки, — это ровно тот дефект, который эта строка и описывает | fixed(приёмка 27.08, дерево сессии) | ревью-пак P8-REVIEW (наткнулся при правке собственной строки, подтверждено редакторским аудитом) | | PD-399 | standards | minor | internal/pgstore/readmodel.go:330 bankCountsTx, internal/httpapi/reading_test.go:113 | GET /books/{bookId}/bank шлёт два поля, которых канон 0.5.0 больше НЕ объявляет. Минор снёс pending_decisions и complete из BankPage и EventBank вместе с пер-термной моделью, которую они обслуживали, а проекция платформы их по-прежнему кладёт на провод, и это НЕ нули: bankCountsTx считает proposed-строки, и её собственный комментарий это говорит. Клиент 0.5.0 лишние поля игнорирует по общему правилу канона, поэтому вес minor, но аллоулист-норма нарушена, а комментарий «the wire fields are the canon's» с этого минора ЛОЖЕН. ⚠ Присутствие полей ЗАПИНЕНО (reading_test.go:113), значит снятие — правка с пином, а не вычёркивание. ⚠ Найдено САМОЙ контрактной сессией и принесено пингом: промт (§3.2-бис) утверждал «поля навсегда нули», а нота D39.160 п.2 — «после сноса канон и деплой совпадут точно»; верно по ПУТЯМ, неверно по ПОЛЯМ. Ошибка оркестратора, исправлена эрратой ⚠ ЗАКРЫТА паком P9 (27.08): поля сняты со всех трёх носителейBankCounts/payload() (кадр EventBank), wireBankPage/listBank (провод), подзапрос к мёртвой bank_decisions ушёл из bankCountsTx; пин reading_test.go ПЕРЕПИСАН и теперь пинит ОТСУТСТВИЕ снятых полей на первой странице (TestTheBankAggregatesRideOnTheFirstPageOnly) | fixed(пак P9, дерево сессии) | пинг контрактной сессии при сдаче минора 27.08, пере-проверен приёмкой | | PD-166 | bug | info | internal/ingest/tail.go, internal/pgstore/sink.go Begin | chunker_version из хендшейка теряется навсегда, если краш пришёлся между двумя стейтментами Begin (привязка engine_run_id и запись версии — два отдельных автокоммита): при повторном чтении своего же hello тейлер видит, что поток уже привязан, и Begin больше не зовёт. Сегодня поле никем не читается (нужно для строки 100), поэтому info; закрывать — одной транзакцией в BeginПАК P8-REVIEW 24.08, рефутер: механизм, который строка описывает (крах между ДВУМЯ автокоммитами Begin), СЕГОДНЯ НЕДОСТИЖИМ: запись версии переехала из Begin в effect и идёт в ОДНОЙ транзакции с курсором (internal/pgstore/sink.go:105-127), а сам Begin объявлен legacy (sink.go:33-38) и для попыток этой сборки недостижим — engine_run_id присваивается в INSERT попытки и бэкфилится в RecordSpawn. Доказано исполнением: при уцелевшей привязке и нулевом курсоре хендшейк идёт в Apply, а не в Begin (Begin calls=0 Apply calls=1), то есть терять между двумя стейтментами нечего. Посадка мутации (вырезан case ingest.TypeHello из effect) роняет TestTheChunkerVersionOfTheStreamReachesTheBook — атомарный писатель есть и запинен. Правильная диспозиция — ЗАКРЫТЬ как построенное: атомарность, которой строка требовала, существует. Читателя у колонки по-прежнему нет, и это отдельный факт, а не этот дефект ⚠ ЗАКРЫТА актом лендинга P9 (D39.162): акт объявил закрытие состоявшимся; статус переведён по аудиту документации 28.08 | fixed(акт D39.162, перевод по аудиту 28.08) | самопроверка дофикса (ревью вне карты) · аудит документации 28.08 | | PD-254 | standards | info | internal/httpapi/problem.go WriteStatusProblem, codeForStatus | Два писателя ошибок на одну форму тела. WriteProblem выводит статус ИЗ кода (пара не может разойтись), а WriteStatusProblem идёт обратно — от статуса к коду — потому что вне версионного префикса (/auth, /readyz) есть статусы, которых в словаре контракта нет вовсе (405, 429). Свести в один писатель можно только назначив форму ответа для этих двух статусов, а это ратификация, не правка зоны. Пока — два пути и обратная функция рядом с прямой ⚠ ЗАКРЫТО решением контрактной сессии 17.08 (релей владельца): поверхность входа отвечает тем же конвертом и БЕЗ машинного кода — это ратифицированное решение, а не пробел («различать причины отказа клиент не может по замыслу», компаньон §2.14 с 0.2.3), а единственный осмысленный для пользователя случай 429 машинен без словаря, потому что лечение едет в Retry-After. Следствие для кода: писатель входа кода не эмитит, codeForStatus и writeRaw удалены — обратной функции, то есть второго источника истины, больше нет. Пин httpapi.TestTheSignInSurfaceAnswersTheSameEnvelopeWithoutAVersionedCode (шесть статусов). Отвергнуто с доводом: расширение ErrorCode (значения, недостижимые на описываемой им поверхности, ломают инвариант «код называет свой статус») и словарь в компаньоне (документ, не нормативный для формы, стал бы нормативным с чёрного хода) | fixed(P7, дерево сессии) | кросс-модельное ревью P7 (линза скоупа) | | PD-255 | doc | info | internal/pgstore/migrations/00016_read_surface.sql, internal/pgstore/readmodel.go | Плотность комментариев выше нормы зоны («одна-две строки почему», владелец 26.07): миграция 00016 — 250 строк, из них около половины проза; у readmodel.go многие символы несут абзацы. Часть прозы несущая (порядок delete/insert в двух таблицах — ровно то, на чём пак и споткнулся), часть — эссе. Подрезано самое тяжёлое; остальное — предмет решения владельца о норме, а не тихой правки ⚠ ЗАКРЫТО решением владельца 21.08: НОРМА СМЕНИЛАСЬ, и строка была открыта против снятой формулы. Счёт строк снят как негодный гейт — комментарий на три строки может быть нужен, на одну достаточен; режется ВОДА (пересказ решений, провенанс, изложение исследования вместо ссылки), а всё, что из одной функции НЕ выводится — порядок блокировок, инварианты между таблицами, цена забывания, вендор-квирк — остаётся, сколько бы строк ни заняло. Формулировка — docs/architecture/12-go-style-notes §1. Обе названные здесь прозы под новой нормой законны: порядок delete/insert между двумя таблицами из функции не выводится, а сам дефект пака это доказал. Тихой правки не было и не будет | fixed(решение владельца 21.08) | кросс-модельное ревью P7 (линза энтропии) | | PD-429 | bug | minor | internal/config/effective_test.go TestAConfiguredAmountIsNeverPrinted | Денежный тест краснел на совпадении с ЧАСАМИ, то есть на факте, которого не существует. Он искал суммы подстрокой во всём буфере slog.NewJSONHandler, а тот несёт time в RFC3339Nano — девять знаков дробной части. Замерено на 200 000 таймстемпов: 7.50 встречается в 205, 42500 в 5, 0.0425 в 3 — и это НА СТРОКУ, а прогон печатает по строке на настройку, поэтому совпадения приходят вспышками (все строки одного прогона делят одну секунду). Воспроизведено -count=3000: падение на 42500. ⚠ Утечки не было и нет: значение скрывается (config.go пишет valueAmount), и этот же тест двумя строками выше сам это и проверяет. ⚠ Почему это чинится, а не оставляется под запретом D39.121: запрет защищает тест, ловящий НАСТОЯЩИЙ дефект, — такой нельзя ослаблять ради зелени. Здесь дефектен САМ тест: ложно-положительное срабатывание на данных, к предмету не относящихся. Оставить как есть — худший исход: флейк в ДЕНЕЖНОМ тесте приучает читать красное как шум, и в день настоящей утечки красное не отличат. ⚠ Лечение НЕ ослабляет: каждая строка разбирается как JSON, поле time выбрасывается, поиск идёт по значениям и ключам всего остального (loggedFields), то есть проверка перестала зависеть от формата времени. ⚠ Честная оговорка, установленная ПОСАДКОЙ: охват при этом НЕ вырос — сырой поиск покрывал и msg, и посадка «сумма печатается в msg» валит ОБЕ формы. Выигрыш ровно один и он назван: убрано ложное срабатывание. Проверка: -count=5000 чисто (падало на 3000), посадка в msg — красная. ⚠ Родня, сегодня безопасная по причине, которую стоит знать: internal/runs/sweep_test.go ищет 1.000000/0.200000 в буфере slog.NewTextHandler, а у текстового обработчика дробная часть ТРИ знака (замерено: .437 против .437860545 у JSON), поэтому шестизначные хвосты там не совпадут никогда — переключение того теста на JSON-обработчик воскресит этот же дефект ⚠ ДОФИКС ПО ВНЕШНЕМУ РЕВЬЮ (29.08), два пункта, оба в коде самой починки: (1) helper звался ВНУТРИ цикла по искомым суммам, то есть буфер разбирался четырежды — вынесен наружу; (2) серьёзнее: пропуск ключа time стоял внутри РЕКУРСИВНОГО обхода, то есть вложенная группа с именем time получала тихое освобождение от денежной проверки. Это дыра, направленная не в ту сторону, в helper'е, чей смысл — что не освобождён никто; плоские строки делали обе версии неотличимыми, и так такая дыра доживает до смены формы. Теперь delete на ВЕРХНЕМ уровне, где поле и пишет обработчик, а обход безусловен. Запинено TestTheTimestampExemptionDoesNotReachInsideTheLine (синтетическая строка с вложенным time), посадка «вернуть пропуск в обход» — красная | fixed(пак P11, дофикс 29.08 + дофикс-2 по внешнему ревью — лендинг оркестратора; статус проставлен зоной) | охотник приёмки; механизм пере-проверен оркестратором №19 и независимо пере-замерен сессией P11; дофикс-2 — внешнее ревью | | PD-430 | hardening | minor, деньги | internal/pgstore/credits.go ReadAccount, пин internal/pgstore/readaccount_test.go | Три денежные цифры, которые ReadAccount возвращает, были ВЗАИМОЗАМЕНЯЕМЫ для всей батареи: перестановка целей Scan оставляла зелёными все 18 пакетов. Причина ровно в том, что делает эту функцию ценной: она — ЕДИНСТВЕННОЕ место, где кэш баланса и сумма леджера сравниваются, и все денежные тесты зоны утверждают, что они РАВНЫ. Поэтому единственное, чего ни один из них не видит, — это их обмен. ⚠ Мутант НЕ эквивалентен, доказано пробой на дрейфе (кэш 4.00 при леджере 1.00): чистый код отвечает Balance=4.000000 LedgerSum=1.000000, с перестановкой — 1.000000/4.000000, при полностью зелёной батарее. Цена условна, но нацелена: дрейф — ровно то состояние, ради выявления которого функция и существует, и именно в нём tmplatformctl balance --user печатал бы СУММУ ЛЕДЖЕРА под словом «баланс», а любое решение по Account.Balance принималось бы по леджеру вместо кэша; предупреждение о дрейфе при этом продолжало бы срабатывать (сравнение обмен переживает), то есть оператор получил бы верную тревогу при двух неверных числах. Класс — PD-394/PD-376: денежный путь, который верен, и верен лишь потому, что никто не опечатался. ⚠ Найдено не мной: сессия пака sqlc (textmachine-1b) замерила, что гейт TestEverySQLStatementParsesAgainstTheMigratedSchema видит только СТРОКУ SQL и никогда Go-сторону вызова — шесть её посадок (перестановки целей Scan, сломанная арность) выжили на полной батарее. Я пере-проверила это независимо на самом денежном из чтений зоны и подтвердила. ⚠⚠ Пак sqlc дыру НЕ расширяет и НЕ сужает её остаток — он убирает одну из двух осей. Типы её не ловили и до него: в HEAD Account.Balance, .Reserved и .LedgerSum — все три money.MicroUSD, так что компилятор молчал ровно так же, и дыра была того же размера. Что пак действительно меняет (замерено сессией textmachine-1b: переставила две денежные колонки в SELECT, регенерировала, Go не трогала — Scan переставился следом и значения остались в своих полях): осей дрейфа было ДВЕ — расхождение SELECT-списка с целями Scan и рукописная проекция имя-в-имя, — а стала ОДНА, потому что первые две теперь генерируются вместе и разойтись не могут. Остаётся ровно то, что закрывает этот пин. Пин проверен против ОБЕИХ реализаций: ловит перестановку и в рукописном Scan, и в генерённом пути (последнее пере-проверено самой textmachine-1b на её дереве, а не с моих слов) | fixed(пак P11, дофикс 29.08 — лендинг оркестратора; статус проставлен зоной) | сессия пака sqlc (textmachine-1b, §3.1 — что sqlc даёт сверх гейта); пере-проверено посадкой сессией P11 на денежном чтении |

Закрытые — эра P12 (3031.08: деньги двери банка · честность экрана · эпоха формы конвейера · снятие обхода --verify-bank)

Пак «долги под ногами»: семь открытых major и висящие обязательства зоны; итог по каждой живёт в её собственной строке ниже и здесь не пересказывается. Канон двинулся один раз: минор 0.9.0 под эпоху формы конвейера (вторая граница пересчёта chapters_done + непрозрачное поле Book.shape_epoch), ратифицирован оркестратором ДО стройки пункта.

Две строки ниже (PD-380, PD-44) паком P12 не чинились — они уже несли свой fixed(...) и лежали под заголовками «Открытые», то есть ровно класс, ради которого 29.08 заведена секция переноса. Двинулась только позиция.

ID Класс Серьёзность Где Суть Статус Источник
PD-425 bug major, деньги internal/httpapi/bank.go:149 (r.Context()), internal/runs/bank.go RecordBankMove Дверь банковских коррекций теряет пост-verb факт НАВСЕГДА при обрыве клиента, и следующая покупка платит за это холдом. Хендлер берёт r.Context(), и RecordBankMove пишется на нём же; клиент закрыл вкладку до ответа — контекст отменён, запись не легла, bank_moved_at остаётся NULL. Второго писателя у факта нет, свипа нет, то есть состояние вечное. Деньги дальше: re_pass отвечает 409, а ОБЫЧНЫЙ прогон допускается с resnapshot=false и убивается снапшот-гардом движка ПОСЛЕ взятия холда — холд взят, попытка потрачена впустую. ⚠ Средство, которое дверь называет, недостижимо по построению: она отвечает 503 со смыслом «клиент пере-шлёт», а запись падает именно потому, что клиента уже нет. ⚠ Шаблон лечения лежит В ЭТОЙ ЖЕ ЗОНЕ и здесь не применён: internal/pgstore/idempotency.go:150 и books.go:205,215 делают WithTimeout(WithoutCancel(ctx), …) ровно с этой мотивировкой. ⚠ ЗАКРЫТО пак P12 (3031.08). RecordBankMove идёт на СОБСТВЕННОМ контексте: context.WithTimeout(context.WithoutCancel(ctx), recordBudget) (internal/runs/bank.go, тот же класс, что internal/httpapi/idempotency.go:151 settleCtx и internal/books/parse.go:271 writeCtx). Детачится РОВНО запись и ничего больше — разбор шире: детач на двери снёс бы два намеренно построенных свойства (мёртвый клиент покидает очередь книги; бюджет verb'а), а детач самого verb'а не нужен, потому что ранний обрыв даёт SIGTERM → движок проверяет контекст ДО своих записей → exit 5 → ErrBankUnavailable, и не теряется ничего. recordBudget (10 с) а не 30-секундный writeBudget интейка: запись идёт ПОД блокировкой книги. Пин — runs.TestTheBankMoveFactSurvivesAClientThatHungUp (гейт entered/release держит verb в полёте, отмена приходит В ЭТОТ момент — контекст, отменённый ДО вызова, до записи не доходит вовсе, его съедает lockBook). Посадка M1 (снять WithoutCancel) — КРАСНАЯ адресно, соседний пин зелен. ⚠ Битый якорь строки исправлен: internal/pgstore/idempotency.go:150 — это чтение ключа, никакого detach там нет; истинные якоря класса — internal/httpapi/idempotency.go:151, internal/books/books.go:205,215 (тело internal/books/parse.go:271) и, чего строка не знала, internal/runs/reconcile.go:119,277ТОТ ЖЕ пакет. ⚠ Остаточное окно названо, а не закрыто: смерть ПРОЦЕССА между verb и записью (SIGKILL, OOM, истёкший grace деплоя) по-прежнему теряет факт навсегда — WithoutCancel переживает клиента, не процесс. Окно сузилось с «вся фаза записи движка плюс round-trip БД» до «round-trip БД». Свипа быть не может: после факта у платформы нет свидетеля свёрнутой коррекции, ровно поэтому дверь штампует из СОБСТВЕННОЙ квитанции. fixed(пак P12) приёмка оркестратора №19 по паку P11 (охотник вне карты)
PD-402 bug major internal/runs/reconcile.go:1221 Resume (switch по статусу, «последний ли» не спрашивается), internal/pgstore/readmodel.go:484 lastRun (order by started_at desc), internal/pgstore/runs.go RestartRun (started_at не пере-штампуется) Возобновление НЕ-последнего прогона книги ломает полосу и карточку сразу двумя путями. (1) Полоса возобновлённого открывается на ЧУЖОЙ работе: базы не пере-снимаются (это правильно для последнего прогона), но между стопом и resume по книге легально прошёл ЦЕЛЫЙ другой прогон, и оба числителя уже накрутили его главы — на нажатие «продолжить» пользователь видит 100% при нуле собственной работы, клэмп режет только превышение единицы, не двойной счёт до неё. (2) lastRun выбирает строку по started_at desc, а RestartRun started_at не трогает — карточка книги, derivedStatus и КАЖДЫЙ progress-кадр живого прогона считаются по ЧУЖОМУ финишировавшему: экран показывает ready и замороженный бар, пока живой прогон тратит деньги невидимо. Найдено воркфлоу-ревью и подтверждено его overlay-прогонами (4 независимые линзы сошлись на одном корне); ответ на сам resume (ReadRun по id) при этом честный — клиент получает от одной ревизии книги два разных бара. ⚠ ЗАКРЫТА read-половина пак P12 (3031.08) (write-половина закрыта F12 ранее). Порядок «живой прогон ПЕРВЫМ» вынесен в одну константу newestRun = order by (finished_at is null) desc, started_at desc, id desc limit 1 (internal/pgstore/readmodel.go) и вклеен во ВСЕ 10 мест склейки lastRun — плюс, и это половина, которой лечение read-модели не сделало бы: ТОТ ЖЕ порядок вклеен в LatestRun (internal/pgstore/runs.go), где order by started_at desc был выписан РУКОЙ. Оставить его — значило ПЕРЕВЕРНУТЬ дефект: экран пошёл бы за живым прогоном, а Resume отказал бы ему как «не последнему». Тотальность порядка обеспечивает runs_one_live_per_book (00002:66, уникальный по book_id при finished_at is null) — живой ровно один. Пины судят ПО МЕСТАМ, не зеленью батареи: pgstore.TestALiveRunOutranksAFinishedOneOnEveryBookScopedRead (карточка, её Run, строка библиотеки, LatestRun), pgstore.TestTheStreamsFramesFollowTheLiveRunAndNotTheFinishedOne (кадр статуса, bookScope, полоса) и pgstore.TestWithNoLiveRunTheBookStillResolvesToTheNewestStarted — обычный случай не тронут. Посадки M6 (снять живой терм) и M7 (расклеить LatestRun) — обе КРАСНЫЕ адресно. ⚠ Цена, названная честно: индекс runs_book_idx (book_id, started_at desc) этот порядок больше не обслуживает целиком — сортировка идёт по выражению. Латераль работает по ОДНОЙ книге, где прогонов единицы, так что замера это не потребовало; если у книги когда-нибудь станут сотни прогонов, носитель — новый индекс, а не откат порядка. fixed(пак P12) воркфлоу-ревью P9 28.08 (линзы bar:monotonicity · bar:double-count · bar:baselines-x-migration · bar:stage-caption), диспозиция оркестратора 28.08: строкой
PD-403 bug major internal/pgstore/sink.go:415 recordWaveShape (edit_wave только растёт), internal/pgstore/readmodel.go:388 finishedUnits, internal/pgstore/books.go ReadBookForRun.ChaptersLeft Книга с уже сложившимся edit_wave=true на деплое, где оператор УБРАЛ редактора, продаёт уже переведённые главы повторно — и ни один прогон больше не доводит полосу до единицы. Безредакторный движок шлёт editTotal=0, но флаг не возвращается (or $2 пишет только false→true); прогон делает 100% купленного черновика и закрывается ready на 50% со stage editing; chaptersDone (edit-колонка) мёрзнет, ChaptersLeft не падает — шкала покупки предлагает те же главы снова (симптом PD-202, записанной fixed этим самым флагом), а follow-up прогон над этой покупкой читает 0/C с первой секунды. Комментарий sink.go:406-409 о собственном коде неверен обеими фразами (запрещённое направление называет разрешённым, «shows up as more chapters finished» не случается). Подтверждено overlay-прогонами воркфлоу. Корень продуктовый — смена формы конвейера на живой книге. ⚠ ЗАКРЫТА пак P12 (3031.08), по решению владельца D39.165 §2. Носитель границы — НОВАЯ пара колонок books.shape_epoch + books.epoch_editor (миграция 00030): форма текущей эпохи ПРИСВАИВАЕТСЯ, счётчик считает пересечения. books.edit_wave остаётся и остаётся МОНОТОННЫМ — пин D39.153 §4б не тронут; уехал только пожизненный счёт, finishedUnits читает epochWave = coalesce(b.epoch_editor, b.edit_wave, true). Движение structure_version отвергнуто: оно инвалидирует курсоры, идентификаторы пар и окна термов, а нарезка книги не менялась — это была бы ложь о структуре ради счётчика. ⚠ ЗАКРЫТА ОДНА половина из двух, и вторая ОСТАЁТСЯ ОТКРЫТОЙ — см. PD-435. Закрыт СЧЁТ КНИГИ: шкала покупки перестаёт продавать переведённое, ChaptersLeft падает, книга дочитывается. НЕ закрыта ПОЛОСА ПРОГОНА: на деплое, где редактора убрали, она по-прежнему тарифицирует edit-волну, которой не будет, и до единицы не доходит. ⚠ Полоса прогона ОСТАВЛЕНА на МОНОТОННОМ books.edit_wave — канон держит её одной монотонной дробью (строка 200), а эпоха ПРИСВАИВАЕТСЯ и ходит в обе стороны; попытка перевести полосу на эпоху была откачена этим же паком по замеру (PD-435). На эпоху уехал только пожизненный счёт книги. PD-401 не вскрыт (парность числителя и базлайна не тронута) и теперь запинен с той стороны тоже: pgstore.TestTheRunsBarIsMonotoneAcrossAShapeBoundaryItSpans. Ложный абзац sink.go о собственном коде СНЕСЁН целиком, не пере-нацелен. Канон — минор 0.9.0 (ратификация оркестратора 30.08): фраза chapters_done называет ВТОРУЮ границу пересчёта рядом с пере-нарезкой, и появилось поле Book.shape_epoch — НЕПРОЗРАЧНЫЙ счётчик поколения; сама форма («есть ли редактор») на провод НЕ выносится, это было бы третьим исключением границы, которого D39.163 не даёт. STACK_DECISIONS §35 пере-подписан, прежняя формулировка оставлена с датой и причиной. Пины: pgstore.TestRemovingTheEditorRecomputesTheBooksCountInsteadOfFreezingIt и pgstore.TestTheEpochMovesOnBoundariesAndNotOnEveryAnnouncement. Посадки M8 (счёт обратно на монотонный флаг) и M9 (эпоха накапливает вместо присвоения) — обе КРАСНЫЕ адресно. fixed(пак P12) воркфлоу-ревью P9 28.08 (линзы bar:stage-caption · bar:double-count · bar:monotonicity), диспозиция оркестратора 28.08: строкой
PD-404 bug major internal/pgstore/sink.go:415 recordWaveShape, internal/pgstore/readmodel.go:388 finishedUnits (живой флаг), потребители bookColumns.chaptersDone и ReadBookForRun.ChaptersLeft Единственное разрешённое движение флага (false→true, оператор ДОБАВИЛ редактора) уводит Book.chapters_done НАЗАД в пределах одного structure_version — то, что канон запрещает прямо. Книга с начерченным заделом: карточка 7/10; первое progress-событие прогона с редактором переворачивает флаг, finishedUnits переезжает на edit-колонку — 7 → 0 одной транзакцией при РАСТУЩЕЙ ревизии (клиент обязан отрисовать спад), ChaptersLeft раздувается обратно и шкала снова предлагает купить купленное. Класс PD-316 (записана fixed) на непокрытом триггере: её пин ловит только admission, а не движение самого флага. Крайний случай той же арифметики: книга, начерченная ПОЛНОСТЬЮ под false, редактором НЕдочитываема ни одним действием API — ChaptersLeft=0 не допускает прогон, а флип случается только в progress-событии допущенного прогона. Подтверждено воркфлоу на in-tree фикстуре (sink_test.go:701) ⚠ ЗАКРЫТА пак P12 (3031.08) ТЕМ ЖЕ носителем, что PD-403 — это одна арифметика с двух сторон, и закрывать её порознь значило бы оставить строку, чья репродукция больше не описывает дефект. Разрешённое движение флага (редактора ДОБАВИЛИ) по-прежнему пересчитывает счёт — так решил владелец, — но теперь пересчёт ОБЪЯВЛЯЕТ СЕБЯ: books.shape_epoch двигается той же транзакцией, и клиент видит новое поколение, а не ход счётчика назад. Крайний случай строки (книга, начерченная ПОЛНОСТЬЮ под false, недочитываемая редактором ни одним действием API) уходит с ней же: ChaptersLeft считается через эпоху, то есть после прихода редактора книга снова имеет что купить. Пин — pgstore.TestAddingTheEditorMovesTheEpochWithTheCountItRecomputes; посадка M9 КРАСНАЯ адресно. fixed(пак P12) воркфлоу-ревью P9 28.08 (линзы race:flip-read-consistency · bar:stage-caption · bar:baselines-x-migration), диспозиция оркестратора 28.08: строкой
PD-405 bug major internal/pgstore/sink.go:219 (ложный комментарий-обоснование), internal/books/parse.go (инлайн-материализация после коммита not_started), internal/pgstore/books.go BooksOwedReadModel (nothingIsRunning) Прогон над книгой без материализованного дерева читает 0/total ВСЮ ЖИЗНЬ при исправно доезжающих progress-событиях — а комментарий, которым этот случай объявлен закрытым, описывает архитектуру, которой больше нет. Окно штатное: интейк коммитит not_started ДО материализации; если она упала/отложилась и пользователь успел стартовать прогон, долг дерева заморожен до конца прогона (nothingIsRunning), unitDone находит 0 строк глав и выходит, вся полоса выведена из chapters → 0/total до ready. Комментарий sink.go:219 «the counters the screen reads today come from the progress event» ложен — эти колонки не читает никто (носитель — PD-411). Достижимость ЗАМЕРЕНА логом живого стенда: на здоровом пути окно «not_started без строк chapters» живёт 0.018 с (обе загрузки пробоя P9: book parsedthe reading surface was refreshed, 0.0179 и 0.0189 с) — опасная форма только УПАВШАЯ/отложенная материализация, и тогда длина окна = вся жизнь прогона. Лечение — честная привязка полосы к бездеревному окну (отказ старта до дерева ИЛИ полоса из progress-событий как раньше); замер достижимости ратифицирован актом D39.162 ⚠ ЗАКРЫТА пак P12 (3031.08), формой «отказ старта до дерева» (из двух названных зоной лечений). Довод против второго — полосы из progress-событий: те колонки НИКТО не читает (PD-411), то есть «как раньше» означало бы построить полосу заново поверх мёртвого носителя; а разделение nothingIsRunning тронуло бы конъюнкт, общий с ReadStream и лизинг-дисциплиной §34. Отказ стоит ДО взятия холда: internal/runs/runs.go Start, предикат book.ChapterCount > 0 && !book.HasTree (новое поле BookRunContext.HasTree, exists(select 1 from chapters …)). Слово на проводе — существующее book_not_ready (409), НЕ новое: собственная глосса канона говорит «still arriving, still being CUT, or was rejected», а книга, должная дерево, ещё нарезается. Ложный комментарий sink.go («the counters the screen reads today come from the progress event») СНЕСЁН — он и был прикрытием этой строки. ⚠ Что вскрылось при этом и названо строкой: ВСЯ runs-батарея ездила на книге, объявляющей главы и не материализовавшей ни одной, — фикстуры РАСШИРЕНЫ деревом (newFixture, secondBook), не обойдены. Пин — runs.TestARunIsRefusedOverABookWhoseChaptersWereNeverMaterialised (проверяет и то, что отказ НЕ ДВИНУЛ ДЕНЬГИ) и runs.TestABookThatDeclaresNoChaptersIsNotRefusedAsUnmaterialised (книга, объявившая НОЛЬ глав, под гард не попадает — её отказывает то, что должно: покупать нечего); посадки M10 и M19 КРАСНЫЕ адресно. ⚠ Названо адверсариальным проходом пака и подписано в коде, а не замолчано: одна популяция этим отказом становится НЕзапускаемой до действия оператора — книга, чей долг читательской поверхности СПИСАН после исчерпания попыток (AbandonReadModelDebt): она никому ничего не должна, свип её не подберёт, и отказ стоит, пока не попросят заново. Ручка существует и названа прямо в комментарии гарда — tmplatformctl book refresh --book <id>, список — tmplatformctl books --abandoned. До пака такая книга запускалась и показывала мёртвую полосу; теперь она отказывает и указывает на лечение. fixed(пак P12) воркфлоу-ревью P9 28.08 (линзы bar:stage-caption · bar:double-count), диспозиция оркестратора 28.08: строкой, достижимость замером (акт D39.162)
PD-411 standards minor internal/pgstore/sink.go:137 и :447 (два писателя), потребителей НЕТ (греп: ни одного select) runs.draft_done/draft_total/edit_done/edit_total — носитель с ДВУМЯ писателями и НУЛЁМ читателей: класс A второго яруса онтологии банка (18-bank-ontology.md), сосед PD-314 по форме. Полоса целиком выведена из chapters (runDone/runTotal), эти колонки не читает ни один SELECT — а пишутся они на КАЖДОМ progress-событии внутри транзакции, держащей блокировку строки книги, двумя РАЗНЫМИ дисциплинами (поток присваивает, resync берёт greatest), и обоснование greatest на sink.go:444-446 защищает полосу, которой не существует, — следующая сессия будет искать монотонность бара не там, где он живёт. ⚠ ЗАКРЫТА пак P12 (3031.08) (запрет сноса в P9 был пер-паковым — слово оркестратора 28.08): колонки снесены миграцией 00031, оба писателя и оба лгущих комментария удалены. Четыре теста, наблюдавшие свойства ЧЕРЕЗ эти колонки, пере-нацелены на то, что progress-событие materialises на самом деле — ETA и объявленную ФОРМУ; это не ослабление, а восстановление предмета: у утверждения над снесённой колонкой предмета нет. ⚠ СНОС ВСКРЫЛ ГАП, и он заведён отдельной строкой, а не замолчан: РЕСИНК — канал починки для прогона, чей поток в карантине, — писал прогресс ТОЛЬКО в эти колонки. Он не трогает ни unit_resolutions, ни chapters, значит не материализует ничего, что читает экран, и полоса такого прогона стоит всю его жизнь, как бы исправно ни отвечал tmctl status. Мёртвые колонки это ПРЯТАЛИ: код выглядел так, будто канал починки чинит. fixed(пак P12) воркфлоу-ревью P9 28.08 (линзы race:flip-read-consistency · bar:double-count · bar:stage-caption), диспозиция оркестратора 28.08: отдельной строкой класса A
PD-369 bug minor internal/pgstore/idempotency.go ClaimIdempotency, claimRounds Легитимный запрос получает 500 под конкуренцией на одном ключе идемпотентности. Ретрай проигранной гонки ограничен claimRounds = 3, а восемь одновременных попыток одного ключа, каждая из которых сразу отдаёт его назад (как делает любой 4xx), могут отобрать гонку у одного и того же проигравшего трижды подряд — и он получает не один из четырёх контрактных ответов, а внутреннюю ошибку. Замерено: 12 падения на ~80 прогонов собственного теста TestAClaimThatLostARaceToAReleaseIsRetriedAndNotAnError (то есть тест ФЛЕЙКОВЫЙ, и это сам сигнал). Комментарий над константой признаёт границу («a caller that loses three rounds is meeting something other than a race») — но восемь racer'ов и есть гонка, а не «что-то другое». ⚠ Направление, не решение: либо граница по ВРЕМЕНИ вместо числа раундов, либо ответ ErrKeyInFlight вместо 500 при исчерпании ⚠ ЗАКРЫТА пак P12 (3031.08), формой ErrKeyInFlight (из двух названных зоной). Довод против границы по ВРЕМЕНИ — она дефект не закрывает: чем бы ни ограничивался цикл, он обязан чем-то ЗАВЕРШИТЬСЯ, и пока терминал — голый fmt.Errorf, это по-прежнему 500, просто реже; две «формы» лежат на разных осях — ответ обязателен, граница свободна. Плюс собственная часовая дисциплина файла: всё время в ClaimIdempotency берётся из ИНЖЕКТИРУЕМОГО now, а дедлайн по стенным часам внутри цикла ввёл бы второй, скрытый источник времени, непроверяемый под замороженными часами. Сделано: pgstore.ErrKeyContended ОБОРАЧИВАЕТ ErrKeyInFlight (лекарство у вызывающего то же — подождать Retry-After и повторить), словарь кодов контракта НЕ расширен — idempotency_conflict/key_in_flight уже есть; на HTTP один писатель ответа (keyInFlight), случай контенции стоит ПЕРЕД более широким и пишет WARN: громко у нас, тихо на проводе. ⚠ Пин НЕ ПРАВЛЕН (D39.121): TestAClaimThatLostARaceToAReleaseIsRetriedAndNotAnError принимал ErrKeyInFlight и по построению принимает обёртку. Новые пины: pgstore.TestGivingUpOnAContendedKeyIsOneOfTheContractsOwnAnswers и подтест «a claim that lost the race too often is told to wait, not answered 500». Пере-замер серией ПОСЛЕ фикса: go test ./internal/pgstore -run '^TestAClaimThatLostARaceToAReleaseIsRetriedAndNotAnError$' -count=8080 PASS, 0 FAIL, 0 SKIP (счёт по ^--- PASS, а не по коду выхода: all-skip читался бы как зелень). fixed(пак P12) попутная находка сессии P8-FIX (флейк батареи), передано оркестратору
PD-431 hardening minor internal/pgstore/sessions.go:49 Touch, internal/pgstore/queries/sessions.sql TouchSession, пин отсутствует Touch выбрасывает RowsAffected, поэтому запрос, не обновивший НИ ОДНОЙ строки, неотличим от успеха — и батарея остаётся зелёной. Замерено адверсариальным проходом пака sqlc 29.08: where token_sha256 = $1where 1 = 0 and token_sha256 = $1, апдейт не трогает ничего — 18 пакетов, EXIT=0, ноль красных. Контроль на том же стенде (обезвреженный delete from reservations в DeleteBook) даёт две красных, то есть харнесс работает и молчание относится к Touch, а не к прогону. Продуктовый смысл: скольжение окна бездействия — то, что держит активного пользователя в сессии, — не проверяет ни один живой тест; два его вызова (pg_test.go:115, TestTouchCannotResurrectAnIdleExpiredSession) решаются предикатами самого SQL и остаются истинными, если Touch не делает ничего. ⚠ Класс, который ПОКРЫТИЕМ не ловится в принципе: для Exec, чей тег отброшен, «оператор исполнился» — это всё, что покрытие когда-либо докажет; нужен мутационный проход по ЗАПИСЯМ. Именно поэтому пак sqlc, считавший непокрытые строки, нашёл три опасных оператора, а этот — четвёртый — не нашёл. ⚠ ЗАКРЫТА пак P12 (3031.08), ЧЕСТНОСТЬЮ ПРОВЕРКИ — поведение НЕ менялось (слово оркестратора: ноль строк — законная гонка с отзывом). Новый сквозной пин pgstore.TestTheIdleWindowSlidesThroughTheAuthenticatorAgainstALiveStore: живой *pgstore.Store подставлен auth.Authenticator как его же auth.SessionStore — интерфейс он удовлетворял всегда, production не менялся, не хватало теста, который соединит два конца. Живёт в пакете pgstore, потому что pgstore импортирует auth, а не наоборот: тот же тест в auth — цикл импорта. Утверждение идёт через ПОЗДНИЙ Lookup за первоначальным дедлайном, а не через код ответа: ошибка Touch логируется и проглатывается, так что 200 не доказывает ничего. Второе утверждение — что скользнули ПРАВИЛЬНЫЕ часы (абсолютный потолок не двинулся). Посадка M12 — ИМЕННО та, которой строка заведена (where 1 = 0 and token_sha256 = $1 в TouchSession, в исходнике и в генерённой константе): новый тест КРАСНЫЙ адресно, оба прежних Touch-теста ЗЕЛЁНЫЕ — то есть дельта ровно та, о которой строка. Добавлен второй пин на отзыв (TestARevokedSessionIsDeniedThroughTheAuthenticatorAgainstALiveStore). fixed(пак P12) адверсариальный проход пака sqlc (П-19), 29.08
PD-432 doc minor docs/STACK_DECISIONS.md §«Гейты батареи» (рецепт стенда), ~/.local/bin/tmctl Рецепт второго гейта батареи не говорит, что движковый бинарь надо ПЕРЕСОБРАТЬ, и устаревший бинарь даёт КРАСНУЮ батарею, которая читается как дефект кода зоны. Замерено 29.08 паком sqlc: tmctl стенда от 24.08 против сегодняшнего backend/configs/models.yaml — три красных теста в internal/books и internal/runner, сообщение tmctl: config: parse …/models.yaml: yaml: unmarshal errors: line 137: field system_messages not found in type config.CapabilitiesConfig. Диагноз стоит времени именно потому, что выглядит как ошибка платформы: падает платформенный тест, а лжёт бинарь движка, собранный до того, как в конфиг движка приехало поле. ⚠ Класс тот же, что у PD-423: условие батареи, которое рецепт называет неполно, и следующая сессия ищет дефект в своём диффе. ⚠ ЗАКРЫТА пак P12 (3031.08): в рецепт «Гейты батареи» дописано, что движковый бинарь второго гейта обязан быть СОБРАН из текущего backend/, с симптомом и командой. ⚠ Той же правкой снята ОПРОВЕРГНУТАЯ команда-проверка четвёртого условия (cut -d: -f3 /proc/self/cgroup/init.scope): к двум точкам PD-423 пак добавил ТРЕТЬЮ — у оболочки этой сессии команда даёт /, а TestARunIsBoundedByItsOwnCgroup при этом зелен в полной батарее со скипами 0. Рецепт теперь велит судить по САМОМУ тесту; кандидат-замена (cgroup.subtree_control целевого среза) назван КАНДИДАТОМ и в рецепт не внесён — диагноз PD-423 не установлен. ⚠ Тем же классом найдено и починено на стенде: КОНФИГИ движка тоже протухают — стендовая копия backend/configs не имела langpacks/ru, появившегося позже; правило дописано в рецепт рядом с бинарём. fixed(пак P12) пак sqlc (П-19), 29.08, найдено первым же прогоном полной батареи
PD-367 bug minor internal/books/parse.go:129, internal/ingest/manifest.go Whole Пол на самосогласованность манифеста стоит только у материализатора, а интейк тот же документ ПРИНИМАЕТ. Манифест {ChaptersTotal: 120, UnitsTotal: 400} с пустым списком глав Whole() отвергает, а books.Parse заводит книгу not_started с chapter_count=120 и пустым деревом — по такой книге можно СТАРТОВАТЬ и ОПЛАТИТЬ прогон (потолок считается от chapter_count). Воспроизведено ревью на живом Postgres. ⚠ ЗАКРЫТА пак P12 (3031.08) по решению оркестратора: пол самосогласованности СИММЕТРИЧЕН, интейк отказывает. ingest.Manifest.Readable() — ВТОРОЙ предикат рядом с Whole(), не расширение DecodeManifest (у того четыре пина намеренно декодируют частичные и чужие документы, и его собственная дока объявляет правило «значение не гейтим» решением). Стоит ПЕРВЫМ в books.Parse, до ветвей по ChaptersTotal: ниже этой черты документ, прочитанный неверно, неотличим от книги, в которой ничего нет, а ЭТО чтение удаляет аплоад. Класс — parser_unavailable: бюджет попыток тратится, файл остаётся. ⚠ ПРАВКА ЗАПИНЕННОГО КОНТРАКТА, заказанная промтом P12 §3.8 — не подгонка под зелень: батарея интейка ездила на документах без списка глав, и фикстуры РАСШИРЕНЫ (wholeManifest), а не обойдены; поимённо пере-подписаны фикстуры books_test.newFixture и два манифеста render_test. Пин — books.TestTheIntakeRefusesTheDocumentItsOwnMaterialiserWouldReject (ровно документ строки: 120 глав, 400 пар, пустой список; проверяет и что chapter_count НЕ записан, и что файл цел); посадка M15 КРАСНАЯ адресно. fixed(пак P12) воркфлоу-ревью волны 2 (P8-FIX); рефутер подтвердил механику и опроверг предложенное лекарство
PD-213 hardening info internal/ingest/manifest.go, internal/books/parse.go Форма манифеста не версионируется на стороне платформы — латентная мина на УДАЛЕНИЕ файла. DecodeManifest не сверяет manifest_version ни с чем; json.Unmarshal тихо игнорирует незнакомые поля и оставляет отсутствующие нулями, поэтому смена формы движком (tm-manifest-v2 → v3, переименование chapters_total) даст валидный разбор с ChaptersTotal = 0. А ноль глав интейк трактует как «источник прочли, книги нет» — тот же терминал и тот же бюджет, что exit 11, то есть после пяти попыток файл пользователя УДАЛЯЕТСЯ, хотя движок книгу прекрасно разобрал. Сегодня формы совпадают поле-в-поле, так что не эксплуатируется; в отличие от потока (мажор отвергается) и от полосы кодов (незнакомый номер безопасен), у манифеста аналога нет. Лечить сверкой manifest_version с известной, где незнакомая версия даёт НЕ-деструктивный класс ⚠ ЗАКРЫТА пак P12 (3031.08) тем же классом «незнакомое → не-деструктивно», как и предписывала строка. ingest.KnownManifestVersion (tm-manifest-v2, зеркало backend/internal/pipeline/manifest.go:46) сверяется в Readable() перед всем остальным, и незнакомая версия даёт parser_unavailable — файл цел. Это ФОРМЕННЫЙ гейт, а не пин значения: сама строка и дока поля правы, что пиннинг значения везде сделал бы каждый релиз движка релизом платформы, поэтому версия сверяется РОВНО в одном месте — там, где неверное чтение разрушительно. Пин — books.TestAManifestShapeThisBuildDoesNotKnowNeverDeletesTheUpload: v3-манифест переживает весь бюджет попыток с целым файлом, а ЗНАКОМАЯ форма с честно нулевыми счётчиками по-прежнему удаляет каталог (иначе гейт съел бы настоящий вердикт о тексте пользователя). Посадка M14 (снять сверку версии) КРАСНАЯ адресно. fixed(пак P12) адверсариальное ревью P6 (линза шва)
PD-427 doc minor internal/ingest/resync.go:37-43 (аллоулист StatusReport), опровергнуто backend/internal/pipeline/status.go projectStoredMemory (лендинг 6ec9f8a, D39.170) Комментарий несёт ПОСЫЛКУ, которую сняли, и читается как действующий довод. Он объясняет, почему платформа сознательно НЕ берёт rebill_units/rebill_usd через шов: «status проецирует СОХРАНЁННУЮ память, и сразу после bank-apply — в единственный момент, когда согласие хотело бы цифру, — он честно читает ноль». Это было верно и ратифицировано (эррата 28.08-к). Движковый пак «деньги» починил ровно это: foldMemoryForRead стал ПЕРВЫМ ответом читающего пути, а projectStoredMemory понижена до фолбэка, и комментарий движка объявляет это дословно — «IT IS NO LONGER THE READ PATH'S FIRST ANSWER». Слепое окно закрыто, status отвечает «сколько будет стоить» ДО покупки, оставаясь $0-глаголом без записи. ⚠ Комментарий неверен ДВАЖДЫ: не только посылка, но и предсказанное лечение — он обещает, что «пара вернётся с движковым ГЛАГОЛОМ, который умеет свернуть и оценить коррекцию ВНЕ прогона», а нового глагола не появилось: починили существующий status. ⚠ ПРОВОДКУ ПОЛЕЙ ЭТА СТРОКА НЕ ОТКРЫВАЕТ (слово оркестратора при передаче): она гейчена вместе с tmctl translate --max-units, и тот гейт в силе — движковый потолок объёма на майнящей банк книге пробивался, лечение легло, но проводка ждёт отдельного решения. То есть предмет строки — ровно устаревший ДОВОД, а не отсутствие полей. Класс — «указатель пережил то, на что указывал», тот же, что PD-310/PD-326/PD-366, только в прозе шва. Зеркалит строку 234 единого бэклога ⚠ ЗАКРЫТА пак P12 (3031.08) — акт закрытия, не работа: комментарий internal/ingest/resync.go уже исправлен 29.08 аудитом доков, снятая посылка из него ушла, предсказание про «новый движковый глагол» тоже. Проверено чтением обеих сторон. Проводку полей строка не открывала и не открывает — гейт --max-units в силе. fixed(пак P12, акт закрытия) оркестратор №19 при лендинге движкового пака (6ec9f8a), проверено чтением обеих сторон сессией P11
PD-203 bug info internal/pgstore/books.go ReadUsage Аккаунт объявляется исчерпанным по паузе ОДНОГО прогона. /usage ставит paused_reason аккаунта, если у какой-нибудь книги последний прогон стоит paused/credit_exhaustedа это потолок ПРОГОНА (сколько глав купил пользователь), а не баланс: на аккаунте может лежать сколько угодно денег, и другой прогон стартует. Контракт про это поле говорит «Set when the account itself is in a halted state». Существовало до этого пака и не им создано; отдельной строкой, потому что различение потолков (PD-199) сделало вопрос «чей это потолок» отвечаемым ⚠ СУЖЕНО P7: Usage.halt_reason получил СВОЙ словарь (AccountHaltReason), и PD-241 убрал самый частый ложный источник — стоп пользователя, приезжавший credit_exhaustedПАК P8-REVIEW 24.08 ПРЕДЛОЖИЛ ЗАКРЫТЬ, проверив предикат по коду: internal/pgstore/books.go ReadUsage читает БАЛАНС аккаунта, а не сканирует книги (platform/internal/pgstore/books.go:1167=case balance <= 0:), а ⚠-комментарий рядом прямо описывает замену (platform/internal/pgstore/books.go:1136=It used to be read off the); единственный писатель Usage.PausedReason — эта же строка (греп PausedCreditExhausted по internal/pgstore/books.go), приехало 9b23e8cЗАКРЫТА пак P12 (3031.08): предикат пере-проверен и предложение P8-REVIEW подтверждено. internal/pgstore/books.go ReadUsage читает БАЛАНС аккаунта и ставит аккаунтную причину только при balance <= 0; книги не сканируются. Канон на той же стороне: AccountHaltReason — «a state of the account, not of a run», отдельный словарь ровно затем, чтобы прогонная причина не зажигала аккаунтный флаг. То есть дефект строки не воспроизводится, и это закрытие, а не пере-открытие. fixed(пак P12) сессия P6 (самопроверка вокруг PD-199)
PD-380 hardening minor internal/pgstore/sessions.go:96=AbsoluteExpiresAt: now.Add(maxAge),, internal/config/config_test.go Абсолютный потолок сессии не запинен в единственном месте, где он становится фактом в базе. CreateSession — единственный писатель sessions.absolute_expires_at, и мутация этого выражения проходит ПОЛНЫЙ пакет pgstore: ни один сессионный тест не краснеет. Пин, который STACK_DECISIONS §13 называет носителем потолка, смотрит только на результат config.Load() (что значение конфигурации не выше ASVS-предела), то есть проверяет НАСТРОЙКУ, а не то, что она доезжает до строки. Родня PD-86, но на шаг раньше: там не запинены клаузы ЧТЕНИЯ и потолок держится транзитивно через Touch, здесь не запинена сама ЗАПИСЬ, а транзитивной страховки у неё нет. Воспроизведение: docs/p8-review/axis2-auth/mutations-axis2.shПере-проверено на ПОЛНОЙ батарее координатором пака (мутация M12): now.Add(maxAge)now.Add(100*maxAge) в CreateSession, дельта против чистой копии ПУСТА на всех 18 пакетах. ⚠ ВЕС ПОДНЯТ info → minor закрывающим ревью, довод — симметрия с PD-375: форма идентична (единственная точка принуждения объявленной границы не исполняется ни одним тестом, мутация переживает ПОЛНУЮ батарею, транзитивной страховки нет — Touch зажимает по значению ИЗ ТОЙ ЖЕ испорченной строки), а вес расходился только по валюте: там деньги и minor, здесь механизм ASVS 7.3.2 уровня 2 при объявленной зоной базовой линии L2 и info. Асимметрия была отпечатком того самого храповика «вреда сегодня нет», который это ревью и нашло ⚠ Якорь пере-нацелен паком sqlc (29.08): прежний токен — s.pool.Exec в CreateSession — исчез, потому что SQL этого запроса уехал в internal/pgstore/queries/sessions.sql и исполняется генерённым кодом. Новая цель — строка, где maxAge СТАНОВИТСЯ значением (AbsoluteExpiresAt: now.Add(maxAge)): именно она несёт факт, о котором строка, и она переживёт следующую генерацию. Сам дефект не тронут — потолок по-прежнему не запинен. ⚠ ЗАКРЫТО паком sqlc (63fcee5, D39.172), пере-проверено ПОСАДКОЙ, а не рассуждением. Пин — pgstore.TestTheTwoSessionDeadlinesAreNotInterchangeable (sessions_test.go): он создаёт сессию с РАЗНЕСЁННЫМИ сроками (idleTTL 1 ч против maxAge 24 ч — фикстура, где они совпадают, здесь ничего не доказывает, потому что Touch зажимает idle к absolute) и утверждает AbsoluteExpiresAt == now.Add(maxAge) после CreateSession, то есть ровно в единственном месте, где потолок становится фактом в базе. Именно та мутация, которой строка заведена — M12, now.Add(maxAge)now.Add(100*maxAge) — теперь КРАСНАЯ адресно (замер 29.08: AbsoluteExpiresAt = 2026-12-07…, want 2026-08-30…). Пак строку не искал: тест писался против перестановки двух сроков, и потолок оказался запинен тем же утверждением — поэтому закрытие подтверждено пере-прогоном ИМЕННО M12, а не сходством формулировок fixed ревью-пак P8-REVIEW, ось 2 (посадка мутации, пере-посажена рефутером на полном пакете)
PD-44 hardening info internal/pgstore/ sqlc не взят, хотя направление §3 предписывает взять его ДО появления денежных таблиц. Весь денежный SQL — сырые строки pgx ⚠ P8-FIX: половина, которая ЛЕЧИТ класс, построена; сам инструмент — вопрос владельцу. Рантайм-ошибки «нет такой колонки» (r.stop_for_signing, chapters_before) случились в СКЛЕЕННОМ SQL read-модели, куда sqlc по построению не доходит, поэтому тем же пунктом заведён постоянный гейт, который доходит: pgstore.TestEverySQLStatementParsesAgainstTheMigratedSchema сворачивает КАЖДЫЙ SQL пакета из исходника (литералы, конкатенации, именованные константы) и планирует его Postgres'ом (explain (generic_plan)) против мигрированной схемы — 162 оператора, все планируются. Посадка мутации в СКЛЕЕННЫЙ фрагмент (c.units_edit_done → несуществующая колонка) гейтом ловится; несворачиваемый SQL — ОШИБКА гейта, а не пропуск (единственное исключение — store.go Ready, где имя таблицы принадлежит goose, и оно выписано таблицей в самом гейте). Тем же гейтом закрыт открытый вопрос фикс-листа «есть ли в read-модели запрос, которого не касается ни один тест»: теперь его касаются все, на каждом прогоне батареи. ГРАНИЦА sqlc: read-модель для него недостижима по построению — склеек в пакете 25 мест из 147; конвертируем только блок из пяти файлов без склейки (credits·identity·idempotency·sessions·observe). ⚠ РЕШЕНО владельцем 22.08 (D39.154): sqlc берётся ОТДЕЛЬНОЙ СЕССИЕЙ, не внутри пака — генерённый код в дереве, пин версии инструмента, гейт актуальности, WithTx для денежных запросов; мешать это с содержательной работой нельзя. ⚠ ЗАКРЫТО: инструмент взят и заленджен63fcee5, ратификация D39.172, отдельным паком, как решил владелец 22.08 (D39.154). Конвертировано 40 запросов из этого самого блока; носители — platform/sqlc.yaml, internal/pgstore/queries/*.sql, генерённые *.sql.go в том же пакете. Актуальность генерации гейчена дважды: sqlc diff пререквизитом make check и pgstore.TestEveryGeneratedQueryMatchesItsSourceFile в батарее (работает без установленного sqlc). ⚠ Замер набора на 29.08 (прежний счёт «41 запрос в 5 файлах» снят — он был сделан до пака P11, добавившего StillLive): в наборе 42 места вызова / 41 различный текст SQL (константа read в idempotency.go исполнялась из двух мест), из них конвертируемых 40. Сороковой не observe.go: Observe спрашивает river_job через to_regclass, а эту таблицу мигрирует River сам, вне goose-миграций, поэтому sqlc отвергает запрос — и добавить схему River в конфиг значило бы завести ВТОРОЙ носитель чужой схемы. Гейт sqlgate после конверсии видит 172 оператора против пола 140 fixed ревью «вне карты»; гейт и граница — P8-FIX

Закрытые — эра P13 (03.09: бюджет из холда · ключ на пути ошибки · владение потоком · гейты хоста и класса)

ID Класс Серьёзность Где Суть Статус Источник
PD-89 hardening minor cmd/tmplatformctl/main.go grant (греп return write(ctx, store, out, *user, *key) Сминченный ключ идемпотентности не печатается при ошибке записи: PD-75 закрыл путь ПОСЛЕ коммита, но неоднозначный обрыв НА коммите остался — оператор видит ошибку, повторяет без --key, newKey() чеканит новый ключ, второе начисление проходит. Фикс — печатать ключ вместе с ошибкой ⚠ ПАК P13 03.09: лечение в дереве. Якорь — cmd/tmplatformctl/main.go write() (греп is spent under this key). Ошибка операции возвращается с ключом и рецептом повтора (… (key K: if the write did commit it is spent under this key, so a repeat under the same key — for grant and adjust, --key K — is applied at most once)) — едет на stderr вместе с ошибкой, out пуст, чтобы скрипт, читающий поток исходов, не принял отказ за исход. Формулировка называет ключ, а не флаг, потому что seed зовёт тот же write() с фиксированным ключом seed-<user> и флага --key не имеет: его простой повтор и так идёт под тем же ключом. Пин TestAFailedWriteNamesTheKeyItUsedSoTheRetryCannotCreditTwice (без DSN). Статус — акт лендинга fixed(6ae3e76) приёмка P2 (панель)
PD-168 bug minor, деньги internal/runs/reconcile.go restart Бюджет перезапуска пересчитывается по ТЕКУЩЕЙ ставке, а не по той, под которую брался холд: s.Pricing.Ceiling(l.CeilingChapters) читает конфигурацию нынешнего деплоя. Смена TM_PLATFORM_USD_PER_CHAPTER между допуском и перезапуском ломает обе стороны — вверх: резервируется больше, чем пользователь видел на шкале (нарушение «явного согласия на оплату»); вниз: остаток уходит в минус и прогон ошибочно встаёт paused/credit_exhausted. Замерено верификатором: при удвоении ставки перезапуск зарезервировал $5.50 вместо $2.50. Исходная сумма восстановима без пересчёта — она лежит в холде первой попытки (reservations.ceiling_micro_usd / run_attempts.ceiling_micro_usd) ⚠ ПАК P8-REVIEW 24.08: якорь дрейфанул, и дефект ШИРЕ записанного. Пересчёт живёт не в restart, а в internal/runs/reconcile.go reopen (греп budget, err := s.Store.RunBudget) (до P13 стояло budget := s.Pricing.Ceiling(l.CeilingChapters)) внутри reopen, и у reopen ДВА вызывающих — реконсиляторный restart и контрактный Resume. То есть смена TM_PLATFORM_USD_PER_CHAPTER между допуском и продолжением бьёт и по пользовательскому резюму, а строка описывает только перезапуск ⚠ Охват уточнён рефутером и ЗАМЕРЕН на стенде: Resume доходит до reopen только из stopped и awaiting_bank (paused отбивается раньше, reconcile.go:1225). Удвоение ставки между допуском и ПОЛЬЗОВАТЕЛЬСКИМ резюмом дало холд 5.500000 вместо ожидаемых 2.500000 — то есть денежный путь дёргает КЛИЕНТ, а не только реконсилятор ⚠ ПАК P13 03.09: лечение в дереве, ОБА места. reopen читает бюджет из холда ПЕРВОЙ попытки (pgstore.RunBudget: reservations.amount_micro_usd по ключу <run>#1) и тем же числом выдаёт funded consent на Resume; ставка в reopen больше не читается (grep -c 'Pricing\.' internal/runs/reconcile.go → 0). Холда нет — ErrNoFirstHold, без фолбэка на ставку. Пины: TestARestartHoldsWhatTheRunWasSoldForWhenTheRateHasMovedSince (свип; ×2, ÷2, ниже потраченного), TestAResumeHoldsWhatTheRunWasSoldForWhenTheRateHasMovedSince (замер ряда: $2.50, не $5.50), TestAResumeOverAMovedBankGrantsTheConsentTheRunWasSoldFor, TestAContinuationWithoutTheFirstHoldIsRefusedRatherThanRepriced. Объявленное следствие: прерванный ре-проход (ceiling_chapters = 0) свип теперь продолжает на остатке холда, а не ставит paused по Ceiling(0) = 0 — пин TestAnInterruptedRePassIsRestartedWithWhatIsLeftOfItsHold. Статус — акт лендинга fixed(6ae3e76) самопроверка дофикса (два верификатора, один исполнением)
PD-214 bug info internal/ingest/tail.go apply Чужая или битая hello-строка в общем журнале книги гасит материализацию СВОЕГО потока. Версия, непустой engine_run_id и seq == 1 проверяются для ЛЮБОЙ hello-строки ДО того, как код решает, чья она: ветка «not mine» стоит после них. Значит чужой процесс (ручной tmctl оператора в каталоге книги, старый мажор, баг чужой сборки) валит Tail ошибкой, а quarantines() считает её терминальной — проекция здорового платящего прогона уходит в карантин НАВСЕГДА (снятия карантина в дереве нет) ⚠ «Снятия карантина в дереве нет» — верно на дату ряда и НЕВЕРНО с пака P13: ручка построена (PD-426, tmplatformctl run unquarantine --run <id>); фраза оставлена как часть замера, поправка 04.09., свежесть падает на медленный ре-синк. Данные целы, деньги целы. Лечится порядком: при известном want чужой id распознаётся ДО валидации хендшейка ⚠ ПАК P13 03.09: лечение в дереве ровно этим порядком. При известном want чужой engine_run_id распознаётся сразу после декода payload и ДО checkVersion / пустого id / seq != 1 (декод остаётся выше: без него принадлежность неизвестна); при want == "" валидация полная — усыновление чужого мажора было бы тем же дефектом с другой стороны. Пины (без DSN): TestAForeignHandshakeThisBuildCannotReadIsSkippedRatherThanQuarantiningOurs (пять форм чужой строки), TestOurOwnHandshakeThisBuildCannotReadStillStopsTheProjection, TestAnAdoptedHandshakeIsStillValidatedInFull, TestAHandshakeThatDoesNotDecodeIsRefusedEvenWhenTheReaderKnowsItsName. ⚠ Одного пере-упорядочивания оказалось МАЛО, и это отдельная строка PD-438: пока владение потоком не переживало проход свипа, тот же класс возвращался строкой ниже — чужие события следующего прохода ложились на нашу попытку. Обе половины вылечены тем же деревом. Статус — акт лендинга fixed(6ae3e76) адверсариальное ревью P6 (линза шва)
PD-426 bug minor internal/pgstore/runs.go Quarantine, internal/ingest/tail.go (четыре отказа выше ветки default) Карантин проекции не снимается НИЧЕМ, а попасть в него можно по чужому законному handshake'у. Первое: quarantine_reason пишется, и во всём дереве нет ни одного места, которое его очищает, — то есть состояние терминально для проекции живого оплаченного прогона. Второе: в ingest/tail.go четыре отказа стоят ВЫШЕ ветки default, которая говорит «другой поток начинается здесь, нас не касается», при том что журнал ПЕР-КНИЖНЫЙ и append-only, так что чужие handshake'ы в нём законны. Вместе: чужой handshake в журнале книги карантинит проекцию прогона, за который заплачено, навсегда. ⚠ Не предмет пака P11 (тейлер и карантин — эры P4/P5), заведено строкой ⚠ ПАК P13 03.09: лечение в дереве, обе половины. Снятие — tmplatformctl run unquarantine --run <id> (pgstore.Unquarantine: чистит quarantine_reason ЖИВОЙ попытки, курсор не трогает — те же байты нечитаемы ⇒ следующий свип карантинит снова с той же причиной; три отказа своими словами: нет прогона · нет живой попытки · не в карантине); причина видна колонкой QUARANTINE в tmplatformctl runs. Порядок в тейлере — PD-214. Формулировка «по чужому ЗАКОННОМУ handshake'у» — переупрощение ровно наполовину: в ОДНОМ проходе законный чужой hello и раньше пропускался (TestAnotherAttemptsStreamInTheSameJournalIsSkipped), карантинил чужой ИЛИ БИТЫЙ (другой мажор · пустой id · seq != 1); а ЧЕРЕЗ проход законный чужой hello и был путём и в карантин, и в порчу проекции — PD-438. Пины: TestALiftedQuarantineMaterializesTheJournalAgainFromTheCursor (свип, DSN), TestLiftingAQuarantineClearsItAndSaysWhatItWas (CLI, DSN). Статус — акт лендинга fixed(6ae3e76) приёмка оркестратора №19 по паку P11 (охотник вне карты)
PD-436 doc minor deploy/README.md (греп безопасного порядка НЕТ ни в одну сторону), internal/ingest/manifest.go Readable (греп m.Version != KnownManifestVersion) Рантбук зоны советовал деструктивный порядок деплоя при бампе формы манифеста: «Правило то же, что у схемы хранилища: сначала платформа, потом движок». Неверно дважды. Первое: гейт интейка — строгое равенство ОДНОЙ константе, окна двух форм нет, поэтому платформа, выкаченная первой со знанием новой формы, отправляет в parser_unavailable КАЖДУЮ книгу ещё не обновлённого движка — тот же симптом и тот же бюджет попыток до rejected, что абзац описывал для обратного порядка; безопасного порядка у бампа формы НЕТ, это стоп-мир (приём закрыт → оба билда → приём открыт) с дренажем «трёх фактов» перед ним, пока в гейте не построено окно двух форм. Второе: правило схемы хранилища, на которое ссылался абзац, — «движок первым → migrate → платформа» (D39.158 п.6), то есть ОБРАТНОЕ названному, и про другой шов. ⚠ Лечение в дереве пака P13 (03.09): абзац рантбука переписан; окно двух форм НЕ строится — не заказано. Статус — акт лендинга fixed(6ae3e76) сборка промта P13 оркестратором; подтверждено чтением manifest.go сессией P13
PD-437 standards info internal/runner/translate_resnapshot_live_test.go TestTheSnapshotGuardIsLoudWithoutTheFlagsAndPassesWithThem, docs/STACK_DECISIONS.md «Стенд разработчика» (рецепт шаблона), движок backend/configs/pipeline-c1.yaml · models.yaml Живой тест снапшот-гарда обещает «free of provider keys», а на шаблоне, собранном по рецепту стенда, движок отказывает ему за ключом ДО гарда. Замер 03.09 при всех четырёх условиях хоста на чистой копии HEAD 494c4ef с движком из того же HEAD: батарея 661 теста, 660 PASS, 0 скипов и ровно один FAIL — the priming run failed (10): tmctl: missing API keys (fill in backend/.env): provider deepseek: env DEEPSEEK_API_KEY is not set (needed for model deepseek-v4-flash). Причина: пробная книга (writeProbeBook) наследует pipeline: шаблона, а шаблон по рецепту = backend/example/book.yamlpipeline-c1.yaml, чей переводчик — deepseek-v4-flash; тест ждёт шаблон, чей пайплайн — локальная $0-пара на 127.0.0.1:11434, но ни рецепт стенда, ни сам тест этого условия не называют. Следствие: «скипов 0 и батарея зелёная» на хосте без ключа недостижимы, а ключ в окружении сделал бы тест платным. Подстановка фиктивного ключа — обход, не лечение; чинить надо либо посылку теста (свой pipeline: с $0-парой поверх шаблона), либо рецепт ⚠ ПАК P13 03.09, по прямому слову владельца («чини»): вылечена ПОСЫЛКА ТЕСТА. writeProbeBook рендерит пробной книге СВОЙ пайплайн (zeroCostPipeline, греп в internal/runner/bankapply_live_test.go): берёт пайплайн шаблона, заменяет модель каждой стадии на модель ЛОКАЛЬНОГО провайдера, снимает хопы эскалации и label_models, обнуляет escalation.budget_usd. Локальная модель НЕ вписана константой, а находится в models.yaml того же шаблона по kind: local — деплой без локального провайдера даёт громкий скип, а не чужой ключ; механизм сверен по коду движка (backend/internal/config/models.go checkKeysFor пропускает провайдера с kind == "local"). Промпты и калибровка пары резолвятся от каталога конфига, поэтому рендер лежит в ЗЕРКАЛЕ каталога деплоя (ссылки на prompts/pairs/langpacks), и вне каталога книги — иначе он попадал в опись пробы «превью ничего не пишет». Пины: TestTheProbePipelineLeavesNoPaidModelReachable (рендер на синтетическом конфиге, без движка) и сам TestTheSnapshotGuardIsLoudWithoutTheFlagsAndPassesWithThem, который теперь ЗЕЛЁН без ключей. Рецепт стенда НЕ трогала: он про деплой оператора, а обещание «free of provider keys» давал тест. Статус — акт лендинга fixed(6ae3e76) пак P13 (базовая линия батареи при полных условиях, до правок)

Закрытые — эра «закрыть цикл» (04.09: живой платный прогон через API · дверь выдачи · терминальная ручка для осиротевшей попытки)

ID Класс Серьёзность Где Суть Статус Источник
PD-442 bug minor internal/pgstore/migrations/00002_readmodel.sql (таблица exports), канон 14-api-contract/openapi.yaml §Export В схеме с самой первой миграции читающей модели жила таблица exports, у которой НЕТ ни одного читателя и ни одного писателя, и её форма ПРОТИВОРЕЧИТ ратифицированному контракту. Она несла ready boolean плюс failed_reason text, а канон объясняет прямо, почему так нельзя: «A state and not a boolean: a boolean merges three situations into "not ready" and a poll on it never ends» (§Export.state). То есть будущая дверь выдачи, честно построенная на существующей таблице, отдала бы поллинг, который не кончается — ровно тот дефект, который канон и запрещает. Обнаружено при постройке двери: sqlc generate отказался с relation "exports" already exists, и это единственный сторож, который у класса был. Проверка отсутствия потребителей: grep -rn '\bexports\b' --include=*.go internal/ cmd/ до пака давал ОДИН хит и тот в комментарии (internal/httpapi/capabilities.go). ⚠ Мигрировать было нечего — ни строки ни разу не записывалось, — поэтому 00032_exports.sql таблицу СНОСИТ и создаёт заново в форме канона; down-путь восстанавливает форму 00002 дословно, потому что откат обязан вернуть то, что выпущенная миграция оставила. Пин формы: internal/pgstore/exports_test.go (четыре состояния, партиальные индексы свипа, каскад с книгой) fixed(дерево пака «закрыть цикл», 04.09) пак «закрыть цикл» 04.09 (найдено постройкой двери выдачи)
PD-447 doc minor docs/STACK_DECISIONS.md §«Как поднять локально» и deploy/README.md шаг 34 (оба исправлены); носители дефолта — internal/config/config.go:330=TM_PLATFORM_ADDR и cmd/tmplatformctl/seed.go:41=base URL of the running tmplatformd Оба стендовых рецепта зоны принимали ОТВЕТ ПО АДРЕСУ за доказательство того, что отвечает СВОЙ процесс, и потому проходили при мёртвом собственном демоне. Замерено исполнением 04.09: на машине разработки четвёртые сутки жил чужой tmplatformd на 127.0.0.1:8080 со своей базой; демон рецепта умирает в этой ситуации на bind: address already in use — молча, он запущен фоном, — а смоук следующей строкой получает healthz=200 и readyz=ready ОТ ЧУЖОГО ПРОЦЕССА. ⚠ Дороже смоука — шаг сида: tmplatformctl seed дарит $25 кредита и грузит книгу ЧЕРЕЗ ЖИВОЙ ИНТЕЙК, то есть по этому рецепту деньги и данные уезжают в чужой деплой. Второй адрес там же был написан отдельным литералом (--url http://127.0.0.1:8080), что и есть штатный способ разъехаться с TM_PLATFORM_ADDR незаметно. ⚠ Три лечения на три РАЗНЫЕ половины, ни одно не заменяет другое: явный _ADDR уводит с общего дефолта; проба по pid слушателя отвечает на вопрос, на который 200 не отвечает в принципе — ЧЕЙ это процесс; --noproxy '*' нужен потому, что при заданных http_proxy голый curl на 127.0.0.1 уходит во внешний прокси. Проба предъявлена в обе стороны: свой pid на своём порту — проходит, чужой демон против своего pid — отвергается. ⚠ Дефолт 8080 в коде НЕ меняется, и это решение: коллизия — tmplatformd против tmplatformd, любой другой номер даст ту же аварию на втором одновременном стенде, а цену смены заплатят все существующие деплои и доки. Чинится посылка, а не номер. Вес minor, а не major, по радиусу: пишет только дев-стенды, боевого пути этим рецептом нет, деньги — стендовый грант. fixed(adf5e53) найдено оркестратором №22 при независимой проверке отзыва причины смертей демона, 04.09; починено зоной