Files
houseplan-card/docs/reviews/CODE-REVIEW-477-r1.md
T
2026-09-06 18:00:59 +00:00

23 KiB
Raw Blame History

CODE-REVIEW #477 · заход r1

  • Issue: #477 «Оптимизировать планы» не должна требоваться повторно
  • Этап: code (PROCESS.md §2.7)
  • SHA: 04f4cd10 (HEAD), диапазон origin/dev..HEAD = 9 коммитов (4693d97d…04f4cd10), из них продуктовый a979699a (fix:, User-Visible: yes), остальные — тест-таргетинг и документация
  • Заход: r1 · блокирующих циклов израсходовано 0 из 4
  • Ребейз: ветка приведена к dev конвейером (+1 коммит dev поверх dfc4ce60 → 04f4cd10); разбор ниже — полный, не по дельте
  • ТЗ: docs/specs/477-editor-writer-fixed-point.md, ревью ТЗ зелёное (r1, см. docs/reviews/SPEC-REVIEW-477-r1.md и комментарий issue от 2026-09-06 16:51)

Скоуп

Диапазон git diff origin/dev...HEAD — 63 файла, из них продуктовый код: src/writer-fixed-point.ts (новый), src/room-reference-transaction.ts (новый), правки src/houseplan-editor-runtime.ts, src/draft-live-commit.ts, src/coincident-partitions.ts, src/align-grid.ts, src/houseplan-card.ts. Плюс новые/изменённые тесты (test/writer-fixed-point.test.mjs, test/align-grid.test.mjs, фикстура test/fixtures/477-writer-fixed-point-writers.json), смоки (demo/smoke_writer_fixed_point.mjs новый, demo/smoke_wall_chain_merge.mjs изменён), бенчмарк-харнесс, scripts/mutation-gate.mjs (+9 новых mutation-свидетелей), scripts/smoke-links.mjs, документация (ARCHITECTURE, CANVAS, STATUS, TESTING, USER-GUIDE ru/en, CHANGELOG ru/en, specs/README) и синхронизированный бандл (три копии).

Соответствует ТЗ §5.1 пункт за пунктом: seed-bounded merge/reconciliation на finish цепочки, единый finish barrier для всех штатных выходов, history-aware Undo/Redo без скрытого шага, room-reference transaction для Delete/Merge, исключение furniture/image из alignAllToGrid, исполняемая матрица writers, перф-свидетель terminal-click/finish, обновлённая документация.

Как проверялось

Зелёного Validate на 04f4cd10 не найдено — прогнал гейты сам.

