Files
houseplan-card/docs/reviews/CODE-REVIEW-481-r1.md
T
2026-09-06 21:14:31 +00:00

154 lines
17 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-481-r1
Issue: #481 — `changed_mutants`: журнал пойманных свидетелей вместо суда по диапазону
Этап: код-ревью (PROCESS.md §2.7) · трек: `small` (спека — в теле issue, спек-ревью — r1 жёлтый → r2 зелёный, оба принятых Medium закрыты текстом issue)
Заход: r1 · блокирующих циклов 0 из 2 (бюджет код-ревью, отдельный от спек-ревью)
SHA материала: `df4104ab386fd339d9bf99d61573215b0bc869da` (сверено `git rev-parse HEAD` непосредственно перед выводом)
## Материал раунда
`git log --oneline origin/dev..HEAD`:
```
df4104ab docs: review document for #481 (класс C, коммит шага публикации спек-ревью r2 — не код автора)
f8ae546f test: бамп версии не меняет отпечатки мутантов ядра; бандл пересобран
c2913b28 docs: refresh screenshot source fingerprint
52aa3323 docs: журнал свидетелей в TESTING.md
97aabddc ci: журнал пойманных свидетелей для changed_mutants
```
Верхний коммит (`df4104ab`) добавляет только `docs/reviews/SPEC-REVIEW-481-r2.md` —
это собственный артефакт конвейера (публикация принятого спек-ревью), не код автора
задачи; в код-ревью не разбирается. Четыре нижних коммита — предмет этого раунда.
Это первый заход код-ревью (`циклов 0/2`), разбор полный.
Диапазон `origin/dev...HEAD`: 42 файла. Класс A (`src/**`, `custom_components/**/*.py`)
не тронут ни одним файлом — задача целиком в классах B (`scripts/**`, `test/**`,
`.github/workflows/**`) и C (`docs/**`), плюс класс D (`dist/**`,
`custom_components/houseplan/frontend/**`) как механическое следствие того, что
`scripts/source-fingerprint.mjs` входит в собственный корпус отпечатка сборки —
поэтому бамп корпуса потребовал пересборки трёх копий бандла и обновления
`docs/images/screenshots.json`, без единого изменённого пикселя (зафиксировано в
сообщении коммита `c2913b28` и проверено мной запуском `check-docs`, см. ниже).
Трейлеры на всех четырёх коммитах: `Issue: #481`, `User-Visible: no` — верно,
видимого поведения продукта нет; оба changelog не тронуты и не должны быть.
## Как проверялось (гейты)
Зелёного Validate на `df4104ab` нет — прогнал дешёвые гейты сам, на этом SHA:
| Гейт | Команда | Результат |
|---|---|---|
| Типы | `npx tsc --noEmit` | ok, без ошибок |
| Юниты | `npm test` | 2163 pass / 0 fail / 1 skipped (2164 всего) |
| Сборка + 3 копии бандла | `npm run bundle:sync` | бандл собран, `custom_components/…/frontend` и `demo/srv/assets` синхронны, `git status` после — пусто |
| Бюджет бандла | `npm run bundle:budget` | ok, initial View 288573 Б (потолок 289500±2000); предупреждение о запасе 12493 Б — долг #367, предсуществующий, этим диффом не создан |
| Документация/скриншоты | `node scripts/check-docs.mjs` | `Documentation checks passed (7 files, 12 external links)` |
| Малый гейт целиком | `node scripts/gate-small.mjs` | все 6 шагов ok; `smoke-select` подтвердил: «Исполняемого frontend-диффа нет (`src/**/*.ts` не тронут)» — браузерные смоки этим диффом не выбираются, гонять нечего |
| Провенанс коммитов | `node scripts/validate-commit-provenance.mjs` | exit 0 |
`src/**` диффом не задет — правило «check-docs обязателен, если диффа касается src/**»
формально не применяется, но я всё равно прогнал его из-за самоссылки корпуса
(`source-fingerprint.mjs` есть в `fingerprintFiles()`) — зелёный.
Не прогонял и почему:
- **browser smokes (`demo/smoke_*.mjs`)** — `smoke-select` по диффу не выбрал ни одного:
фронтенд (`src/**`) не тронут, смоки гоняют собранную карточку, а не скрипты гейта.
- **`npm run golden:verify`** — диффом не меняется ни рендер, ни геометрия, ни стили.
- **`python -m pytest tests_backend -q`** — `custom_components/**/*.py` не тронут.
Попутно (не как обязательный гейт, а как часть интеграционной проверки AC2 ниже)
наткнулся на `FAIL … python3 -m pytest … No module named pytest` — это дыра
окружения ревьюера (pytest не установлен), а не регресс диффа: соответствующий
мутант к #481 отношения не имеет и не входит в реестр этой задачи.
- **инварианты модели (`npm run invariants`)** — диффом не задеты рёбра комнат,
толщина, `layout`, `marker.space`, `open_spans`.
- **perf-профили** — не названы в AC, чувствительные пути не тронуты.
## AC → доказательство → чем краснеет (PROCESS.md §2.7, «таблица красноты»)
| AC | Чем доказан | Чем краснеет (мутация/негативная проба, результат) |
|---|---|---|
| AC1 (отпечаток версионно-неизменен, но чувствителен к правке патча/гарда/объявления, CRLF-нейтрален) | `node --test --test-name-pattern="#481 AC1" test/mutation-gate.test.mjs` | Свидетель `ledger-version-sensitive` (патч убирает вызов `normalize`): `node scripts/mutation-gate.mjs --id=ledger-version-sensitive` → `тест покраснел, как обязан`, поймано 1 из 1. Я вручную применил тот же патч к рабочей копии — `#481 AC1` падает: `AssertionError: бамп версии — не изменение свидетеля`, хеши расходятся |
| AC2 (`--changed --ledger`: совпавший отпечаток → пропуск, чужой/отсутствующий → в прогон) | `node --test --test-name-pattern="#481 AC2" test/mutation-gate.test.mjs` (unit на `splitByLedger`) | Unit доказывает саму функцию отбора; **интеграционного CLI-теста на фикстуре, обещанного ТЗ («unit + CLI на фикстуре»), в диффе нет** — см. находку Low ниже. Я воспроизвёл интеграцию вручную: посчитал реальный отпечаток мутанта `ledger-version-sensitive` через `witnessFingerprint`, положил его в журнал и прогнал `node scripts/mutation-gate.mjs --changed=origin/dev..HEAD --ledger=<seed>` на настоящем реестре → `по журналу пропущено 1 (отпечатки совпали), к прогону 7` при 8 отобранных диффом — счёт verно, только помеченный мутант пропущен |
| AC3 (журнал пишется сразу после каждого пойманного; сбежавший не пишется; переживает битый/чужой журнал) | `node --test --test-name-pattern="#481 AC3" test/mutation-gate.test.mjs` | Свидетель `ledger-written-at-end-only` (запись только после >1 записи): `--id=ledger-written-at-end-only` → поймано 1 из 1. Я применил патч вручную — тест падает на первой записи: `ENOENT …/nested/ledger.json` (файла ещё нет, потому что запись отложена) |
| AC4 (`--ledger` без `--changed` → код 2, внятное сообщение) | `#481 AC4` (spawnSync реального CLI) | Убрал вручную блок отказа в `main()` — тест падает: `1 !== 2` (без отказа код возврата другой) |
| AC5 (`validate.yml`: restore по шарду с `restore-keys`, `save` `if: always()`, уникальный на прогон ключ, `--ledger` в команде) | `test/validate-workflow.test.mjs`, тест «журнал свидетелей changed_mutants…» | Убрал вручную `if: always()` из шага `Сохранить журнал свидетелей` — тест падает: `красный или отменённый шард обязан сохранить уже пойманное` |
| AC6 (структурная реконструкция #480: отбор по диффу не менялся, сужение — только `splitByLedger` по отпечатку, версия-инвариантность — следствие AC1) | ревью кода (метод согласован спек-ревью r2) | Прочитано: `selectChangedMutants`/`guardFiles` (#475) в диффе не изменены (сравнение `git diff` — ноль строк). Числа автора (`134→2`, `113 из 113`) — иллюстрация, не пересчитывались; структурный вывод подтверждён чтением, не цифрами |
| AC7 (три негативных свидетеля, каждый «поймано 1 из 1») | `node scripts/mutation-gate.mjs --id=<witness>` по каждому из трёх | `ledger-records-escaped` → 1/1, `ledger-version-sensitive` → 1/1, `ledger-written-at-end-only` → 1/1. Все три реально приводят к красному тесту при обратном патче (проверено запуском, не чтением) |
Примечание к AC7/поиску по `ledger-records-escaped`: у этого мутанта два патча
(один в `main()`, второй в `splitByLedger`). Патч в `main()` в изоляции тесты
`#481 AC2`/`AC3` не красит (они не вызывают `main()`); ловит его именно второй
патч — расширение сравнения в `splitByLedger` (`|| mutant.id in ledger.caught`),
которое `#481 AC2` красит немедленно. Проверил оба патча по отдельности и вместе,
итог совпадает с официальным прогоном — мутант пойман, находка не подтвердилась.
## Находки
Нет High и Medium. Два Low, оба сняты без правки (в рамках лёгкого трека и
корректности остального изменения не блокируют):
1. **Low.** AC2 задокументирован в ТЗ доказательством «unit на функцию отбора +
CLI на фикстуре», но в диффе — только unit на `splitByLedger`; CLI-интеграция
`main()` (чтение журнала → отбор → запись) не покрыта отдельным тестом. Я
воспроизвёл интеграцию вручную на реальном реестре (см. таблицу AC2 выше) и
она работает верно, поэтому AC для этого раунда доказан исполнением, а не
находка блокирует. Снимаю с записью: будущая правка `main()` (например,
случайная перестановка `run`/`skipped`) не будет поймана ни одним из текущих
тестов — стоит закрыть отдельным маленьким CLI-тестом на фикстуре при первой
же следующей правке этого файла, не обязательно прямо сейчас.
2. **Low.** `scripts/source-fingerprint.mjs`: над `withoutProductVersion`
остались рядом два JSDoc-блока — старый (описывающий прежнее инлайновое имя
отбора, #245) и новый (#481); только второй, ближайший, будет прочитан
инструментами документации, первый становится сиротой. Плюс `productVersion`
экспортирован, но нигде за пределами этого файла не импортируется — сейчас
не используется extern. Ни то, ни другое не влияет на поведение или гейты;
снимаю без правки, чисто косметическое.
## Что проверено и корректно
- `witnessFingerprint`: сортировка файлов детерминирована, JSON.stringify
объявления мутанта включён в хеш, нормализация версии — через общий
`withoutProductVersion` (не дублирует логику `visualFingerprint`, а
`visualFingerprint` теперь сам через неё же — рефактор без изменения
поведения, отпечаток скриншотов обновился только из-за самоссылки корпуса,
не из-за смены алгоритма).
- `readLedger`: отсутствующий/битый/чужой-схемой файл → пустой журнал
(«гонять всё») — три случая закрыты тестом AC3 и вручную перепроверены.
- `recordCaught`/`splitByLedger`: инкрементальная запись, сравнение именно
отпечатка (не факта присутствия id) — оба свойства пойманы негативными
пробами.
- `main()`: `--ledger` без `--changed` — код 2; `--ledger` с `--changed` —
сужает уже отобранный по диффу набор, не расширяет; при пустом плане после
журнала — явное сообщение и код 0, не ошибка.
- Полный прогон (`mutation-gate.yml`, `--check`, `--id`), локальный сценарий
(`pre-push-gate.mjs`, `--changed` без `--ledger`) — журнал не трогают,
подтверждено чтением вызовов (`grep -rn "mutation-gate.mjs"` по workflow и
scripts), совпадает с п.5 ТЗ «что не меняется».
- `validate.yml`: `cache/restore` до шага прогона, `cache/save` после, с
`if: always()`; ключ уникален на `run_id`, `restore-keys` — префикс по
шарду. Порядок шагов и присутствие обоих действий проверены и тестом, и
чтением самого файла.
- Трейлеры, разбиение на коммиты по классам (B/C/D), `User-Visible: no` без
правок changelog — верно.
- Ни один файл `src/**` или `custom_components/**/*.py` не тронут — гейты
golden/smoke/backend/invariants обоснованно не требуются этим диффом.
## Вердикт
Вердикт: зелёный · заход r1 · блокирующих циклов 0/2 · High: 0 · Medium: 0 → в задаче
Документ: docs/reviews/CODE-REVIEW-481-r1.md
---
<!-- material-anchors: сгенерировано конвейером (#414) -->
## Материал раунда
- Ветка: `issue/481-mutation-ledger`, коммит `0f36eb1c1cc6` — ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет.
- Дерево материала: `0e080a48f396df7d234779dc0f41146e50cc2180`
```
git log --all --format='%H %T' | grep 0e080a48f396
```