mirror of
https://github.com/Matysh/houseplan-card
synced 2026-10-02 21:01:21 +00:00
154 lines
17 KiB
Markdown
154 lines
17 KiB
Markdown
# 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
|
||
```
|