Issue: #264 User-Visible: no
22 KiB
CODE-REVIEW-264-r1
- Issue: #264 — отдельный контроллер Resize и единый инвариант сохранения записей толщины стен
- Этап: код-ревью (PROCESS.md §2.7), заход r1, блокирующих циклов израсходовано 0/4
- Ветка:
issue/264-resize-controller, HEAD722f26a3c5500c3c7bc3c9dba33e505cc9bd385f - merge-base с
origin/dev:78850a87331680c89f321878734d3a33b228b142= tiporigin/dev— без ребейза - Спецификация:
docs/specs/264-resize-controller.md, принята зелёным вердиктомdocs/reviews/SPEC-REVIEW-264-r2.md
Скоуп проверки
Диапазон — весь git diff origin/dev...HEAD (33 файла, +3955/-2575), пять
продуктовых/тестовых коммитов поверх спеки:
a3e32995,6e1aea3a,c46c714c,536b750d,ef32d232— спецификация и её ревью (уже принято r2, не пересматривается здесь заново по существу, но читалось целиком как контракт для кода);ba66698f— «refactor: extract resize controller» (сам код);4a833c5e— «test: keep resize preflight mutant effective» (усиление мутанта commit-preflight);722f26a3— «docs: refresh screenshot source fingerprint» (толькоdocs/images/screenshots.json).
Задача заявлена как refactor-only, User-Visible: no, класс A+B+C. Проверялось
соответствие всем 12 AC спецификации, инвариант сохранения кладки (#254),
единственность источника величин и продуктовая рамка docs/SCOPE.md (J6 —
«keep the plan true… drag/resize»).
Как проверялось
Прочитано целиком: docs/SCOPE.md, AGENTS.md, PROCESS.md, тело и все
комментарии #264 (включая цепочку #277→#264 и обе спек-ревью), docs/RESIZE.md,
docs/ARCHITECTURE.md (секция Resize), docs/TESTING.md (секция Room resize),
docs/specs/264-resize-controller.md целиком (461 строка), новые модули
src/resize-controller.ts и src/wall-record-preservation.ts целиком, полный
git diff по src/houseplan-card.ts, scripts/model-invariants.mjs,
scripts/mutation-gate.mjs, всем тестовым и demo-файлам из diffstat.
Гейты (все прогнаны на HEAD в этой сессии):
| Гейт | Результат |
|---|---|
npx tsc --noEmit |
зелёный, без вывода |
npm test |
1373 passed, 1 skipped (известный env-специфичный кейс), 0 failed — совпадает с npm run inventory (1374) |
npm run build + npm run bundle:sync |
зелёный; dist/houseplan-card.js, custom_components/houseplan/frontend/houseplan-card.js, demo/srv/assets/houseplan-card.js побитово идентичны (diff -q), git status по всем трём копиям чист |
node scripts/check-docs.mjs |
«Documentation checks passed (7 files, 10 external links)» |
npm run invariants -- --config test/fixtures/real-plan-first-floor.json |
0 нарушений, 2 известных наблюдения (не регрессия) |
npm run invariants -- --config test/fixtures/real-plan-second-floor.json |
1 известное нарушение partition_over_room_wall — совпадает буква в букву с самоотчётом автора, не новое |
node scripts/smoke-select.mjs --base origin/dev --head HEAD |
18 «прямых совпадений» + 2 «зарегистрированные связи»; полный вывод см. ниже |
| 20 смоков из выборки (18 прямых + 2 зарегистрированных) | все OK, поимённо ниже |
+ smoke_resize_labels, smoke_resize_outer_reconciliation, smoke_resize_wall_thickness |
все OK — эти три смока сами входят в diffstat (переведены на новый API) и явно названы в §15 спеки, поэтому прогнаны независимо от категории смок-селектора |
node scripts/mutation-gate.mjs --id=safe-resize-commit-preflight-bypassed |
поймано 1/1 |
node scripts/mutation-gate.mjs --id=invariant-loses-wall-record |
поймано 1/1 |
node scripts/mutation-gate.mjs --id=resize-preview-reject-silent |
поймано 1/1 |
node scripts/mutation-gate.mjs --id=resize-wall-key-collision-drops-record |
поймано 1/1 |
npm run benchmark:safe-resize |
pass: true; commitPreflight p95 0.0024мс (бюджет 75мс) |
npm run benchmark:safe-resize-render |
pass: true; render p95 1.5мс (бюджет 25мс), snapshotCallsPerFrame: 1 |
CI Проверка (CI) на SHA 4a833c5e (run 33099227634, это HEAD минус только docs-фингерпринт) |
frontend(типы/юниты/мутанты/бандл), golden, все 3 шарда browser-смоков, performance_smoke — все success; docs job на этом SHA был красным именно из-за фингерпринта, что и закрыл следующий коммит 722f26a3 (его собственный CI-прогон зелёный, тяжёлые job'ы там skipped по path-filter, т.к. диф — только docs/images/screenshots.json) |
Полный вывод smoke-select.mjs:
Изменено файлов src/**: 3 · символов проекта на изменённых строках: 60
Матрица: 192 смоков · порог «широкого» символа: больше 38 смоков
Прямое совпадение (18):
smoke_bg_color, smoke_edit_walk, smoke_resize_inner_dimensions,
smoke_wall_union_isolation, smoke_card_tool_conflict, smoke_grid_snap,
smoke_hide_layers, smoke_pan_any_zoom, smoke_render_perf,
smoke_resize_audit_1550, smoke_room_resize, smoke_wall_junctions,
smoke_device_preview_parity, smoke_glow_fail_dark, smoke_junction_holes,
smoke_open_passage, smoke_sun, smoke_zero_wall_migration_unblocked
Слабая связь — одно распространённое имя (35): все через _curSpaceCfg /
_wallUnionCache / _snap / _resize (в т.ч. smoke_resize_labels,
smoke_resize_outer_reconciliation, smoke_resize_wall_thickness — попали в
«слабые», потому что _resize как символ распространён по всей матрице шире
порога, а не потому что связь с диффом слабая)
Зарегистрированная связь (2):
smoke_real_plan_masonry (← wallBodiesGeometry)
smoke_resize_pointer_real_plan (← SafeResizePlan/…/validateSafeResize)
Не учитывались как широкие: _cellCm, _cfgEpoch, _gridPitch, _serverCfg,
_space, _tool
Решение по строкам, которые не прогонялись: из 35 «слабых связей» прогнаны
только три (smoke_resize_labels, smoke_resize_outer_reconciliation,
smoke_resize_wall_thickness) — они сами изменены в этом диффе и явно названы
в §15 спеки как «behavioral parity». Остальные 32 связаны только через
_curSpaceCfg/_wallUnionCache/_snap — общими для всего редактора символами,
не специфичными для Resize-контроллера; диф не трогает ни один из них
семантически (карточка читает те же геттеры, что и раньше, только через новый
адаптер). Полный прогон 192 смоков, golden:verify (полный) и полный
performance_smoke — это пред-релизный гейт (PROCESS.md §8), не гейт этого
ревью; частично они уже зелёные в CI на 4a833c5e (см. таблицу выше).
python -m pytest tests_backend не запускался — диф не трогает
custom_components/**/*.py.
Проверка по критериям приёмки (§16 спеки)
| AC | Проверка | Результат |
|---|---|---|
| AC1 | grep по src/houseplan-card.ts на _rszSel|_rszDrag|_rszPreview|_rszLive|_rszEligibilityCache — 0 совпадений; test/resize-production-path.test.mjs источниковый guard проверяет то же и проходит |
выполнено |
| AC2 | test/resize-controller.test.mjs покрывает wrong pid → no-op, duplicate delta → no-op, cancel/reject/success — каждый даёт один детерминированный outcome; прочитан весь resize-controller.ts построчно, автомат §6 соответствует коду |
выполнено |
| AC3 | Preview строится из JSON.parse(snapshot) — свежая копия на каждый move (_rszProjectPreview); _serverCfg не мутируется до commit (grep подтверждает единственную точку записи в _rszUp); неудачная projection откатывает session.accepted = previous — тест «failed projection retains…» проходит |
выполнено |
| AC4 | _rszUp/finish() выполняет ровно один _commitPhysicalGeometry только при kind: 'commit'; остальные исходы (no-op/rejected) не создают записи — подтверждено юнитом и smoke_room_resize (disabled_zero_write, mixed_role_zero_write) |
выполнено |
| AC5 | Все прежние точки вызова _rszCancelDrag/lifecycle-сброса сохранены построчно (Escape, Ctrl+Z, pointercancel/lostpointercapture, tool/mode/space exit) — grep подтверждает те же вызовы на тех же местах, что и до рефактора, только через _resize.cancel/reset; smoke_resize_audit_1550 (pointercancel → 03_drag_cleared, без double outcome) зелёный |
выполнено |
| AC6 | scripts/model-invariants.mjs больше не содержит собственной реализации, импортирует checkWallRecordsPreserved из test-build; test/resize-production-path.test.mjs содержит source-guard doesNotMatch(invariantCli, /function checkWallRecordsPreserved/); оба фикстурных прогона npm run invariants воспроизводят самоотчёт автора |
выполнено |
| AC7 | checkWallRecordsPreserved(..., {exactMultiplicity: true}) вызывается ровно два раза в resize-controller.ts — в move() при принятии preview и в finish() перед commit; unit-тест «exact wall profile fails closed on preview and is rechecked on finish» инъецирует потерю записи на каждом из двух этапов отдельно |
выполнено |
| AC8 | 23 смока (18 прямых + 2 зарегистрированных + 3 явно затронутых) зелёные, включая smoke_resize_wall_thickness (disabled a11y, no history/write), smoke_room_resize (eligibility/clamp/opening stops/Undo/Redo), smoke_resize_pointer_real_plan (реальный курсор на втором этаже) |
выполнено |
| AC9 | benchmark:safe-resize-render → snapshotCallsPerFrame: 1, render p95 1.5мс/25мс бюджет; _rszResolution кэш подтверждён юнитом «controller owns latent selection and bounded eligibility cache» (1 вызов на context, не 2) |
выполнено |
| AC10 | Нет диффа в CSS/i18n/schema (git diff по src/i18n/*.json, стилям — пуст); golden уже зелёный в CI на 4a833c5e (тот же код, что на HEAD минус docs-фингерпринт) |
выполнено (см. «чего не проверял» — сам не гонял golden:verify локально) |
| AC11 | tsc/test/build — зелёные локально в этой сессии; bundle:sync синхронизирован и подтверждён побитовым diff -q всех трёх копий |
выполнено |
| AC12 | docs/ARCHITECTURE.md, docs/RESIZE.md, docs/TESTING.md, docs/STATUS.md описывают границу контроллера и общий инвариант; changelog не менялся (User-Visible: no) |
выполнено (с одной Low-заметкой ниже) |
Находки
Ни одной High или Medium. Одна Low-заметка, не блокирует и не требует отдельного цикла.
L1 (Low). docs/TESTING.md:2196 (секция про «paper», не про Room resize)
всё ещё называет живой preview по имени удалённого поля: «A live resize
preview (_rszPreview) moves the paper together with the dragged wall.» Поле
_rszPreview в src/houseplan-card.ts больше не существует (AC1, подтверждено
grep) — источник теперь this._resize.preview. Смок demo/smoke_bg_color.mjs,
который проверяет ровно это поведение, был правильно переписан на новый API
(тот же коммит поменял его комментарий на «A live controller preview…», см.
diff) — но соответствующая строка в TESTING.md осталась старой. Поведение не
меняется, только имя внутреннего поля в тексте проверочного листа; воспроизвести
— grep -n "_rszPreview" docs/TESTING.md. §15 спеки называет docs/TESTING.md
среди файлов, которые обязаны отражать границу контроллера, поэтому это
отмечается, а не полностью вне скоупа; ценность правки настолько мала (одно
слово в комментарии чек-листа), что блокировать зелёный вердикт из-за неё
избыточно. Оставляю на решение автора: снять с записью либо поправить в этом
же цикле бесплатно.
Отдельно зафиксировано и НЕ считается находкой: docs/WARM-REMOUNT.md:142
упоминает _rszSel в списке warm-remount состояния — этот файл не входит в
diff (git log -1 на него — коммит 9e74051, вне #264) и ссылка описывает
сериализационный ключ rszSel в WarmViewport, который спека §9 прямо
разрешает не переименовывать. Не в скоупе этой задачи.
Один источник числа/значения
Диф не добавляет и не меняет ни одной новой пользовательской величины (нет
новых чисел на экране — задача refactor-only). Единственная «величина»,
затронутая структурно, — мультимножество cm записей стен, и оно теперь
считается ровно одной функцией (checkWallRecordsPreserved), используемой и
production-путём Resize, и CLI-инвариантами (AC6/AC7), что устраняет прежнее
дублирование (было: inline-проверка в houseplan-card.ts + отдельная копия в
scripts/model-invariants.mjs). test/single-source-numbers.test.mjs не
затронут диффом (не проверялся отдельно — символы контроллера не входят в его
покрытие пользовательских величин).
Что проверено и корректно
- Автомат §6 (
idle/dragging,selectRoom/escapeIdle/begin/move/finish/cancel) реализован вsrc/resize-controller.tsбез побочного мутабельного состояния снаружи класса; root не содержит ни одного из пяти запрещённых полей (AC1). - Preview — атомарная замена
{preview, delta, labels}, откатывается целиком при ошибке адаптера (try { publish; measure } catch { rollback }вmove()), включая случай, когдаpublishуже отработал, аmeasureбросил исключение — controller восстанавливает предыдущий accepted и повторно вызываетpublishс ним же. finish()проверяет snapshot identity → topology (validateSafeResize) → wall-records (exact) →validatePreview(физический preflight) — порядок отличается от буквального перечисления в спеке §7.6 (там wall-records назван последним), но это внутренняя свобода реализации: каждая проверка — самостоятельный булев guard, и юнит-тестwall-recordsявно проверяет её срабатывание приvalidatePreview() => true, так что порядок не скрывает недостижимый код и не ослабляет ни одну проверку.wall-record-preservation.ts— единственная реализация;finiteCmосознанно исключаетNaN/Infinity/нечисловыеcmиз мультимножества (спека §8 явно объявляет это решением, не дефектом: такие записи проверяют другие гейты модели).- Все явные вызовы
_rszCancelDrag/lifecycle-сброса сохранены на тех же местах, что до рефактора (Escape мид-драг, Ctrl+Z,_rszPointerCancel, переключение tool/пространства/режима) — построчно сверено с версией наorigin/dev. - Demo/smoke-адаптации (
smoke_room_resize,smoke_resize_wall_thickness,smoke_resize_labels,smoke_resize_outer_reconciliation,smoke_resize_inner_dimensions,smoke_bg_color,smoke_edit_walk,smoke_pan_any_zoom,smoke_hide_layers,smoke_resize_audit_1550,demo/golden/harness.mjs,demo/benchmark_large_house.mjs,demo/performance/card-contract.mjs,test/performance-contract.test.mjs) переносят прежние проверки на новый API один в один, без ослабления ассертов — сверено построчно с diff. - Мутационные тесты
safe-resize-commit-preflight-bypassedиinvariant-loses-wall-record(ключевые для #253/#254, отдельно упомянутые автором как «усилен и снова ловится») подтверждены заново мной, а не приняты на слово: оба поймали инъецированный дефект. - Трейлеры
Issue: #264/User-Visible: noна всех 8 коммитах диапазона; changelog не менялся, что верно дляUser-Visible: no. - Продуктовая рамка: задача — внутренний рефакторинг J6 («keep the plan
true… drag/resize»), видимого поведения не добавляет, ничего из
docs/SCOPE.mdне расширяет и не сужает.
Чего не проверял
- Полный
npm run golden:verifyлокально не гонял (требует захвата и сравнения с эталонами, факт «0 отличий» уже подтверждён зелёным CI-jobgoldenна SHA4a833c5e, которое равно HEAD минус docs-only коммит) — беру этот CI-результат как доказательство, а не как рассказ автора, потому что сверил его сам черезgh run view. - Полный смок-сет (192 файла) не гонял локально — 3 шарда браузерных смоков
уже зелёные в том же CI-прогоне на
4a833c5e; локально я запустил выборку из 23 смоков (18 прямых + 2 зарегистрированных + 3 явно затронутых diffstat'ом), что покрывает AC8 предметно. python -m pytest tests_backend— не запускал, диф не трогаетcustom_components/**/*.py(класс A backend не затронут).- Полный набор
mutation-gate.mjs(все id) не гонял — только 4 id, прямо относящихся к Resize/wall-record; остальные мутанты проверяют неизменённые участки (Optimize, wall-thickness UI и т.д.) и не тронуты этим диффом. test/single-source-numbers.test.mjsотдельно не читал — вошёл в общий зелёный прогонnpm test, но не разбирал его покрытие предметно, так как диф не вводит новых отображаемых величин.- WSL/Windows-специфичные ручные смоки (Chromium quirks из AGENTS.md) не прогонялись — среда ревью Linux CI-подобная, что и предписано процессом для этой роли.
Вердикт
Зелёный. Все 12 AC выполнены и подтверждены самостоятельно (не на слово автора), гейты зелёные, единственная находка — Low-заметка о стаустаревшей строке в чек-листе документации, не блокирующая и не требующая отдельного цикла.