Files
houseplan-card/docs/reviews/CODE-REVIEW-558-r1.md
T
2026-09-13 17:44:19 +00:00

220 lines
18 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.
# CODE-REVIEW-558-r1
Issue: #558 · «Мутант: bounded refactor/оптимизация scripts/mutation-gate.mjs»
Материал: `c53fd1a54f7ac7e504372d48e93ea87ebdd4b802` (рабочая копия совпадает,
`origin/dev..HEAD` = один коммит). Заход r1, предыдущих раундов ревью по
этой задаче не было (первая передача в код-ревью была отменена конвейером до
чтения кода из-за конфликта ребейза — см. комментарий от 17:28, цикл не
израсходован).
## Скоуп
Инфраструктурная задача (issue без `S*`-меток, вход по прямому назначению
владельца, §1 PROCESS.md). Диапазон изменений — ровно класс B/C/ни одного
файла класса A:
```
docs/ARCHITECTURE.md | 16 +
docs/STATUS.md | 2 +-
docs/TESTING.md | 8 +-
scripts/check-inputs.mjs | 7 +-
scripts/mutation-evidence.mjs | 115 + (новый)
scripts/mutation-execution.mjs | 233 + (новый)
scripts/mutation-gate.mjs | 10161 +--- (10161 строк удалено, CLI-обвязка + ре-экспорт)
scripts/mutation-guard-outcome.mjs | 4 +-
scripts/mutation-registry.mjs | 9487 + (новый, декларации)
scripts/mutation-selection.mjs | 306 + (новый)
test/classify-changes.test.mjs | 12 +-
test/mutation-gate.test.mjs | 120 +-
test/smoke-exception-guard.test.mjs | 2 +-
```
Задача — bounded-рефакторинг: 9678-строчный монолит `scripts/mutation-gate.mjs`
разбит на `mutation-registry.mjs` (720→725 деклараций мутантов),
`mutation-selection.mjs` (отбор по диффу, guard-input closure, invocation-scoped
кэш), `mutation-evidence.mjs` (fingerprint + ledger) и `mutation-execution.mjs`
(worktree/сборка/запуск), при сохранении стабильного CLI и публичного
API-поверхности `scripts/mutation-gate.mjs`.
Продуктового кода, changelog нет (`User-Visible: no` — верно, трейлеры на
коммите корректны: `Issue: #558`, `User-Visible: no`).
## Как проверялось
Дешёвые гейты прогнаны лично на этом SHA (не только по ссылке на Validate,
т.к. объём диффа велик и трогает системообразующий инструмент ревью же):
| Гейт | Команда | Результат |
|---|---|---|
| typecheck | `npx tsc --noEmit` | green |
| unit | `npm test` | 2652 passed, 1 штатный skip — совпадает с числом автора после ребейза |
| unit (точечно) | `node --test test/mutation-gate.test.mjs` | 57/57 passed |
| mutation anchors | `node scripts/mutation-gate.mjs --check` | 725 ok, 0 FAIL |
| check-inputs coverage | `node scripts/check-inputs.mjs --coverage` | green (без вывода) |
| process-gate | `node scripts/process-gate.mjs --issues` | green, ожидаемый инфра-WARN §8 (#562) |
| дифф-режим (реальная проверка отбора) | `node scripts/mutation-gate.mjs --changed=origin/dev..HEAD --plan-only` | `мутантов затронуто 56 из 725 (по файлам 56, по определениям 12)` — согласуется с заявленными автором «12 tooling-мутантов» |
| smoke-select (по диффу) | `node scripts/smoke-select.mjs --base origin/dev --head HEAD` | «Browser-smoke этим диффом не выбираются» — `src/**` не тронут, смоки не нужны |
| Validate на материале | https://github.com/Matysh/houseplan-card/actions/runs/34771849370 | `success` на точном SHA `c53fd1a5` (сверено `headSha`) |
Не прогонялись и почему: `npm run build` (со сверкой копий бандла) —
покрыт зелёным Validate на этом же SHA, диффом бандл не затронут (класс D не
менялся); `golden:verify` — diff не меняет визуал (`src/**` не тронут);
`python -m pytest tests_backend` — Python не менялся; performance-профили —
не названы в AC и не затронуты (`src/iso-*`, `src/live-*`, `src/render-*`
не в диффе); `check-docs` — не обязателен, `src/**` не тронут.
### Верификация мутационно-защищённого AC (мутант в моём прогоне, не в реестре)
AC «Cache сбрасывается при изменении tree/input; одинаковая строка guard на
другом материале не наследует старый результат» защищён юнит-тестом
`test/mutation-gate.test.mjs` — «#558: invocation-scoped resolver computes
each unique guard once and cannot leak across trees». Тест чистый (не
браузерный/бэкенд-гвард), поэтому по §2.7 PROCESS.md достаточно прогона со
снятой защитой, приведённого здесь, отдельного мутанта в реестре не
требуется:
| AC | Чем доказан | Чем краснеет |
|---|---|---|
| Резолвер не даёт кэшу утечь между материалами/деревьями | `node --test --test-name-pattern="invocation-scoped resolver" test/mutation-gate.test.mjs` | Ввёл модульный `GLOBAL_LEAK_CACHE` вместо `cache = new Map()` внутри `createGuardInputResolver` (имитация утечки кэша между инстансами) → тест упал: `secondTree(guard)` вернул `['scripts/example-guard.mjs', 'test/a.test.mjs']` вместо ожидаемого `[...'test/b.test.mjs']` — старое дерево унаследовано новым резолвером. Патч откачен, дерево чистое (`git status` пусто) |
### Проверка обратной совместимости легаси-фолбэка (не покрыта отдельным unit-тестом — проверено вручную вживую)
`baseRegistry()` в `scripts/mutation-selection.mjs` сначала читает
`scripts/mutation-registry.mjs` из указанной базы, а при неудаче — старый
монолитный `scripts/mutation-gate.mjs` (комментарий в коде: «Compatibility
across the one-time #558 split»). Прямого юнита на этой ветке нет, но
`origin/dev` в этом самом ревью — ровно родитель коммита c53fd1a5, то есть
последний pre-#558 SHA с монолитным реестром. Проверено этим:
```
node -e "import('./scripts/mutation-selection.mjs').then(async m => {
console.log(await m.baseRegistry('origin/dev'));
})"
→ legacy base loaded: true 725
```
725 деклараций прочитаны из `origin/dev:scripts/mutation-gate.mjs` (легаси-
слой) без исключений — путь живой и работает на реальном историческом SHA,
не на синтетике. Recommendation (Low, не блокирует): закрепить эту ветку
отдельным юнитом на фикстуре, а не полагаться на то, что `origin/dev`
останется pre-#558 достаточно долго для случайной ре-проверки — см. «Низкое».
## Найдено и корректно (сверено построчно, не по заявлению автора)
- **Механическая идентичность execution-модуля.** Все 11 функций
`mutation-execution.mjs` (`applyPatches`, `makeWorktree`, `dropWorktree`,
`guardNeedsTestBuild`, `guardNeedsBundle`, `seedTestBuild`, `buildTestBuild`,
`buildBundle`, `printMutantOutcome`, `runMutant`, `runCleanGuards`) побайтово
идентичны своим прежним телам в `origin/dev:scripts/mutation-gate.mjs`
(сравнено программно, diff пуст для каждой).
- **Механическая идентичность большей части selection-модуля.** 10 из 13
функций идентичны байт-в-байт (`guardFiles`, `wrapperInputs`, `guardInputs`,
`anchorSpan`, `anchorRegion`, `parseDiffRanges`, `patchTouched`,
`packageJsonRelevance`, `registryDelta`, `shardMutants`). Три отличаются
осмысленно и ожидаемо: `selectChangedMutants` (ранний выход на пустом
`changed`, снимает лишний расчёт closure — чистая оптимизация, поведение то
же: старый код на пустом `changed` тоже возвращал `[]`, просто вычисляя
`inputsOf` вхолостую), `selectForDiff` (проверка «дифф трогает реестр»
теперь смотрит `MUTATION_REGISTRY_FILES` вместо жёстко зашитого имени
монолита — корректно, т.к. декларации переехали), `baseRegistry` (добавлен
легаси-фолбэк, см. выше).
- **Реестр деклараций перенесён без искажений.** Массив `MUTANT_DEFINITIONS`
побайтово идентичен прежнему, за вычетом ровно 12 записей — тех, чьи патчи
указывали на `scripts/mutation-gate.mjs` как на файл-цель (тулинговые
мутанты, проверяющие сам гейт). У них поле `file` механически переведено на
новый реальный файл (`mutation-selection.mjs` × 8, `mutation-evidence.mjs`
× 4), с точным совпадением числа, заявленного автором («12 tooling-мутантов
механически перенесены… все их якоря живы») — подтверждено количественно
(`git diff` по извлечённому массиву показывает ровно 12 строк
`file: 'scripts/mutation-gate.mjs'` замененных на новые пути) и поведенчески
(`--check` → 725/725 green на рабочей копии).
- **Публичный API-контракт сохранён.** `mutation-gate.mjs` ре-экспортирует всё,
что реально импортируют внешние потребители: `test/mutation-gate.test.mjs`
(17 символов), `test/device-presentation-policy.test.mjs` (`MUTANTS`),
`scripts/mutation-gate-report.mjs` (`MUTANTS` через динамический `import`).
Ни один внешний вызов не сломан — подтверждено запуском полного `npm test`
(импорт бросил бы ошибку загрузки модуля, тест-файл не прошёл бы вовсе).
- **Внешние entrypoints не тронуты.** `.github/workflows/mutation-gate.yml`,
`.github/workflows/validate.yml`, `scripts/pre-push-gate.mjs` вызывают
только стабильный `node scripts/mutation-gate.mjs …` — ни один не адресуется
к внутренним модулям напрямую.
- **Invocation-scoped резолвер не создаёт бессрочный кэш поверх ledger.**
`createGuardInputResolver` — фабрика с локальным `Map`, создаётся заново в
каждом вызове `main()` (`mutation-gate.mjs:43`); ledger (`mutation-evidence.mjs`)
остаётся отдельным явным персистентным слоем со своей схемой (`LEDGER_SCHEMA
= 2`, не менялась). Кэш и ledger не путаются: `splitByLedger` принимает
`fingerprintOf`, который может быть как кэширующим, так и нет —
эквивалентность проверена тестом «cached and uncached representative plans
and fingerprints are equivalent» (сам прогнал: green).
- **`MUTATION_REGISTRY_FILES`/`LEAF_FILES` синхронизированы с новой раскладкой.**
`scripts/check-inputs.mjs`: `LEAF_FILES` перенесён с `mutation-gate.mjs` на
`mutation-registry.mjs` (патч-данные, похожие на импорты, лежат теперь там),
`changed_mutants.entries` расширен до `scripts/mutation-*.mjs` — покрывает
все пять новых/оставшихся файлов. Проверено: `check-inputs --coverage`
green, `test/classify-changes.test.mjs` явно перечисляет все пять файлов
под ожиданием `mutants=true, frontend=true, backend=false`.
- **Документация обновлена в том же коммите и по существу.**
`docs/ARCHITECTURE.md` получил раздел «Mutation tooling boundaries (#558)» с
верным направлением зависимостей (CLI → границы, не наоборот);
`docs/TESTING.md` — актуализированы команды (`git show
<base>:scripts/mutation-registry.mjs` + упоминание легаси-фолбэка);
`docs/STATUS.md` — строка кандидата бета дополнена #558.
- **Измерение производительности не является голым заявлением.** Автор привёл
таблицу (9893→5319 чтений, 4661.9→2312.4 ms) и утверждение, что
selection/fingerprints/ledger outcomes совпадают до/после — это утверждение
закреплено исполняемым тестом («cached and uncached … equivalent»,
прогнан мной, green), а не декларацией.
## Находки
Нет находок уровня High или Medium.
**Low (снимаю с записью, не блокирует):** легаси-ветка `baseRegistry()` —
фолбэк на монолитный `scripts/mutation-gate.mjs` для базы старше #558 — не
покрыта отдельным юнит-тестом с фикстурой; она проверяется только косвенно,
через существование подходящего исторического SHA. Я вручную воспроизвёл
сценарий на `origin/dev` (реальный pre-#558 коммит, 725 деклараций прочитаны
корректно), так что путь сейчас работает и доказан этим ревью, но регрессия в
этой ветке в будущем не будет поймана автоматически, т.к. подходящий
исторический материал в конце концов перестанет попадать в типичные диапазоны
диффа. Автор сам пометил её как «one-time» совместимость — цена вреда низкая,
и правки в этой же задаче не требую; следующая правка `mutation-selection.mjs`
может добавить unit на синтетической фикстуре, если сочтёт нужным.
## Чего не проверял
- Полный `npm run build` со сверкой трёх копий бандла — не прогонял лично,
беру зелёным Validate на точном SHA (класс D в диффе не менялся, риска
расхождения нет).
- `golden:verify`, браузерные смоки, `pytest tests_backend`, performance-
профили — не прогонял: diff не касается `src/**`/`custom_components/**/*.py`
/рендера, `smoke-select.mjs` подтвердил «выбирать нечего».
- Абсолютные числа из измерительной таблицы автора (9893/5319 чтений,
4661.9/2312.4 ms на матрице 720 definitions/476 guards) не переисполнял
бит-в-бит — дорогая матрица не входит в дешёвые гейты; вместо этого
верифицирована сама *логика* эквивалентности (тест «cached and uncached…»)
и реальный дифф-прогон на этом дереве (56/725, 12 по реестру, кэш-хиты 952
из 1418 запросов) — направление и порядок величин совпадают с заявленным.
## Вердикт
Зелёный. Рефакторинг механический, границы модулей проверены построчным
сравнением с прежним монолитом, защитный AC (изоляция кэша между деревьями)
воспроизведён с реальной сломанной мутацией и красным тестом, легаси-путь
проверен вживую на настоящем историческом SHA, публичный API и внешние
entrypoints не нарушены, документация и трейлеры в порядке.
---
<!-- material-anchors: сгенерировано конвейером (#414) -->
## Материал раунда
- Ветка: `issue/558-mutation-modules`, коммит `c53fd1a54f7a` — ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет.
- Дерево материала: `6f2cdb8678dd01ac67a25fa926039c75f77b772a`
```
git log --all --format='%H %T' | grep 6f2cdb8678dd
```
- Тело issue: `d18e5fa3c60b241b22f7efd851e378e07998e55e947d136e36947a85e3484690`
- Вердикт конвейера: `green` · High 0