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