# Посадки мутаций восьми субагентов пака 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 - spent` → `remaining := 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 = , 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 = 100` → `u.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.Minute` → `1 * 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) < MaterializeBudget` → `time.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.JobTimeout` → `time.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.StalledRuns` → `StalledRuns: 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.Pattern` → `route := 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.StalledRuns` → `StalledRuns: 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 <= $1` → `where $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: ЗАПРОС наблюдения запинён, непокрыт ровно шов «запрос → гейдж». |