15 KiB
CODE-REVIEW #569 · r1
Материал: 542650234de6f2bc92f0a5a73ba9c20febbf6f42 (ветка
issue/569-revive-dead-witnesses, один коммит поверх dev@7d08a28d).
Трек: инфраструктурный (единственный изменённый файл — scripts/mutation-registry.mjs;
ни одного файла класса A). Заход r1, блокирующих циклов израсходовано 0 из 4.
Скоуп
Ночной полный мутационный прогон по расписанию (2026-09-14, dev@60f93a10)
дал 726 из 735: девять именованных свидетелей не дошли до заявленного теста —
все девять падали с исходом ошибка подготовки до заявленного теста
(MUTATION_OUTCOME.SETUP, см. scripts/mutation-guard-outcome.mjs:15), а не
красили тест реальным падением. Задача — вернуть каждому из девяти способность
доказывать себя, не меняя смысла мутации («что именно ломает мутант» из поля
because).
Автор называет три независимых механизма отказа подготовки (не один, как предполагала гипотеза из #566/#568):
- Статически мёртвая ветка теряет сужение типов, сделанное выше (4 шт.:
strict-wall-barrier-accepts-degraded,version-recovery-auto-reloads-ordinary-view,summary-index-rebuilds-on-state-value,room-drafts-mirror-still-throws).if (false && …)/if (true) throw— компилятор помечает ветку недостижимой и не удерживает предшествующее сужение (unitedпослеif (united == null) continue;,_relationдо вариантаmismatch,previousпосле проверки непустоты, остаток функции после безусловногоthrow). Лечится условием, ложным/истинным в рантайме, но не статически (String(x) === 'mutant-never...',drafts.length >= 0). - Пустой литерал
[]выводится какnever[](2 шт.:space-copy-drops-existing-partitions,wall-draw-local-neighbour-dropped) — падают не сами мутанты, а их вызывающие (Property 'id' does not exist on type 'never'). Лечится сохранением типа при потере содержимого (geometryList(...).slice(0, 0),[] as string[]). - Несовпадение типов в самой замене (3 шт.:
lattice-unknown-fields-recursive,resize-audit-resolver-bypassed,vacuum-route-warning-stays-silent) —canonicalizeNumberобъявленаunknown, функция обязана вернутьT; подмена резолвера литералом сузила union и увела веткуresolution.reasonвnever; бракованная строковая меткаcaseломалаswitchпо union (TS2678). Лечится приведением типа (as T), сохранением вызова резолвера с перекрытием только вердикта ({ ...resolveSafeResize(...), enabled: true } as SafeResizeResolution) и возвратом валидных меток соответственно.
Как проверялось
Дешёвые гейты на этом SHA уже подтверждены зелёным Validate
(https://github.com/Matysh/houseplan-card/actions/runs/34815306997), поэтому
npx tsc --noEmit, npm test целиком и npm run build не перегонялись.
Прогнано мной лично:
-
node scripts/mutation-gate.mjs --check— 738 определений, 0 FAIL (совпадает с заявленным автором числом). -
Каждый из девяти именованных свидетелей отдельно,
node scripts/mutation-gate.mjs --id=<id>(это и есть единственный настоящий гейт этой задачи — не покрыт Validate, потому что мутация не гоняется в обычном CI):id результат strict-wall-barrier-accepts-degradedok … заявленный тест покраснел на мутанте— поймано 1 из 1version-recovery-auto-reloads-ordinary-viewто же — поймано 1 из 1 lattice-unknown-fields-recursiveто же — поймано 1 из 1 room-drafts-mirror-still-throwsто же — поймано 1 из 1 space-copy-drops-existing-partitionsто же — поймано 1 из 1 summary-index-rebuilds-on-state-valueто же — поймано 1 из 1 resize-audit-resolver-bypassedто же — поймано 1 из 1 vacuum-route-warning-stays-silentто же — поймано 1 из 1 wall-draw-local-neighbour-droppedто же — поймано 1 из 1 Все девять перешли от
ошибка подготовкик настоящему красному прогону (заявленный тест покраснел на мутанте) — это именно то, что задача обещает. Дисциплина «тест умеет падать» подтверждена исполнением, не чтением: сам--id=запуск и есть демонстрация падения на мутированном коде. -
Чтение диффа построчно (все 9 хунков
scripts/mutation-registry.mjs) — сверил каждый комментарий-обоснование с фактическим патчем и с полемbecauseтого же определения (см. «Разбор» ниже). -
git diff origin/dev...HEAD --stat— единственный файл, класс B (scripts/**), продуктовый код (класс A) не тронут ни строкой. -
Трейлеры:
Issue: #569,User-Visible: no— корректны (нет пользовательского поведения), второй changelog не требуется и не тронут.
Не прогонялись и почему:
check-docs.mjs— дифф не трогаетsrc/**.npm run invariants— дифф не трогает геометрию,layout,marker.space,open_spans.golden:verify— дифф не меняет рендер.- браузерные смоки — дифф не трогает
src/**/demo/**;smoke-select.mjsне применим (нет диффа во фронтенде). pytest tests_backend— дифф не трогаетcustom_components/**/*.py.- Полный ночной прогон целиком — дорогой (шардированный, часы), не нужен:
задача проверяется поштучным
--id=каждого из девяти названных свидетелей, что и есть точное определение приёмки (ближайший ночной прогон подтвердит это на реальном расписании, но это не гейт ревью).
Разбор
Для каждого из трёх механизмов проверил, что лечение не меняет смысл
мутации (поле because), а только делает мутированный код компилируемым:
- Мёртвая ветка → рантайм-ложь.
String(united.status) === 'mutant-never-degraded-extra'и аналоги гарантированноfalseв рантайме (значение никогда не равно специально придуманной строке), но компилятор больше не видит литеральнуюfalse, поэтому не сворачивает ветку вnever. Наблюдаемое поведение мутанта («веткаdegraded-extraникогда не обрабатывается») сохранено бит-в-бит. never[]→ сохранение типа при потере содержимого.geometryList(source.partitions).slice(0, 0)— вызов остаётся с правильным типом элемента, но.slice(0, 0)гарантированно возвращает пустой массив: ровно то же поведение, что и голый[], только типизированное.[] as string[]аналогично.- Приведение типа/резолвер/метка.
resize-audit-resolver-bypassed— самый тонкий случай: резолвер теперь реально вызывается, и только его вердикт перекрываетсяenabled: true. Автор честно оговорил в хендоффе, что слово «bypassed» в id стало шире правды. Проверил полеbecauseэтого определения (scripts/mutation-registry.mjs:2174-2175): текст не про «резолвер не вызывается», а про то, что «всегда-включённый аудит делает базовую линию бессмысленной» — это утверждение осталось точным, оно не меняется. Расхождение только в имени id, а id — ключ ledger; автор аргументированно не стал его переименовывать (переименование обнулило бы накопленные доказательства). Не блокирует, отдельного Low не завожу — автор уже назвал это явно и оставил решение ревьюеру: текстbecauseне требует правки, потому что он не утверждает ничего опровергнутого. vacuum-route-warning-stays-silent— заменилcase 'нет такого'(заведомо не входящий в union, ломавший самswitch) на исходные валидные метки сreturn nullвместоreturn resolution.kind. Смысл мутации («предупреждение теряется молча») сохранён: функция теперь возвращаетnullвместо кода предупреждения — то же наблюдаемое поведение, что описано вbecause.
Все девять патчей — точечная правка только внутри своего patches[].replace;
find-якоря не менялись (кроме случаев, где это и есть исправление), поэтому
--check (738/0 FAIL) подтверждает, что реестр не разошёлся с деревом ни в
одном из 738 определений, включая не тронутые этой задачей.
Что проверено и корректно
- Инфраструктурный трек подтверждён: диапазон
origin/dev...HEADсодержит один коммит, единственный файл —scripts/mutation-registry.mjs. - Все девять свидетелей лично прогнаны через
--id=и даютзаявленный тест покраснел на мутанте— приёмочное условие задачи выполнено исполнением, не чтением. --checkзелёный на всём реестре (738/0), правка не осиротила ни один якорь, включая непричастные к задаче 729 определений.- Каждое лечение проверено против поля
becauseсвоего определения — смысл мутации не размыт, изменился только способ гарантировать компилируемость. - Трейлеры корректны:
Issue: #569,User-Visible: no, второй changelog не нужен. - Явно названный owner'ом отказ от более общего решения (статический запрет
на
if (false …)) обоснован исполняемой проверкой (#568: 45 живых мутантов используют этот идиому) и не входит в скоуп этой задачи — не считаю это недоработкой.
Чего не проверял
npx tsc --noEmit,npm testцеликом,npm run build— зелёный Validate на этом точном SHA уже подтвердил.- Полный ночной мутационный прогон целиком (все ~735-738 мутантов, 4 шарда) —
дорогой, не входит в гейт ревью; приёмка задачи по определению — поштучный
--id=для девяти названных, что и прогнано. - Остальные 45 мутантов с идиомой
if (false …), не входящие в список девяти — они вне скоупа задачи (задача чинит только те, что реально красили ночной прогон), их прогон не требуется для этого AC.
Вердикт
Зелёный. Находок нет — High: 0, Medium: 0. Все девять свидетелей лично
перепроверены --id= и подтверждено, что каждый переходит от «ошибка
подготовки» к настоящему красному исходу на мутированном коде; реестр
остаётся согласованным целиком (--check: 738/0 FAIL). Инфраструктурный трек,
трейлеры корректны, класс A не тронут.
Материал раунда
- Ветка:
issue/569-revive-dead-witnesses, коммит542650234de6— ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет. - Дерево материала:
24c8aaaaf21d073e7036204940ce0d3cdf653de4git log --all --format='%H %T' | grep 24c8aaaaf21d - Тело issue:
1d221cb3307af4063325876f6ea351b32f823d3c91363c98b9139e890fe3be03 - Вердикт конвейера:
green· High 0