Дешёвые гейты (обязательные):

  • npx tsc --noEmit — чисто, без вывода.
  • npm test — 2156 pass / 0 fail / 1 skip (не 46/40, как заявил автор в комментарии — это был targeted подмножество; полный прогон зелёный).
  • npm run build и npm run bundle:sync — сборка чистая, working tree после синка не изменился (git status --porcelain пуст) → три копии бандла (dist, custom_components/.../frontend, demo/srv/assets) совпадают.
  • node scripts/check-docs.mjs — «Documentation checks passed (7 files, 12 external links)» (diff трогает src/**, гейт обязателен и пройден).

Инварианты модели (diff трогает partitions, openings, marker.vacuum.segment_map — обязателен по инструкции):

  • npm test уже гоняет их на всех моделях проекта — зелёный.
  • node scripts/model-invariants.mjs --config test/fixtures/optimize-storage-roundtrip.json --json → violations: [].
  • то же для test/fixtures/477-writer-fixed-point-writers.json → violations: [].

Мутационный гейт:

  • node scripts/mutation-gate.mjs --check → 530/530 ok, exit 0, включая 9 новых writer-* свидетелей (writer-finish-reset-bypasses-finalizer, writer-finish-adopts-unsafe-candidate, writer-history-skips-finished-chain-normalization, writer-room-delete-keeps-direct-reference, writer-room-delete-keeps-vacuum-reference, writer-align-snaps-free-decor-transform, writer-terminal-click-runs-finish-reconciliation, writer-finish-skips-seed-merge, writer-finish-skips-coincident-reconciliation) — дисциплина «тест умеет падать» проверена самим прогоном гейта: guard реально краснеет при мутации и гейт репортит ok, то есть проверяемый код ловит регресс.

Смоки — выбраны по выводу node scripts/smoke-select.mjs --base origin/dev --head HEAD (228 смоков всего; изменено 7 файлов src/**, 81 символ на изменённых строках). Прогнал прямые совпадения и одну зарегистрированную связь, относящиеся к затронутым путям (wall-chain finish, room reference, terminal click, room acceptance/reconcile):

Смок Связь Результат
smoke_writer_fixed_point.mjs прямое (новый, специально под #477) OK, все 12 полей true
smoke_wall_chain_merge.mjs прямое (_finishWallChain, изменён в diff) OK, все 9 полей true
smoke_wall_draw_click.mjs прямое (_activeWallChainId, _path…) OK, все 8 полей true (rejected-state atomicity)
smoke_unified_wall_tool.mjs прямое OK, все 16 полей true, включая acceptedRoomIsOptimizeFixedPoint
smoke_room_resize.mjs прямое (_checkSpacePhysicalGeometry, изменена сигнатура) OK
smoke_optimize_coincident_partition.mjs зарегистрированная (reconcileCoincidentPartitions, #276) OK, все 10 полей true
smoke_v8_draft_write.mjs прямое (_activeWallChainPartitionIds, _pendingPhysicalWrites) OK, все 8 полей true

Не прогонял остальные ~46 прямых/слабых совпадений (smoke_room_autoclose, smoke_editor_tabs, smoke_free_walls, zigbee/topology/decor смоки и т.д.): совпадения там — по общим геттерам состояния (_curSpaceCfg, _path, _saveConfig), которые не входят в изменённый контракт: правки не трогают zigbee, decor-персист, resize-контроллер или furniture-палитру напрямую; выбор ограничен темой diff, а не полным списком «прямых» имён. smoke_edit_walk (зарегистрированная связь, «дефекты рождаются в редактировании») не прогонялся отдельно — покрытие того же класса рисков дают smoke_wall_chain_merge+smoke_wall_draw_click+smoke_writer_fixed_point.

Перформанс (AC9, §11 ТЗ — обязателен, путь #461 задет): npm run benchmark:wall-draw-click — pass: true, structural: true, timingPass: true. Ключевые числа (сам прогнал, не со слов автора): click median 7 ms / max 10.7 ms (бюджет 150/250), remote median 11.4 ms (бюджет 30.5), finish 4.7 ms (бюджет 250), remote-finish (удвоенные unrelated rooms/partitions, remoteFixture.positiveSegments = 2×base) 9.5 ms (бюджет 4.7×1.5+20=27.1) — это ровно требование §11 «удвоение unrelated rooms/partitions не должно более чем в 1.5 раза увеличивать median finish плюс 20 ms». Структурные счётчики: terminal click — fullSpacePhysicalChecks:0, configWrites = 1 на клик, history = 1 на клик; finish — один localPhysicalChecks, один configWrites, history: 0, wallFinishBarriers: 1 — подтверждает «один bounded transaction, без дополнительной history-команды».

Golden: НЕ прогонял npm run golden:verify (222 сценария, полный предрелизный гейт). Обоснование по чтению кода и матрицы, а не по заявлению автора: (1) merge коллинеарных partitions и reconcile — lossless-операции над идентичной абсолютной геометрией (те же a/b, тот же cm), рендер стен не зависит от числа записей partitions с одинаковым итоговым контуром; (2) два единственных golden-сценария, упоминающих Optimize (optimize-preflight-dialog-*, optimize-orphan-references-dark-en), подставляют синтетический card._alignDialog = {...} вручную (demo/golden/harness.mjs:1582-1610), а не вызывают реальный alignAllToGrid/optimizePlans на fixture — furniture-exemption в align-grid.ts не может повлиять на их пиксели; (3) ни один сценарий матрицы не проходит через _finishWallChain/Delete-Merge room. Риск ложного пропуска низкий, но это явное решение, а не тихий пропуск.

Explicit-skip (по diff и AC не требуются):

  • python -m pytest tests_backend — custom_components/**/*.py не тронут.
  • Полный набор demo/smoke_*.mjs (228 шт.) — задача не задевает всё (zigbee/vacuum/isometric/decor-палитра и т.д. вне скоупа).

Находки

Находок уровня High или Medium в скоупе задачи не обнаружено.

Мелкое наблюдение (не находка, не блокирует): в комментарии реализации автор написал «расширенный регрессионный набор — 40/40» вместо честного полного npm test; сам прогнал полный набор — 2156/0/1, разночтений с продуктовым кодом не выявлено. Указываю для полноты картины, замечание не по существу кода.

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

  • AC1 (finished chain fixed point). test/writer-fixed-point.test.mjs прогоняет 2- и 3-сегментные коллинеарные цепочки через finalizeWallChainSpace → optimizePlans(...).changed === false; смок smoke_wall_chain_merge подтверждает то же через реальный _markupClick/ _finishWallChain в собранном бандле (straightRunMergedAtFinish: true).
  • AC2 (coincident chain fixed point). unit-тест на фикстуре 276-coincident-partition.json: partitionsReconciled: 1, openingsRehosted: 1, абсолютная геометрия проёма (x, length, contact, future_field) сохранена, следующий Optimize — no-op; смок smoke_optimize_coincident_partition подтверждает атомарность Apply/reload/Undo. Прочитано и проверено: seed-scope действительно ограничивает reconciliation только выжившими ID цепочки (survivingSeedIds в writer-fixed-point.ts:47-58), несвязанные швы не трогаются (тест «seed scope merges the touched chain but leaves unrelated historical seams alone»).
  • AC3 (fail-closed atomicity). Юнит-тест «unsafe coincidence is fail-closed and byte-equivalent» — 3 варианта (unknown field, column conflict, orphan opening host) не дают частичной записи, report.config.spaces[0] побайтово равен before. На уровне транзакции — commitWallChainFinishGeometry (draft-live-commit.ts) строит candidate на клоне, проверяет wallModelOffGridValueCount + _checkSpacePhysicalGeometry
    • _junctionLimitsIntroduced до adoptWallSegmentModelCandidateInPlace; при отказе _finalizeWallChainPartitions (houseplan-editor-runtime.ts:1481+) восстанавливает _path/_activeWallChainId/_wallChainRedo/_closingWallCm из локального session-снапшота. Мутация writer-finish-adopts-unsafe-candidate красит guard, если проверку убрать — свидетель умеет падать (гонял gate, ok). Смок smoke_wall_draw_click: rejectedStateRestored, rejectedSessionRestored, rejectedAddsNoHistory, rejectedQueuesNoWrite — все true.
  • AC4 (history continuity). _canonicalWallChainHistoryState (houseplan-editor-runtime.ts:1976-2016) прогоняет lossless finalizer на snapshot только если цепочка уже не активна (this.host._activeWallChainId проверен первым), иначе возвращает snapshot буквально — соответствует §7 ТЗ («пока цепочка активна, _applyGeometryState применяет snapshot буквально»). Смок smoke_writer_fixed_point: finishedChainUndoOptimizeNoop, finishedChainUndoHasOnePartition, finishedChainRedoOptimizeNoop, finishedChainRedoHasOnePartition — все true, т.е. Undo/Redo после finish не возвращает optimizer debt и не плодит лишний partition. finishAddsNoHistory: true — история не получает скрытый шаг. Мутация writer-history-skips-finished-chain-normalization подтверждена красной.
  • AC5 (all finish owners). Source-contract тест «routes every explicit chain finish through one finalizer» проверяет: _finishWallChain вызывает _finalizeWallChainPartitions; Reset-кнопка (btn.reset) в контекст-трее теперь идёт через _finishWallChain(), а не напрямую через _cancelPath() (проверил диф — _cancelPath не удаляет уже принятые стены, только сессионное состояние, так что смена поведения кнопки корректна и соответствует §6.4); _applyWallFaceBatch при «не создавать ни одну предложенную комнату» и _keepClosedAsPartitions тоже проведены через _finalizeWallChainPartitions; _slideTo, _onHashChange, Escape (в houseplan-card.ts) вызывают _finishWallChain; тест явно проверяет ОТСУТСТВИЕ вызова финализатора из _markupClick (terminal-click путь) — прочитал _stagePointerCancel: pointer cancel/pinch не вызывают _finishWallChain/_finalizeWallChainPartitions, соответствует «pointer cancel, pinch и второй touch сами по себе не заканчивают цепочку».
  • AC6 (room reference transaction). room-reference-transaction.ts прочитан целиком: rewriteMarkerRoomReferences меняет только точное совпадение room_id/значений vacuum.segment_map, сканирует markers всех пространств (глобальная уникальность room ID, как того требует §8.1), пустой segment_map/vacuum удаляется канонически только если больше ничего не осталось. restoreMarkerRoomReferences восстанавливает только захваченные поля, не трогая более поздние правки прочих полей — юнит-тест явно это проверяет («unrelated later marker fields survive Undo»). В runtime _confirmRoomDelete/_commitMerge вызывают rewrite до геометрической мутации и сохраняют before.roomReferences для отмены; _recordGeometry делает симметричный after-снэпшот. Смок smoke_writer_fixed_point: deleteClearsExactReferences, undoRestoresGeometryAndReferences, redoReappliesGeometryAndReferences, unrelatedMarkerFieldsSurvive — все true. Две мутации (writer-room-delete-keeps-direct-reference, writer-room-delete-keeps-vacuum-reference) подтверждены.
  • AC7 (continuous transforms stay canonical). align-grid.ts: kind === 'furniture' || kind === 'image' пропускается до какого-либо изменения (строка добавлена перед инкрементом total/moved, то есть транформ вообще не участвует в отчёте — соответствует ТЗ «не увеличивает moved, coordsCanonicalized... из-за их transform»). Юнит-тест: byte-equivalent furniture/image (включая future_field, флаги отражения), report.total === 1 (только ordinary decor учтён), обычный rect по-прежнему двигается. Мутация writer-align-snaps-free-decor-transform подтверждена.
  • AC8 (writer coverage manifest). test/fixtures/477-writer-fixed-point-writers.json (27 строк, уникальные owners) + source-contract тест, который AST-парсит houseplan-editor-runtime.ts (через typescript), собирает все методы, вызывающие _commitPhysicalGeometry/_recordGeometry, и падает, если найденный owner отсутствует в манифесте — реальный «краснеет при добавлении нового writer» механизм, не декларативный список.
  • AC9 (perf). См. «Как проверялось» выше — прогнал сам, оба структурных и оба тайминговых критерия зелёные, включая seeded-scaling budget.
  • AC10 (compatibility/docs). model_version/схема не менялись (grep по diff — новых persisted полей нет; wallChainSeedIds/roomReferences объявлены в SpaceGeometryState с пометкой «Session-only» и не попадают в ServerConfig, что и требуется §12). i18n: toast.geometry_unsafe — уже существующий ключ (использовался в 9 др. местах runtime до этого диффа), новых строк не добавлено. USER-GUIDE.md/.ru.md, CANVAS.md, ARCHITECTURE.md, STATUS.md, TESTING.md обновлены синхронно и по существу совпадают с кодом (сверил формулировки построчно с реализацией, а не только с фактом правки файла). docs/specs/README.md получил строку на #477.
  • Трейлеры. Все 9 коммитов несут Issue: #477; User-Visible: yes только на продуктовом a979699a, который включает оба changelog (docs/CHANGELOG.md, docs/CHANGELOG.ru.md) в одном коммите — соответствует требованию.
  • Одно число — один источник. Диф не добавляет и не меняет ни одной новой пользовательски видимой величины (Optimize-отчёт — существующий механизм, его числа как и раньше берутся из одного источника, alignAllToGrid/ optimizePlans; никакого нового дублирующего представления числа в UI не введено). test/single-source-numbers.test.mjs прошёл в составе npm test.

Чего не проверял и почему

  • npm run golden:verify — не прогонял (обоснование выше: код-чтение показывает, что изменённые пути lossless и не пересекаются с двумя Optimize-related golden-сценариями, которые используют синтетический _alignDialog, а не реальный расчёт). Если ревьюер/владелец сочтёт риск неприемлемым, это единственный неисполненный гейт из «по необходимости».
  • python -m pytest tests_backend — не запускал, custom_components/**/*.py не тронут диффом.
  • Полная матрица demo/smoke_*.mjs (228 файлов) — не запускал; задача не задевает весь редактор (zigbee-топология, isometric, decor-палитра и т.п. вне скоупа и вне выборки инструмента).
  • Остальные ~46 «прямых»/«слабых» совпадений из вывода smoke-select.mjs (smoke_room_autoclose, smoke_editor_tabs, smoke_free_walls, smoke_wall_junctions, decor/save-race/zigbee смоки и т.д.) — не запускал: совпадения там по широко переиспользуемым геттерам состояния (_curSpaceCfg, _path, _saveConfig), не по изменённому контракту; прогнанные 7 смоков покрывают все реально изменённые точки входа (wall-chain finish, terminal click, room acceptance/reconcile, room reference transaction).
  • Ручного тестирования в браузере вне смоков не проводил (в этом цикле его и не предполагается — «Ручного тестирования в цикле нет»).

Вердикт

Зелёный. Все AC доказаны исполняемыми свидетелями (unit + smoke + mutation + source-contract + benchmark), гейты прогнаны лично и по существу, документация и трейлеры в порядке, High/Medium-находок в скоупе нет.


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

  • Ветка: issue/477-optimizer-fixed-point, коммит dfc4ce60033f — ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет.
  • Дерево материала: 5ea87e1947f3f9ed13fefda60629a7aecc427bf8
    git log --all --format='%H %T' | grep 5ea87e1947f3
    
  • ТЗ docs/specs/477-editor-writer-fixed-point.md, блоб a07a7248e008229e6e865ef793b213fdb524d5e4
    git log --all --find-object=a07a7248e008229e6e865ef793b213fdb524d5e4 -- docs/specs/477-editor-writer-fixed-point.md