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

103 lines
40 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Посадки мутаций восьми субагентов пака 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 = <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 = 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: ЗАПРОС наблюдения запинён, непокрыт ровно шов «запрос гейдж». |