Files
houseplan-card/docs/reviews/CODE-REVIEW-569-r1.md
T
2026-09-14 07:04:18 +00:00

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):

  1. Статически мёртвая ветка теряет сужение типов, сделанное выше (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).
  2. Пустой литерал [] выводится как 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. Несовпадение типов в самой замене (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-degraded ok … заявленный тест покраснел на мутанте — поймано 1 из 1
    version-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 — ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет.
  • Дерево материала: 24c8aaaaf21d073e7036204940ce0d3cdf653de4
    git log --all --format='%H %T' | grep 24c8aaaaf21d
    
  • Тело issue: 1d221cb3307af4063325876f6ea351b32f823d3c91363c98b9139e890fe3be03
  • Вердикт конвейера: green · High 0