From c1e6199b46e90279af5d6aca03b795e6ee3dae51 Mon Sep 17 00:00:00 2001 From: "claude[bot]" <209825114+claude[bot]@users.noreply.github.com> Date: Sun, 6 Sep 2026 21:14:31 +0000 Subject: [PATCH] docs: review document for #481 Issue: #481 User-Visible: no --- docs/reviews/CODE-REVIEW-481-r1.md | 153 +++++++++++++++++++++++++++++ 1 file changed, 153 insertions(+) create mode 100644 docs/reviews/CODE-REVIEW-481-r1.md diff --git a/docs/reviews/CODE-REVIEW-481-r1.md b/docs/reviews/CODE-REVIEW-481-r1.md new file mode 100644 index 00000000..0e53c1f7 --- /dev/null +++ b/docs/reviews/CODE-REVIEW-481-r1.md @@ -0,0 +1,153 @@ +# 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=` на настоящем реестре → `по журналу пропущено 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=` по каждому из трёх | `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 + +--- + + + +## Материал раунда + +- Ветка: `issue/481-mutation-ledger`, коммит `0f36eb1c1cc6` — ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет. +- Дерево материала: `0e080a48f396df7d234779dc0f41146e50cc2180` + ``` + git log --all --format='%H %T' | grep 0e080a48f396 + ```