textmachine/platform/docs/p8-review/agent-mutations.md

40 KiB
Raw Permalink Blame History

Посадки мутаций восьми субагентов пака P8-REVIEW (24.08.2026)

Что здесь есть и чего НЕТ. Это таблицы, которые вернули сами агенты: что посажено, где, какой пин обязан был упасть, упал ли, и вердикт. СЫРЫЕ логи их прогонов НЕ сохранены — они жили в копиях деревьев агентов и не были затребованы заданием. Упрёк назван старшим ревьюером и принимается: вердикт «пойман» здесь пере-ранить нельзя, можно только пере-посадить по описанию. Посадки координатора пака воспроизводимы полностью — plant.py, mutations.log, mutations-full.log, mutations-round2.log.

Проверено отдельно и с ЧИСЛОМ, а не ощущением: вердиктов «пойман» здесь 21, из них 19 называют КОНКРЕТНЫЙ тест (TestXxx) и потому проверяемы грепом — ни один не приходится на известный флейк стенда runner.TestARunIsBoundedByItsOwnCgroup (единственное совпадение по нему стоит на строке с вердиктом «носителя нет», то есть на ВЫЖИВШЕЙ). Оставшиеся 2 вердикта теста не называют (оба на оси 2: «httpapi.server_test — кросс-сайтовый выход» и «проверял, есть ли утверждение О ПРЕФИКСЕ»), и для них утверждение «пойман не флейком» НЕ проверено — сказано прямо, чтобы приёмка знала, где именно улика тоньше.

axis1-money

финдер

что посажено где какой пин обязан был упасть упал вердикт
SpendBound перестаёт выбирать НАИМЕНЬШУЮ базовую линию среди более поздних попыток книги и берёт наибольшую — то есть верхняя граница отложенного расчёта становится самой слабой. Правка: select min(a.spend_baseline_micro_usd)select max(a.spend_baseline_micro_usd) internal/pgstore/runs.go:588 (копия) TestADeferredSettlementIsNotChargedForTheNextRunOfTheSameBook (internal/runs/sweep_test.go:817) — единственный тест, который реестр называет пином PD-159 НЕТ ВЫЖИЛ — и это находка (см. находку про PD-159). Мутация НЕ эквивалентная: на трёх попытках книги (базовые линии 100000/200000/500000) чистый код отдаёт 200000, мутант — 500000; написанный мной пин a1_spendbound_test.go падает под мутацией (the bound is 0.500000, want 0.200000) и проходит на восстановленном коде (PASS 1.02s). Причина слепоты названа точно: в фикстуре существующего пина ПОЗЖЕ стартовала ровно ОДНА попытка, а на множестве из одного элемента min и max — одно и то же.
Верхняя граница потолка прогона перестаёт перепроверяться на сервере: клиентское ceiling_chapters больше не сверяется со шкалой. Правка: if in.CeilingChapters < bounds.Min // in.CeilingChapters > bounds.Max {if in.CeilingChapters < bounds.Min { internal/runs/runs.go:243 (копия) носителя нет — единственный тест, называющий ErrCeilingOutOfBounds, это таблица «ошибка → 409» в internal/httpapi/v0_test.go:359, которая Start не вызывает НЕТ ВЫЖИЛ ПОД ВСЕЙ БАТАРЕЕЙ — главная находка. Поведенческий характер доказан на проводе: чистая сборка отвечает 409 ceiling_unavailable/bounds_moved на {"ceiling_chapters":800} для трёхглавой книги, мутант отвечает 202 и берёт холд 24000000 микро ($24.00) из баланса 25440000, оставляя счёт с $1.44, а юниту выдаёт --ceiling-usd 24.200000.
Из закрытия резервации убран стражевой предикат состояния — тот, что делает повторный расчёт отказом, а незалоченное чтение владельца безопасным. Правка: update reservations set state=$2, closed_at=$3 where engine_run_id=$1 and state='open'... where engine_run_id = $1 internal/pgstore/credits.go:343 (копия), функция closeReservation любой тест на повторный расчёт/возврат и на равенство кэша леджеру да Пойман двумя пакетами. Идемпотентность расчёта «отказом, а не повтором» закрыта по-настоящему.
Перезапуск/резюм перестаёт вычитать уже потраченное и заново покупает ВЕСЬ потолок прогона — прямое нарушение «один прогон не может потратить свой потолок дважды». Правка: remaining := budget - spentremaining := budget - spent*0 internal/runs/reconcile.go:1099 (копия), функция reopen тесты резюма и перезапуска на остаток бюджета да Пойман — девятью тестами и с точными числами в сообщениях. Остаток бюджета прогона закрыт очень плотно.

