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

17 KiB
Raw Blame History

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


Материал раунда

  • Ветка: issue/481-mutation-ledger, коммит 0f36eb1c1cc6 — ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет.
  • Дерево материала: 0e080a48f396df7d234779dc0f41146e50cc2180
    git log --all --format='%H %T' | grep 0e080a48f396