Files
houseplan-card/docs/reviews/CODE-REVIEW-113-r2.md
T
2026-08-18 23:59:36 +00:00

19 KiB
Raw Blame History

CODE-REVIEW-113-r2 — honest optional contract for _spaceModel(), post-rebase re-check

  • Issue: https://github.com/Matysh/houseplan-card/issues/113
  • ТЗ: docs/specs/113-optional-space-model.md, ревью ТЗ — docs/reviews/SPEC-REVIEW-113-r1.md (зелёный, Medium-1 → #184, не блокирует)
  • Предыдущий цикл: docs/reviews/CODE-REVIEW-113-r1.md — зелёный, High 0, Medium 0. Слияние автоматически не прошло (конфликт issue/113-optional-space-model ↔ dev), поэтому issue вернулся в S6-in-progress не за переделку кода, а за ребейз (AGENTS.md «Two-agent workflow»: «после ребейза на сдвинутый dev это другой код, и принимать его без проверки нельзя»).
  • Диапазон: origin/dev..HEAD, origin/dev = c1676cf (docs: review document for #117). git merge-base origin/dev HEAD = c1676cf — ветка полностью перебазирована, дивергенции нет.
  • Коммиты (5): 0c2a5de (specify contract), 135497b (spec review doc), 1e8503b (реализация — «Make empty space model explicit», это переехавший 09b5a74 из r1), 047363c (r1 code-review doc), 66fa8f4 (новый после r1 — «Keep mutation anchor aligned after optional model change»).
  • Цикл: r2/4 (счётчик по этапу код-ревью; ревью ТЗ отдельно на r1)
  • Роль: ревьюер кода (Claude), свежая сессия, без контекста реализации автора и без контекста собственного прогона r1.

1. Скоуп цикла r2

dev продвинулся между r1 и этим ребейзом: слиты #132/#157 (hosted/partition openings) и #173 (unified wall topology). Автор перебазировал issue/113-optional-space-model на новый dev (c1676cf) и разрешил конфликты, сохранив обе стороны. Это другой код, чем тот, что получил зелёный вердикт в r1 — задача этого цикла не «доверять r1», а заново пройти по чек-листу §2.7 на итоговом дереве.

Diff origin/dev...HEAD (16 файлов) идентичен по содержанию r1 плюс один новый коммит:

  • 66fa8f4 меняет только scripts/mutation-gate.mjs: якорь мутации same-space-room-change-recenters использовал буквальный текст prevPos.s === targetSpace, но ребейз/реализация переименовала локальную переменную в targetSpaceId (проверено чтением src/houseplan-card.ts:12908,13018,13033 — переменная везде называется targetSpaceId, старого имени targetSpace в файле нет). Мутационный якорь — точное совпадение строки, поэтому несовпадающий текст сделал бы мутационный тест тихо неработающим (find не совпадает → патч не применяется → «поймано» покажет ложный OK, а не реальную проверку). 66fa8f4 возвращает якорь к соответствию исходнику.
  • Issue: #113 / User-Visible: no на месте на всех 5 коммитах, включая новый.

Остальные 15 файлов — то же содержание, что было предметом r1 (проверено git diff: единственный новый функциональный код появляется только в 66fa8f4). Часть строк diff'а (_saveMarker, partitions) теперь физически стоит рядом с кодом #132/#157/#173, слитым в dev до ребейза — эти call sites _spaceModel()/.partitions уже были в скоупе оригинальной реализации (видно по targetSpaceModel, space.partitions/model.partitions в diff'е — не новые правки этого цикла, перенеслись без изменений при ребейзе). Отдельно проверено: слитый в dev код не добавил новых необработанных обращений к _spaceModel() вне diff'а этой ветки (см. §3).

User-Visible: no остаётся в силе без изменений (§13 ТЗ), changelog не трогается — согласовано.

2. Как проверялось — таблица гейтов

Гейт Команда Результат
Typecheck npx tsc --noEmit OK, без ошибок
Unit npm test 877/877 OK (было 811/811 в r1 — рост за счёт тестов, слитых в dev вместе с #132/#157/#173, не связан с этим диффом)
Build + 3 копии бандла npm run build, затем cmp dist/houseplan-card.js custom_components/houseplan/frontend/houseplan-card.js и cmp dist/houseplan-card.js demo/srv/assets/houseplan-card.js OK, обе сверки — байт-в-байт совпадение
Именованный smoke (AC3–AC6) node demo/smoke_optional_space_model.mjs OK, все 9 подпроверок true
Именованный smoke (AC6, read-only cold start) node demo/smoke_readonly_cold_start.mjs OK, все 13 подпроверок true
Мутационный guard AC10/AC4/AC5 (не менялся этим циклом) node scripts/mutation-gate.mjs --id=empty-space-cleanup-disabled поймано 1 из 1, чистый прогон зелёный
Мутационный guard, якорь которого правит 66fa8f4 node scripts/mutation-gate.mjs --id=same-space-room-change-recenters поймано 1 из 1 — якорь совпал с исходником, патч применился, smoke_subarea.mjs покраснел, как обязан. Это прямое подтверждение, что правка якоря не сделала тест немым
Именованный smoke затронутого якоря node demo/smoke_subarea.mjs (чистое дерево) OK, все 14 подпроверок true
Regression: поверхности, слитые из #132/#157/#173, рядом с которыми правился diff (_saveMarker/partitions) node demo/smoke_partition_openings.mjs, node demo/smoke_unified_wall_tool.mjs, node demo/smoke_room_autoclose.mjs OK, все зелёные
Process gate node scripts/process-gate.mjs --issues диапазон origin/dev..HEAD, коммитов 5, гейт пройден, предупреждений 0
Контракт call sites grep -n "_spaceModel()!" src/houseplan-card.ts и grep -n "_spaceModel()\s*\." src/houseplan-card.ts | grep -v "?\." пусто — ни non-null assertion, ни naked deref после _spaceModel() в production source

Не прогонялось, и почему:

  • Полный набор 135(+) браузерных смоков — не расширился в этом цикле относительно r1: этот ребейз не добавил новых поверхностей, кроме тех трёх (partition_openings, unified_wall_tool, room_autoclose), которые были рядом с изменёнными строками и прогнаны выше. Остальное логическое обоснование r1 (guard, недостижимый при непустой модели) не изменилось — новый функциональный код этого цикла ограничен одной правкой строки в тестовом скрипте, не в продукте.
  • npm run golden:verify — не прогонялся; тот же аргумент, что и в r1 (guard добавлен до существующей логики и физически недостижим при непустой модели), плюс этот цикл не меняет ни одной строки src/houseplan-card.ts относительно r1 — только scripts/mutation-gate.mjs (класс B, не влияет на визуальный результат).
  • python -m pytest tests_backend — ни один файл custom_components/**/*.py не тронут ни в этом коммите, ни в диапазоне.
  • Performance-профили — не названы в AC, hot path не тронут этим циклом (единственная правка — тестовый скрипт).
  • Остальные смоки вне таблицы — сознательно не прогонялись; сужение соразмерно объёму нового кода цикла (одна строка в mutation-gate.mjs), а не всей задаче #113 заново.

3. Проверка AC (docs/specs/113-optional-space-model.md §10) — переподтверждение после ребейза

Продуктовый код (src/houseplan-card.ts, src/space-model-selection.ts) идентичен принятому в r1 (проверено git diff — единственный новый коммит цикла трогает только scripts/mutation-gate.mjs), поэтому доказательства AC1–AC9 из r1 переносятся без изменений; ниже — что перепроверено заново на итоговом дереве, а не просто процитировано.

AC Статус в r2
AC1–AC2 (optional тип, отсутствие non-null assertions) перепроверено заново: tsc --noEmit зелёный на итоговом дереве; grep на _spaceModel()!/naked deref — пусто (см. §2)
AC3–AC6 (empty lifecycle, delete-last, pending action abort, read-only cold start) перепроверено исполнением: smoke_optional_space_model.mjs, smoke_readonly_cold_start.mjs — оба зелёные на итоговом дереве после ребейза
AC7 (non-empty View/editors pixel/action parity) перепроверено smoke-регрессией на поверхностях, слитых из #132/#157/#173 и физически соседствующих с diff'ом (partition_openings, unified_wall_tool, room_autoclose, subarea) — все зелёные; логический аргумент r1 (guard недостижим при непустой модели) не изменился, т.к. продуктовый код не менялся
AC8 (stale explicit id не мутирует первый space) код не менялся; test/space-model-selection.test.mjs в составе зелёного npm test
AC9 (active-id fallback) код не менялся; тот же unit-тест
AC10 (type/source gates ловят регрессию сигнатуры) перепроверено исполнением заново — empty-space-cleanup-disabled (не менялся) и, отдельно, мутационный тест, чей якорь чинит этот цикл (same-space-room-change-recenters): подтверждено, что после правки якорь совпадает с исходником и мутация по-прежнему ловится (§2, §4)

4. Дисциплина «тест умеет падать» — что именно проверено исполнением в этом цикле

Ядро этого цикла — не переисполнение доказательств r1 (код там не менялся), а верификация того, что единственная новая правка (66fa8f4) не превратила существующий мутационный гейт в фиктивный:

  • До правки старый якорь (prevPos.s === targetSpace) не совпал бы ни с одной строкой текущего src/houseplan-card.ts (переменная называется targetSpaceId во всех трёх местах использования — проверено grep'ом). Несовпадающий find в scripts/mutation-gate.mjs не бросает ошибку — он просто не применяет патч, тест против чистого дерева проходит с эффективно нулевым мутационным покрытием, а поймано 1 из 1 в CI осталось бы зелёным без реальной проверки — то есть перед 66fa8f4 этот конкретный guard тихо перестал бы что-либо ловить.
  • Прогон node scripts/mutation-gate.mjs --id=same-space-room-change-recenters на текущем дереве подтверждает обратное: find совпал, патч применился, smoke_subarea.mjs покраснел — тест умеет падать по факту исполнения, не предположительно.
  • То же самое для empty-space-cleanup-disabled (якорь не менялся этим циклом, но перепрогнан заново на итоговом дереве после ребейза, а не процитирован из r1): поймано 1 из 1.

5. Находки

Нет находок High. Нет находок Medium.

Два Low из r1 (стейл _splitSel/_mergeDialog/_wallDialog при пустом переходе; недостижимый защитный if (!space) return nothing в render()) относятся к продуктовому коду, который в этом цикле не менялся — они разобраны и сняты в docs/reviews/CODE-REVIEW-113-r1.md §5, повторно не переоткрываю.

6. Что проверено и корректно

  • git merge-base origin/dev HEAD совпадает с origin/dev — ветка действительно линейно перебазирована, диапазон origin/dev..HEAD не содержит посторонних коммитов и не пропускает ни один слитый коммит основной линии.
  • Продуктовый diff (src/houseplan-card.ts, src/space-model-selection.ts, docs/ARCHITECTURE.md, docs/TESTING.md, тесты, smoke) идентичен r1 построчно — единственная дельта цикла ограничена scripts/mutation-gate.mjs (класс B).
  • Call sites, физически расположенные рядом со слитыми из #132/#157/#173 фичами (_saveMarker targetSpaceModel/targetSpaceId, .partitions в _openPairs/соседних хелперах), уже были частью оригинальной классификации #113 (видно по targetSpaceModel/space.partitions в diff'е — не новые изменения этого цикла); слитый код сам по себе использует новую optional-сигнатуру корректно там, где обращается к _spaceModel() (например, _spaceModel()?.partitions в коде, пришедшем из dev) — проверено чтением и не нарушено ни одним из трёх smoke по этим поверхностям.
  • docs/ARCHITECTURE.md: баннер «Updated» корректно продвинут на дату ребейза, инвариант optional-space описан консистентно с кодом; docs/TESTING.md чек-лист ссылается на реальные smoke/unit/mutation имена, все они существуют и зелёные.
  • Трейлеры на всех 5 коммитах диапазона: Issue: #113, User-Visible: no — включая новый 66fa8f4. node scripts/process-gate.mjs --issues зелёный, 0 предупреждений.
  • tsconfig.test.json и docs/specs/README.md — механические регистрации нового файла/записи, соответствуют содержимому.

7. Чего не проверял

  • Полный браузерный набор смоков и golden:verify — обоснование в §2 (нулевой продуктовый diff относительно r1, узкий новый код цикла — одна строка в тестовом скрипте).
  • pytest tests_backend — класс A/B бэкенда не тронут.
  • Performance-профили — не названы в AC, hot path не тронут ни в r1, ни в этом цикле.
  • Содержательный повторный аудит AC1–AC9 «с нуля» без опоры на r1 — не требуется: код, который они доказывают, не менялся байт в байт (проверено git diff между продуктовым кодом r1 и текущим деревом), поэтому это не «доверие прошлому вердикту», а подтверждённый факт отсутствия изменений в этой части.
  • Medium-1/#184 (fallback-семантика ТЗ §6) — по-прежнему отдельный issue, не в скоупе этого код-ревью и не в скоупе #113.

8. Вердикт

Ребейз на c1676cf не изменил ни строки продуктового кода #113: единственная новая правка цикла — восстановление мутационного якоря, сломанного переименованием переменной в результате мержа. Правка проверена исполнением на способность ловить регрессию (не голословно), все фаст-гейты и релевантные smoke зелёные на итоговом дереве, AC1–AC10 подтверждены заново (без изменений там, где код не менялся, и напрямую — там, где менялся). High: 0. Medium: 0.

Вердикт: зелёный · цикл r2/4 · High: 0 · Medium: 0 → нет · Документ: docs/reviews/CODE-REVIEW-113-r2.md