рефутер

что посажено где какой пин обязан был упасть упал вердикт
Снята ВЕРХНЯЯ половина перепроверки потолка прогона — та самая, которую доккомментарий runs.go:212-216 называет несущей («the number that decides how much money is reserved cannot be one the caller chose unilaterally»). Правка: строка 243 if in.CeilingChapters < bounds.Min // in.CeilingChapters > bounds.Max {if in.CeilingChapters < bounds.Min { // R1-MUT-A upper bound removed /home/ubuntu-26/tm-p8-review/r1-money/platform/internal/runs/runs.go:243 носителя нет — единственный тест, называющий ErrCeilingOutOfBounds, это таблица «ошибка → 409» в internal/httpapi/v0_test.go:359, которая Start не вызывает; ближайшая по духу фикстура control_test.go:640-645 просит 10 глав у книги на 100 (control_test.go:62), то есть остаётся внутри границ НЕТ ВЫЖИЛ — подтверждает находку 1 финдера (с сужением веса до minor). Эквивалентной мутация не является: мой пин artifacts/r1_ceiling_bound_test.go под ней падает (a ceiling of 100 chapters on a 3-chapter book: err = <nil>, want ErrCeilingOutOfBounds), а на восстановленном коде проходит (PASS 0.51s). Пакеты выбраны не наугад: код проверки живёт в internal/runs, единственный его HTTP-вход — в internal/httpapi, деньги — в internal/pgstore; тест вне этих трёх мутацию видеть не может.
SpendBound перестаёт брать НАИМЕНЬШУЮ базовую линию среди более поздних попыток книги и берёт наибольшую — то есть верхняя граница отложенного расчёта становится самой слабой, при том что доккомментарий runs.go:572 называет инвариант словом «the SMALLEST». Правка: строка 588 select min(a.spend_baseline_micro_usd)select max(a.spend_baseline_micro_usd) -- R1-MUT-B /home/ubuntu-26/tm-p8-review/r1-money/platform/internal/pgstore/runs.go:588 TestADeferredSettlementIsNotChargedForTheNextRunOfTheSameBook (internal/runs/sweep_test.go:818) — пин, который реестр называет доказательством закрытия PD-159 НЕТ ВЫЖИЛ — подтверждает находку 2 финдера. Мутация поведенческая: мой собственный пин artifacts/r1_spendbound_test.go (четыре попытки одной книги, базовые линии 50000 → 900000 / 200000 / 700000, наименьшая в СЕРЕДИНЕ) под ней даёт the bound is 0.900000, want 0.200000, а на восстановленном коде PASS. Причина слепоты существующего пина названа точно и проверена чтением фикстуры: у него в книге ровно ОДНА более поздняя попытка (sweep_test.go:821 и 841), а на множестве из одного элемента min и max — одно и то же.
МОЯ собственная посадка, в соседнюю арку той же функции, чтобы отделить «непокрыт весь ReadUsage» от «непокрыта именно арифметика». Испорчен кламп доли на 100%, который комментарий кода прямо объясняет контрактом («the contract's field is 0..100 and a client that got 137 would have to invent a meaning»). Правка: books.go:1050 u.RemainingPercent = 100u.RemainingPercent = 137 // R1-MUT-C /home/ubuntu-26/tm-p8-review/r1-money/platform/internal/pgstore/books.go:1050 любой тест доли /usage на счёте, где баланс не меньше суммы грантов да Пойман двумя тестами — и это уточняет находку 4, а не расширяет её: верхний кламп доли запинен по-настоящему, дырка ровно и только в арифметике ветки default (books.go:1053), куда управление попадает лишь когда баланс НИЖЕ суммы грантов. Отдельной находкой не оформляю: свойство закрыто.

axis2-auth

финдер

что посажено где какой пин обязан был упасть упал вердикт
Контроль оснастки: Digest перестаёт хешировать и возвращает сам токен (_ = sha256.Sum256([]byte(token)); return []byte(token) — sha256 оставлен использованным, иначе не компилируется) platform/internal/auth/session.go:55-58 (копия) pgstore.TestStoredCredentialIsAHashNotTheToken и auth.TestTokensAreUniqueAndDigestIsStable да Пойман. Оснастка рабочая — дальнейшие «выжил» не артефакт метода.
Выход перестаёт гасить куку: ClearSession выдаёт TTL time.Hour вместо -time.Second, то есть перевыпускает куку с мёртвым токеном на час platform/internal/auth/cookie.go:52 (копия) носителя нет — ни один тест не читает Max-Age ответа /auth/logout НЕТ ВЫЖИЛ — находка (PD-1). Свойство «выход убирает копию креденшела из браузера» не закрыто ничем, кроме чтения кода; сегодняшнее поведение верно, проверено живым curl.
Ни одна кука больше не истекает: в общем set ветка ttl < 0 даёт maxAge = 3600 вместо -1 — гаснуть перестают и сессионная кука, и __Host-tm_login, которую колбэк обязан снимать после round trip platform/internal/auth/cookie.go:62-66 (копия) носителя нет НЕТ ВЫЖИЛ — та же находка, шире: не запинена вся ветка истечения, включая state-куку, про которую cookie.go:58-59 пишет «a state cookie that outlives its round trip is a replay waiting for an accident». ⚠ Пин на ОБРАТНОЕ свойство есть (TestARefusedCallbackKeepsTheLoginItRefused — 429 НЕ стирает куку), а на прямое нет.
Поддерево /auth/ теряет слой CSRF: LimitBody(DefaultMaxBody)(csrf(d.Login.Routes(...)))LimitBody(DefaultMaxBody)(d.Login.Routes(...)) platform/internal/httpapi/server.go:132 (копия) httpapi.server_test — кросс-сайтовый выход да Пойман. Разводка CSRF на /auth/ запинена; ./internal/login/ и ./internal/auth/ при этом остались зелёными — то есть пин ровно один и он в httpapi, что и правильно (там разводка и живёт).
Дев-вход перестаёт ротировать сессию: удалён блок if old, _, ok := auth.Present(r, d.cookies.SessionName()); ok { d.store.RevokeSession(...) } (фиксация сессии) platform/internal/login/dev.go:154-158 (копия) предполагал, что носителя нет (боевой путь пинится TestLoginRevokesThePresentedSession, дев — под вопросом) да Пойман, гипотеза о дыре опровергнута: у дев-входа СВОЙ пин, TestTheDevelopmentSignInReplacesThePreLoginSession. Сверено и живьём: повторный dev-login со старой кукой делает старый токен 401, новый 200.
Абсолютный потолок, записываемый в строку сессии, умножен на 100: now.Add(maxAge)now.Add(100*maxAge) (30 объявленных суток становятся 3000) platform/internal/pgstore/sessions.go:64 (копия) должен был бы поймать любой из трёх пинов, которые STACK_DECISIONS §13 называет закрывающими ASVS 7.3.2 НЕТ ВЫЖИЛ — находка. Объявленный §13 пин TestSessionClocksStayWithinTheDeclaredBaseline смотрит на результат config.Load(), а не на строку в БД; Touch-зажим и клаузы Lookup проверяют чтение и скольжение. Место, где число становится фактом, не видит никто. ⚠ В этом прогоне ПОЛНОГО пакета pgstore упал посторонний TestAClaimThatLostARaceToAReleaseIsRetriedAndNotAnError — это флейк под нагрузкой хоста, а не поимка: на чистом дереве он прошёл 3 из 3.
Продовые куки теряют префикс __Host-: CookieName → "tm_session_v2", LoginCookieName → "tm_login_v2" (то, что запрещает соседнему поддомену писать сессионную куку — session.go:15-18) platform/internal/auth/session.go:18 и platform/internal/auth/cookie.go:18 (копия) проверял, есть ли утверждение О ПРЕФИКСЕ да Пойман, но СЛУЧАЙНО и слабо: покраснел тест про различение двух отказов до авторизации, у которого имя __Host-tm_session зашито литералом в фикстуре, — он упал потому, что перестал аутентифицироваться, а не потому, что кто-то проверяет префикс. Переименование, аккуратно обновившее литералы (единственный способ, которым это сделал бы человек), прошло бы батарею. Отдельной находкой не оформляю: значение — константа, читаемая глазами, и цена ошибки видна на первом же живом запросе.

рефутер

что посажено где какой пин обязан был упасть упал вердикт
КОНТРОЛЬ ОСНАСТКИ: RevokeSession перестаёт отзывать — set revoked_at = $2 заменено на set last_used_at = $2 (запрос остаётся валидным, отзыв исчезает) /home/ubuntu-26/tm-p8-review/r2-auth/platform/internal/pgstore/sessions.go:72 pgstore.TestSessionLifecycle / TestRevokeUserSessionsEndsAllOfThem да Пойман. Оснастка рабочая, дальнейшие «выжил» — не артефакт метода.
Выход перестаёт удалять куку: ClearSession выдаёт TTL time.Hour вместо -time.Second /home/ubuntu-26/tm-p8-review/r2-auth/platform/internal/auth/cookie.go:52 носителя нет — ни один тест не читает Max-Age ответа /auth/logout (grep -rn "MaxAge" --include=*_test.go internal/ даёт только перевыпуск при скольжении, middleware_test.go:221-263) НЕТ ВЫЖИЛ — подтверждает находку финдера №2 по ПИНУ, но сужает её по ЦЕНЕ. Собрал демон из мутанта и снял живой ответ: «Set-Cookie: tm_session=; Path=/; Max-Age=3600», значение ПУСТОЕ, а тот же токен следующим запросом даёт 401. Значит регрессия оставляет в браузере пустую куку, а не «мёртвый токен на час».
Ни одна кука не истекает: в общем set ветка ttl < 0 даёт maxAge = 3600 вместо -1 (гаснуть перестают и сессионная кука, и __Host-tm_login) /home/ubuntu-26/tm-p8-review/r2-auth/platform/internal/auth/cookie.go:62-66 носителя нет НЕТ ВЫЖИЛ — ветка истечения действительно не закрыта ничем. Оговорка та же: ClearLogin тоже передаёт ПУСТОЕ значение (cookie.go:60), поэтому state-куку реиграть нечем, и цитата «a replay waiting for an accident» к этой посадке не относится.
Абсолютный потолок, записываемый в строку сессии, умножен на 100: now.Add(maxAge)now.Add(100*maxAge) /home/ubuntu-26/tm-p8-review/r2-auth/platform/internal/pgstore/sessions.go:64 любой из трёх пинов, которые STACK_DECISIONS §13 называет закрывающими ASVS 7.3.2 НЕТ ВЫЖИЛ — находка финдера №3 подтверждена, и подтверждена сильнее: полный пакет pgstore, которого он не гонял чисто, тоже слеп. Структурная причина видна в теле TestSessionLifecycle (pg_test.go:92-143): оно утверждает idle <= absolute, а при ×100 это утверждение остаётся истинным.
МОЁ: срок хранения журнала входов поднят со 180 суток до 180 лет — const loginJournalRetention = 180 * 365 * 24 * time.Hour /home/ubuntu-26/tm-p8-review/r2-auth/platform/cmd/tmplatformd/main.go:257 должен был бы поймать тот пин, которым PD-29 объявлен закрытым («ретеншен журнала 180 дней свипом») НЕТ ВЫЖИЛ — моя новая находка. Свойство, на которое опираются сразу две строки реестра (PD-23 open и PD-29 fixed), не удерживается ничем.
МОЁ: сам свип журнала снят с демона — строка events, err := db.DeleteOldLoginEvents(...) заменена на var events int64 (тикер продолжает чистить только брошенные логин-состояния) /home/ubuntu-26/tm-p8-review/r2-auth/platform/cmd/tmplatformd/main.go:273 тот же НЕТ ВЫЖИЛ у ТЕСТОВ. Красное даёт только линтер и только побочно — потому что константа стала мёртвой; правка «как сделал бы человек» (снять и вызов, и константу) прошла бы и его. ⚠ Отдельно: первая моя попытка сломать ретенцию через сам SQL (at < $1 - interval '100 years') БЫЛА поймана — pgstore.TestEverySQLStatementParsesAgainstTheMigratedSchema, «operator does not exist: timestamp with time zone < interval», — но лишь потому, что мутант перестал типизироваться: гейт проверяет планируемость, а не смысл.

axis3-queue

финдер

что посажено где какой пин обязан был упасть упал вердикт
Механизм 1 лечения — РАЗДЕЛЕНИЕ прохода на фазы. phaseBudget перестаёт делить остаток пополам: context.WithTimeout(ctx, time.Until(deadline)/2)context.WithTimeout(ctx, time.Until(deadline)) internal/runs/reconcile.go:226 (копия) TestTheSettlementPhaseIsReachedWhenTheRunPhaseSpendsThePass да Пойман, двумя пинами сразу. Механизм 1 закреплён.
Механизм 2, ЧИТАЮЩАЯ сторона — выборка свипа перестаёт чтить отсрочку: условие (a.reconcile_after is null or a.reconcile_after <= $1 or r.stop_requested_at is not null) заменено на тавтологию ($1::timestamptz is not null) and true internal/pgstore/runs.go:279-280 (копия) TestARunTheSweepCannotFinishStopsHoldingTheHeadOfTheList / TestTheDeferralEngagesAtTheRatioThisZoneShips / TestAStopOutranksADeferral да Пойман тремя независимыми пинами. Читающая сторона отсрочки закреплена.
Механизм 2, ПИШУЩАЯ сторона в фазе денег — settleOne перестаёт откладывать элемент, выбравший бюджет: три строки записи отсрочки (context.WithTimeout(WithoutCancel…) + deferItem) заменены на no-op internal/runs/reconcile.go:207-209 (копия) TestASettlementNobodyCanFinishStopsHoldingTheHeadOfTheMoneyList да Пойман. Вторая половина блокера (PD-353) закреплена — но только для ДОРОГОЙ неудачи; дешёвая не покрыта ничем, это находка F1.
Пара констант через границу пакетов: бюджет прохода материализации падает НИЖЕ бюджета одной книги — const refreshSweepBudget = 10 * time.Minute1 * time.Minute (при readmodel.MaterializeBudget = 5m) cmd/tmplatformd/runner.go:200 (копия) носителя нет — ни в cmd/tmplatformd, ни в internal/readmodel нет теста на СВЯЗЬ двух чисел НЕТ ВЫЖИЛ — и это находка F4. Не «свойство недостижимо»: проба показывает, что на этой стороне пары Drain при проходе 4м59с делает claims=0, engine calls=0 и возвращает nil, то есть материализация отключается полностью и молча. У двух сиблингов той же семьи связь закреплена (intakeSweepBudget выведен формулой, claimGrace выведен И запинен books_test.go:933) — у этой нет ни того, ни другого.
Аренда долга чтения становится десятой частью работы, которую обязана покрывать: ClaimReadModelDebt(ctx, b.ID, b.OwedAt, MaterializeBudget)…, MaterializeBudget/10 internal/readmodel/readmodel.go:107 (копия) TestTheDrainSkipsABookSomebodyElseIsAlreadyMaterializing да Пойман. Я заходил сюда с гипотезой «пара окна аренды и бюджета работы не запинена» — гипотеза ОПРОВЕРГНУТА исполнением: пин требует РАВЕНСТВА окна и MaterializeBudget. В находки не пошло; остаток (равенство без запаса, тогда как у сиблинга claimGrace запас +5 мин) вынесен в obstacles как непроверенный.
Композиция такта: фаза прогонов уходит из ГОЛОВЫ такта в хвост — вызов pass("runs", sweepBudget, s.runs.Sweep) перенесён из первой строки one() за проходы readmodel/intake/idempotency cmd/tmplatformd/runner.go:251 → :277 (копия) носителя нет — в пакете main всего два теста (markerArgv и runsConfig), функция sweep не покрыта ничем НЕТ ВЫЖИЛ — и это половина находки F3. Свойство достижимо и наблюдаемо (моя проба меряет частоту фазы прогонов по вызовам Runner.Stop), просто ни один тест зоны не смотрит на композицию такта.

рефутер

что посажено где какой пин обязан был упасть упал вердикт
Инвариант «CLEARED BY EVIDENCE, never by the absence of it» (reconcile.go:126-131): проход, который ничего у движка не спрашивал, больше не выходит рано и обнуляет счётчик неудач заклиненного прогона — та самая осцилляция 1,0,1,0, из-за которой StalledAfter был недостижим. Правка: if !established {if !established && false { (форма выбрана так, чтобы переменная осталась использованной и пакет собрался). /home/ubuntu-26/tm-p8-review/r3-queue/platform/internal/runs/reconcile.go:122 TestTheFailureCountSurvivesThePassesThatAskTheEngineNothing (internal/runs/stalled_test.go:676) да Пойман ровно ожидаемым пином. Несущий инвариант первой фазы закреплён.
Ключ списка расчёта денег: UnsettledRuns перестаёт смотреть на КОНЕЦ ПОПЫТКИ и смотрит на конец ПРОГОНА — регрессия, которую его собственный доккомментарий называет вслух («a list that filtered on the run never looked at it again — the hold stayed reserved for the life of the account»). Правка: join run_attempts a on a.run_id = r.id and a.ended_at is not null… and r.finished_at is not null. /home/ubuntu-26/tm-p8-review/r3-queue/platform/internal/pgstore/sink.go:693 TestAnInterruptedAttemptsHoldIsStillFoundWhileItsRunGoesOn (internal/runs/sweep_test.go:981) да Пойман двумя независимыми пинами. Ключ списка расчёта закреплён.
Гейт бюджета между книгами в readmodel.Drain — живой носитель, которым закрыта PD-293 («проверка бюджета между книгами живёт в readmodel.Drain»): проход больше не отказывается начинать книгу, которой не может дать целый MaterializeBudget. Правка: time.Until(deadline) < MaterializeBudgettime.Until(deadline) < 0 && deadline.IsZero() (тождественно ложно при ok=true, то есть гейт выключен целиком). /home/ubuntu-26/tm-p8-review/r3-queue/platform/internal/readmodel/readmodel.go:183 носителя нет — ни один тест дерева не даёт Drain дедлайн (grep -rn "WithTimeout\/WithDeadline" --include=*_test.go internal/readmodel/ → пусто; единственное упоминание MaterializeBudget в тестах — readmodel_test.go:436, и оно про окно клейма) НЕТ ВЫЖИЛ — и это моя находка N1. Не «свойство недостижимо»: близнец того же гейта у интейка (parse.go:429) при точно такой же порче краснеет пином books_test.go:1272, то есть свойство и наблюдаемо, и зона умеет его пинить — просто на второй половине не запинила, а строка PD-293 при этом стоит fixed.
Тот же гейт у ИНТЕЙКА — контрольная посадка к предыдущей: books.Sweep перестаёт отказываться начинать книгу, которой не может дать целый jobs.JobTimeout. Правка: time.Until(deadline) < jobs.JobTimeouttime.Until(deadline) < 0 && deadline.IsZero(). /home/ubuntu-26/tm-p8-review/r3-queue/platform/internal/books/parse.go:429 TestAPassTooShortForAParseStartsNoneAtAll (internal/books/books_test.go:1258) да Пойман. Посажен ради контраста с R3: две половины одного свойства, одна запинена, вторая нет — именно этот контраст и делает R3 находкой, а не догадкой о недостижимости.

axis4-metrics

финдер

что посажено где какой пин обязан был упасть упал вердикт
Гейдж застрявших прогонов навсегда ноль: StalledRuns: o.StalledRunsStalledRuns: 0 (наблюдаемая половина PD-346, который стоял BLOCKER'ом) cmd/tmplatformd/runner.go:309 (в копии) носителя нет — ожидал, что не покраснеет; кандидатом был metrics.TestTheRunnersStateIsExposedWithItsUnits, но он не задаёт поле StalledRuns НЕТ ВЫЖИЛ — находка (см. находку 2). Единственный сигнал P8-FIX об застрявшем прогоне может быть мёртв, и батарея это не заметит.
Счётчик недошедших проходов не может вырасти: s.metrics.ObserveSweep(name, time.Since(start), errors.Is(err, context.DeadlineExceeded))…, false) (ровно механизм PD-351, но со стороны вызывающего) cmd/tmplatformd/runner.go:245 (в копии) носителя нет; пин PD-351 TestAPassThatRanOutOfTimeSaysSo живёт в internal/runs и проверяет, что Sweep ВОЗВРАЩАЕТ обёрнутый DeadlineExceeded, а не что вызывающий его считает НЕТ ВЫЖИЛ — находка. Закрытый дефект возвращается в шов, на который не смотрит ни один тест.
Глубина очереди и число живых прогонов поменяны местами: QueueDepth: o.LiveRuns / LiveRuns: o.QueueDepth cmd/tmplatformd/runner.go:303,306 (в копии) носителя нет НЕТ ВЫЖИЛ — находка. Оператор читал бы одно число как другое, и ничто не возражает.
Лейбл маршрута несёт СЫРОЙ ПУТЬ: route := r.Patternroute := r.URL.Path (ломает объявленную §24 кардинальность и PD-3) internal/metrics/metrics.go:211 (в копии) metrics.TestRequestsAreCountedByRoutePatternAndNeverByPath и TestAnUnroutedRequestGetsOneSeriesAndNotOnePerPath да Пойман обоими пинами, которые §24 и называет. ⚠ Отмечено попутно: пины монтируют мидлварь ВНУТРЬ мукса, тогда как в бою она снаружи (httpapi/server.go:145-148) — то есть боевую разводку они не доказывают; я закрыл это живой пробой (см. clean).
Обёртка теряет Unwrap: удалён func (r *recorder) Unwrap() — посадка, которую STACK §25 объявляет падающей internal/metrics/metrics.go:253 (в копии) metrics.TestTheWrapperKeepsTheResponseControllerReachable и httpapi.TestTheUploadRouteExtendsItsOwnReadDeadlineAndOnlyItsOwn да Пойман обоими уровнями — заявление STACK_DECISIONS §25 «посадка «убрать Unwrap из метрик» падает» проверено исполнением и ИСТИННО.
Лейбл метода — токен вызывающего как есть: method := knownMethod(r.Method)method := r.Method (возврат PD-184) internal/metrics/metrics.go:218 (в копии) metrics.TestAnUnroutedRequestGetsOneSeriesAndNotOnePerPath да Пойман. Дерево после всех посадок восстановлено: diff -r --brief против репозитория → «identical to the repository».

рефутер

что посажено где какой пин обязан был упасть упал вердикт
Гейдж застрявших прогонов навсегда ноль на ШВЕ демона: StalledRuns: o.StalledRunsStalledRuns: 0 (наблюдаемая половина PD-346, стоявшего BLOCKER'ом) /home/ubuntu-26/tm-p8-review/r4-metrics/platform/cmd/tmplatformd/runner.go:309 носителя нет и быть не может: observe() — неэкспортированная функция пакета main, единственный тест-файл каталога (runner_test.go) её не зовёт НЕТ ВЫЖИЛ — подтверждает находку 2. Полную батарею для этого гонять не нужно: неэкспортированная функция пакета main достижима ровно из этого пакета, значит зелень здесь и есть зелень батареи.
Счётчик недошедших проходов не может вырасти: третий аргумент s.metrics.ObserveSweep(name, time.Since(start), errors.Is(err, context.DeadlineExceeded))false (механизм PD-351 со стороны вызывающего; сам реестр называет cmd/tmplatformd/runner.go pass местом того дефекта) /home/ubuntu-26/tm-p8-review/r4-metrics/platform/cmd/tmplatformd/runner.go:245 носителя нет; пин PD-351 TestAPassThatRanOutOfTimeSaysSo живёт в internal/runs/stalled_test.go:811 и проверяет, что Sweep ВОЗВРАЩАЕТ обёрнутый DeadlineExceeded НЕТ ВЫЖИЛ — подтверждает находку 2. Закрытый дефект возвращается в шов, который реестр называет своим адресом, а тест не смотрит.
Гейдж застрявших нацелен на ноль ВНУТРИ самой метрики (моя посадка, финдер её не делал): m.stalledRuns.Set(float64(r.StalledRuns))m.stalledRuns.Set(0) /home/ubuntu-26/tm-p8-review/r4-metrics/platform/internal/metrics/metrics.go:167 кандидат — TestTheRunnersStateIsExposedWithItsUnits, но его литерал Runner содержит шесть полей из восьми (metrics_test.go:96-97), а runs_stalled не ассертится ни в одном тесте репозитория НЕТ ВЫЖИЛ — расширяет находку 2: дыра не только в cmd/tmplatformd, она и в самом пакете метрик. Единственный сигнал P8-FIX о застрявшем прогоне не пинится ни на одном уровне.
КОНТРОЛЬ харнесса: счётчик недошедших проходов перестаёт считать — удалён m.sweepUnfinished.WithLabelValues(name).Inc() /home/ubuntu-26/tm-p8-review/r4-metrics/platform/internal/metrics/metrics.go:188 metrics.TestTheRunnersStateIsExposedWithItsUnits (§24 называет его пином формы) да Пойман. Ставился ради того, чтобы выживание предыдущей посадки в ТОМ ЖЕ файле нельзя было списать на неработающий прогон.
Из списка расчёта снята клауза отсрочки (механизм PD-353, на который опирается находка 3): where a.reconcile_after is null or a.reconcile_after <= $1where $1 = $1 /home/ubuntu-26/tm-p8-review/r4-metrics/platform/internal/pgstore/sink.go:695 ожидал покраснения: клауза объявлена «второй половиной фикса голодания» в доккомменте UnsettledRuns да Пойман двумя пинами — и это ВАЖНО для вердикта по находке 3: отсрочка в списке расчёта не баг, а ратифицированное запинённое поведение. Значит чинить надо AbandonRun (или сообщение), а не фильтр.
КОНТРОЛЬ границы: SQL наблюдения перестаёт считать застрявшие попытки — and a.reconcile_failures >= $1>= $1 + 1000 /home/ubuntu-26/tm-p8-review/r4-metrics/platform/internal/pgstore/observe.go:49 internal/runs stalled_test.go:310-316 («the gauge behind the CLI counts the same thing») да Пойман. Задаёт точную границу находки 2: ЗАПРОС наблюдения запинён, непокрыт ровно шов «запрос → гейдж».