docs: review document for #569

Issue: #569
User-Visible: no
This commit is contained in:
claude[bot]
2026-09-14 07:04:18 +00:00
parent 542650234d
commit 84a7773e29
+183
View File
@@ -0,0 +1,183 @@
# 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 не тронут.
---
<!-- material-anchors: сгенерировано конвейером (#414) -->
## Материал раунда
- Ветка: `issue/569-revive-dead-witnesses`, коммит `542650234de6` — ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет.
- Дерево материала: `24c8aaaaf21d073e7036204940ce0d3cdf653de4`
```
git log --all --format='%H %T' | grep 24c8aaaaf21d
```
- Тело issue: `1d221cb3307af4063325876f6ea351b32f823d3c91363c98b9139e890fe3be03`
- Вердикт конвейера: `green` · High 0