Files
houseplan-card/docs/reviews/CODE-REVIEW-478-r2.md
T
2026-09-06 16:14:56 +00:00

26 KiB
Raw Blame History

CODE-REVIEW-478-r2

  • Issue: #478 — отказ от сущности «черновик контура» (room_drafts), wall model v9 → v10
  • Ветка: issue/478-remove-room-drafts
  • SHA материала: 0c670a9862ab4266a677b5553c070f2e41bab0c0 (сверено git rev-parse HEAD)
  • SHA предыдущего раунда (r1): 1cadd520c0658e14ec8fd1410241e55ddbac21e1 — назван явно в r1-документе и в r1-комментарии; не пришлось восстанавливать по истории.
  • Диапазон дельты: git diff 1cadd520..HEAD — 50 файлов, +1036/-593 (без учёта дублирования bundle-копий: правки продукта в src/wall-segment-model.ts, custom_components/houseplan/validation.py, custom_components/houseplan/wall_segment_model.py, src/houseplan-editor-runtime.ts; правки гейтов в demo/golden/harness.mjs, demo/smoke_preflight_diagnostics.mjs, demo/smoke_unified_wall_tool.mjs, demo/smoke_wall_draw_click.mjs, scripts/mutation-gate.mjs, test/wall-segment-model.test.mjs, tests_backend/test_wall_segment_model.py, новая фикстура test/fixtures/478-room-draft-migration-vectors.json; docs (docs/TESTING.md, docs/images/*); bundle-копии.
  • Заход: r2 · блокирующих циклов на входе в раунд — 0 из 4 (значение из заголовка задачи); фактическая видимая история — r1 уже был красным («блокирующих циклов 1/4» в его собственном вердикте), так что счётчик в заголовке этого раунда, по всей видимости, не кумулятивен между раундами. Довожу до сведения, не блокирую этим находку.

Вердикт

Жёлтый. High: 0, Medium: 1 (в скоупе задачи — тот же файл документации, который AC15 требует держать в соответствии с v10), Low: 0.

Обе High-находки r1 закрыты и подтверждены исполнением, все четыре Medium и оба Low r1 закрыты и подтверждены. Дельта сама внесла один новый Medium: канонический документ подсистемы разошёлся с кодом ровно в той фразе, которую эта дельта должна была держать верной.

Скоуп

Разбор по дельте (не заново): dev не двигался (git merge-base origin/dev HEAD == git rev-parse origin/dev == c82ba84c), контракт поведения не меняется, новая подсистема не затронута, размер дельты (50 файлов, +1036/-593) кардинально меньше исходной задачи (154 файла, +3515/-4309 на r1). Полный повторный разбор не требуется — граница определена решением по п. «разбор остаётся полным, если…», ни одно условие не выполняется.

Предмет раунда — код и тесты, добавленные commit'ом 38abe78c (fix: address room draft review findings) поверх материала r1, плюс два docs-коммита с принятием скриншота (0b577be5, 0c670a98), которые к продуктовому коду отношения не имеют.

Закрытие раунда r1

Находка r1 Чем закрыта Где это видно
High-1 — TS/Python дают разные ID и разные accept/reject на одном JSON (cm: null, явный id: null, допуск нулевой длины) Общие функции legacyFiniteNumber/legacyDraftPoint/legacyIdentity (TS) и _legacy_finite_number/_legacy_draft_point/_legacy_identity (Python) с одинаковой numeric-regex и одинаковым epsilon 0.001; единая JSON-фикстура test/fixtures/478-room-draft-migration-vectors.json потребляется обоими движками через новые тесты src/wall-segment-model.ts:723-741, custom_components/houseplan/wall_segment_model.py:33-56; тесты test/wall-segment-model.test.mjs:91-113, tests_backend/test_wall_segment_model.py:422-447. Проверено исполнением: 28/28 в node --test test/wall-segment-model.test.mjs на HEAD; откат файла wall-segment-model.ts к версии r1 (git show 1cadd520:...) на этой же фикстуре ловит регресс (WallSegmentModelError: duplicate-id: wall-d2ypm2qxakxh3ptdvgvl — см. «Как проверялось»)
High-2 — устаревшая v9-запись с room_drafts поверх сохранённого v10 не отклоняется Новая явная проверка в validate_wall_model_transition: old_model >= 10 and new_model < 10 and any("room_drafts" in space ...) → WallModelClientOutdatedError, выполняется до contour_geometry_changed и до CONFIG_SCHEMA custom_components/houseplan/validation.py:183-193; вызов до схемы подтверждён в custom_components/houseplan/websocket_api.py:1568 (validate_wall_model_transition — первая строка _validate_config_cpu, до CONFIG_SCHEMA(msg["config"])). Новый тест tests_backend/test_wall_segment_model.py:327-343 строит именно сценарий r1 (сохранённая v10-модель, эхо v9 с восстановленным room_drafts, геометрия не менялась) — прочитан, не исполнен (нет homeassistant в среде), но трассировка не зависит от HA-рантайма
Medium-1 — golden-харнесс не переименован вслед за полем, сценарий превью толщины реально сломан demo/golden/harness.mjs:1287,1289,1308 переименован _activeDraftId→_activeWallChainId, _draftSegmentCms→_wallChainSegmentCms Диф demo/golden/harness.mjs. Проверено исполнением: node demo/golden/run.mjs --mode=verify (полная матрица, 161 сценарий) — wall-junctions-plan-preview-light теперь passed (было different, diffRatio≈0.0024 на r1)
Medium-2 — путь поглощения комнаты не атомарен при _spaceModel() === null Введён abortMutation() (_restoreGeometryStateLocal(before) + toast) во всех точках выхода после мутации sp.rooms/sp.walls в _applyWallFaceBatch и _commitRoom; добавлена ранняя проверка if (!before) { abort(...); return; } src/houseplan-editor-runtime.ts:2517-2545 (churn ID), :6764-6893 (_applyWallFaceBatch), :6931-7040 (_commitRoom). Проверено исполнением: node demo/smoke_unified_wall_tool.mjs → nullModelRollsBackWholeRoomMutation: true, classicNullModelRollsBackWholeRoomMutation: true (оба теста форсируют _spaceModel() в null после мутации и проверяют побайтовый откат)
Medium-3 — AC13 (фикс-пойнт Optimize сразу после accept) не доказан через реальный путь редактора Новый сценарий смока: partial-overlap с «чужой» несвязанной partition + вызов _openAlignDialog() (реальный Optimize-путь) сразу после accept; новый мутационный свидетель room-accept-leaves-coincident-partitions, целящийся именно в allowCoincidentPartitions: true demo/smoke_unified_wall_tool.mjs (+50 строк), scripts/mutation-gate.mjs:2519-2530. Проверено исполнением: смок → acceptSplitsOnlyCoincidentForeignInterval: true, acceptedRoomIsOptimizeFixedPoint: true; node scripts/mutation-gate.mjs --id=room-accept-leaves-coincident-partitions → «чистый прогон» + «мутант покраснел, как обязан»
Low-1 — мёртвые поля removedDrafts/redundantDraftsRemoved в моке смока Убраны из литерала отчёта demo/smoke_preflight_diagnostics.mjs:53-56
Low-2 — churn ID активной цепочки при отклонённом клике _activeWallChainId восстанавливается к beforeChainId вместо генерации нового при откате src/houseplan-editor-runtime.ts:2520,2538; смок demo/smoke_wall_draw_click.mjs:26,30 (sessionRestored: ... card._activeWallChainId === chainIdBeforeReject) — проверено исполнением, rejectedSessionRestored: true

Все восемь находок r1 закрыты фактическим изменением кода/теста, а не заявлением; шесть из восьми подтверждены исполнением в этом раунде (не только чтением).

Унаследовано из r1

Без повторной проверки принято (дельта их не касается):

  • продуктовая рамка и соответствие docs/SCOPE.md (J6) — из r1-документа, SHA 1cadd520;
  • полнота ТЗ и решения владельца — docs/reviews/SPEC-REVIEW-478-r1.md (зелёное, 0 циклов), из вердикта в issue от 2026-09-06T12:05:47Z, SHA ТЗ 30a16ed3;
  • корректность миграции v9→v10 в частях, не тронутых дельтой: перенос points/segments в partitions, идемпотентность повторного commit, структура fixedTopologyWallLineageHints, структура reconcileCoincidentPartitions (сигнатура, а не вызовы вокруг неё) — из r1-документа, раздел «Что проверено и корректно», SHA 1cadd520;
  • AC4 (performance/mutation, bounded fast path #461), AC5 (session-only chain), AC6 (room face detection), AC7/AC8 (кроме нового partial-overlap-с-чужой-partition кейса, который дельта как раз добавила и который я перепроверил — см. Medium-3 выше), AC10, AC11, AC12, AC14 (golden — но верифицировано заново полным прогоном, см. ниже), AC15 (typecheck/build/bundle — доверено зелёному Validate) — из r1-документа, SHA 1cadd520.

Как проверялось (этот раунд)

Гейт Команда Результат
Дешёвые гейты (typecheck/test/build) подтверждены зелёным Validate на точном SHA 0c670a98: https://github.com/Matysh/houseplan-card/actions/runs/34043584139 (упомянут автором и проверен по номеру run) не перегонял tsc --noEmit/полный npm test/npm run build отдельно — доверяю этому прогону
Целевой unit-тест высокого приоритета npx tsc -p tsconfig.test.json && node scripts/fix-test-build.mjs && node --test test/wall-segment-model.test.mjs 28/28 green
Тест умеет падать (не декларация) тот же тест на файле src/wall-segment-model.ts, возвращённом к состоянию r1 (git show 1cadd520:src/wall-segment-model.ts), с восстановлением после проверки (git status чист) not ok — WallSegmentModelError: duplicate-id: wall-d2ypm2qxakxh3ptdvgvl — тест ловит регресс
node scripts/check-docs.mjs напрямую (diff трогает src/**) «Documentation checks passed (7 files, 12 external links)»
npm run bundle:sync + сверка 3 копий напрямую (потребовалось для запуска браузерных смоков — demo/srv/assets не закоммичен) dist / custom_components/houseplan/frontend / demo/srv/assets побайтно идентичны; git status после — чисто (совпадает с уже закоммиченным)
Инварианты модели (диф трогает геометрию миграции) node scripts/model-invariants.mjs --config <результат commitWallSegmentModel на accepted-фикстуре> и то же с --lattice «ссылки разрешимы, записи толщины находятся»; 16/16 координат точно на узле решётки, 0 расхождений ключа
node scripts/smoke-select.mjs --base 1cadd520 --head HEAD напрямую 11 «прямых совпадений», 15 «слабых связей» (общий символ _activeWallChainId) — вывод приложен ниже
11 смоков прямого совпадения node demo/smoke_{decor_images,edit_walk,help_affordance,junction_limits,near_axis_optimize,optimize_coordinate_canonicalization,partition_openings,room_resize,tap_ctx,zero_divider_taper,zero_wall_migration_unblocked}.mjs все 11 OK
demo/smoke_unified_wall_tool.mjs (изменён этой дельтой) напрямую все 16 полей true, OK, включая 4 новых поля Medium-2/Medium-3
demo/smoke_wall_draw_click.mjs (изменён этой дельтой) напрямую все 8 полей true, OK
demo/smoke_v8_draft_write.mjs (слабая связь, соседняя тема — v8/v9 запись) напрямую, точечно все 8 полей true, OK
node scripts/mutation-gate.mjs --id=room-accept-leaves-coincident-partitions напрямую чистый прогон green, мутант покраснел — «поймано 1 из 1»
golden:verify (диф правит рендер-путь превью толщины) node demo/golden/run.mjs --mode=verify — полная матрица, фильтр по сценарию запрещён самим скриптом (policy.mjs) 161 сценарий: 146 passed, 15 different. Ранее сломанный wall-junctions-plan-preview-light теперь passed. Оставшиеся 15 different — те же категории (isometric-*, *-color-popover-*), что r1 уже определил как несвязанные с этим диффом и сверил с origin/dev; дельта их не касается (ни один файл изометрии/попапов цвета в diff 1cadd520..HEAD не участвует)

Что НЕ проверял и почему

  • python -m pytest tests_backend — среда без homeassistant (ModuleNotFoundError), как и в r1. Компенсировано: (а) High-2 проверен трассировкой кода и порядком вызовов в websocket_api.py, не зависящим от HA-рантайма; (б) High-1 перепроверен исполнением эквивалентного TS-движка на той же самой JSON-фикстуре, которую потребляет и Python-тест — фикстура едина, разошедшегося поведения в TS уже не осталось, а сам факт единого источника снижает шанс, что Python читает её иначе, не отменяя необходимость дождаться зелёного backend-джоба CI (заявлен автором и Validate-job включает его).
  • 14 из 15 «слабых связей» смок-выборки (smoke_active_chain_ink, smoke_editor_tabs, smoke_free_walls, smoke_island_rooms, smoke_optional_space_model, smoke_plan_drawing_repairs, smoke_plan_snap_overlay, smoke_room_autoclose, smoke_wall_chain_merge, smoke_wall_chain_thickness, smoke_wall_face_overlap, smoke_wall_junctions) — общий символ _activeWallChainId очень распространён, а дельта трогает его ровно в двух строках (_markupClick, сохранение beforeChainId вместо генерации нового id), и это поведение уже покрыто и подтверждено напрямую в smoke_wall_draw_click.mjs. golden:verify (полная матрица, включает wall-junctions-* сценарии) прошёл зелёным, что косвенно снимает риск визуальной регрессии для смежных сценариев.
  • Полный npm test/npx tsc --noEmit/npm run build по отдельности — см. таблицу выше, доверено Validate на этом SHA.
  • Performance-профиль (#461) — не в скоупе дельты r1→r2 (только новые тесты/фиксы, профиль не тронут); AC4 унаследован из r1 без изменений.

Находки

Medium-1 — канонический документ подсистемы утверждает поведение, которого дельта только что лишила код (docs/CONFIG-COMPATIBILITY.md)

docs/CONFIG-COMPATIBILITY.md:203 (раздел «Ordinary wall chains — model v10 (#478)», не тронутый этой дельтой) буквально говорит: «Malformed geometry, invalid thickness or an id collision fails closed» — то есть коллизия ID при миграции черновика обязана останавливать запись целиком.

Это было верно на SHA 1cadd520 (код r1): if (id) { if (used.has(id)) throw WallSegmentModelError('duplicate-id', id); }. Именно этот текст в документе и описывал то поведение.

Фикс High-1 в этой дельте (38abe78c) заменил ветку на if (!id || used.has(id)) { /* сгенерировать новый id детерминированно, добавить суффикс -N при повторной коллизии */ } — коллизия ID теперь не бросает исключение никогда, а тихо заменяется на свежесгенерированный id. Это сделано намеренно и раскрыто автором в хендоффе («…отсутствующие/повторные ID») и подтверждено собственной фикстурой задачи: test/fixtures/478-room-draft-migration-vectors.json, accepted.input содержит сегмент черновика с явным "id": "wall-d2ypm2qxakxh3ptdvgvl", дословно совпадающим с ID уже существующей partition, и ожидает не отказ, а успешную миграцию с новым id wall-d2ypm2qxakxh3ptdvgvl-2.

Проверено исполнением (не только чтением): та же фикстура на файле wall-segment-model.ts, откаченном к r1 (1cadd520), даёт WallSegmentModelError: duplicate-id: wall-d2ypm2qxakxh3ptdvgvl — то есть код на SHA 1cadd520 действительно бросал исключение (док был верен), а код на HEAD (0c670a98) мигрирует эту же фикстуру успешно (28/28 green) — то есть текст дока с этим самым SHA уже разошёлся с фактическим поведением.

Чем краснеет: прямое чтение docs/CONFIG-COMPATIBILITY.md:203 против src/wall-segment-model.ts:774-782 и custom_components/houseplan/wall_segment_model.py:695-707 на HEAD, плюс эмпирическое подтверждение через откат/повтор теста выше.

Почему Medium, а не High: AC15 требует «канонические документы… соответствуют v10» как условие приёмки самой задачи — расхождение прямое и в файле, который эта же задача обязана была актуализировать (задача трогала docs/CONFIG-COMPATIBILITY.md в исходных коммитах 5ab41a72/04d6c5cd, просто не при этой правке). Не High, потому что: (а) поведение кода не регрессирует ни один AC — наоборот, оно устраняет реальный TS/Python-разнобой High-1, и на практике коллизия ID при миграции черновика уже отсекается на входе отдельным, куда более старым инвариантом схемы (_space_geometry_invariants в validation.py:1592-1610, общий seen-набор ID по всем категориям пространства включая room_drafts[].segments[].id) для любого документа, прошедшего запись после появления этого инварианта — то есть реальная достижимость этой ветки на боевых данных низкая (только для документов, унаследованных до появления инварианта); (б) единственный пострадавший артефакт — текст документации, откат которой не требует правки продукта.

Как чинится: одна строка в docs/CONFIG-COMPATIBILITY.md:203 — убрать «or an id collision» из списка отказов и в одном предложении описать реальное поведение (детерминированная регенерация + суффикс при повторной коллизии), не трогая соседние verified-фразы про malformed geometry/thickness (они по-прежнему бросают исключение — подтверждено той же фикстурой: null-cm, boolean-cm, null-point, epsilon-point все в rejected).

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

  • High-1 и High-2 из r1 закрыты фактически, не декларативно — оба перепроверены исполнением (см. таблицу выше и раздел «Как проверялось»).
  • Medium-1..3 и Low-1..2 из r1 закрыты, каждое подтверждено конкретным зелёным прогоном нового или изменённого теста/смока/мутанта, а не только чтением диффа.
  • golden:verify на полной матрице подтверждает: ранее сломанный сценарий исправлен, новых регрессий не внесено.
  • Инварианты модели (ссылки, записи толщины, ключ решётки) — 0 нарушений на мигрированной фикстуре задачи.
  • bundle:sync — три копии побайтно идентичны, дерево чисто после локальной пересборки.
  • Один источник числа (test/single-source-numbers.test.mjs) — этой дельтой не тронут ни один видимый пользователю числовой путь (правки — миграция/атомарность/тестовая инфраструктура), предмета для отдельной проверки в этом раунде не возникло.

Итог

Ни одной High-находки. Ровно одна Medium — внутри скоупа задачи (тот же canonical-документ, что AC15 требует держать актуальным), не блокирует, чинится без нового цикла по решению владельца 2026-08-19 (#202). Возврат автору для одной правки документации.

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

  • git diff 1cadd520..HEAD (дельта r1→r2, основной предмет разбора)
  • git diff origin/dev...HEAD (полный контекст, не как предмет полного разбора — только для сверки, что dev не двигался и делать полный разбор не требуется)
  • Issue #478 и все комментарии, включая полный текст docs/reviews/CODE-REVIEW-478-r1.md (закоммичен в дерево на b754d362)
  • docs/specs/478-remove-room-drafts.md (не менялся в этой дельте)
  • docs/CONFIG-COMPATIBILITY.md, docs/SCOPE.md, AGENTS.md, PROCESS.md

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

  • Ветка: issue/478-remove-room-drafts, коммит 0c670a9862ab — ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет.
  • Дерево материала: e46feab93fe544c6506b07ea404d10ccfe191c7c
    git log --all --format='%H %T' | grep e46feab93fe5
    
  • ТЗ docs/specs/478-remove-room-drafts.md, блоб 70719ed585a993825720f90a2483fa760747bb06
    git log --all --find-object=70719ed585a993825720f90a2483fa760747bb06 -- docs/specs/478-remove-room-drafts.md