Files
houseplan-card/docs/reviews/SPEC-REVIEW-477-r1.md
2026-09-06 17:52:05 +00:00

22 KiB
Raw Permalink Blame History

SPEC-REVIEW-477-r1

  • Issue: #477 — «Оптимизировать планы» не должна требоваться повторно
  • Этап: ТЗ на ревью (PROCESS.md §2.4), трек полный (не small)
  • Материал: docs/specs/477-editor-writer-fixed-point.md на SHA aa962ed2c5a03bd2f50b4ae066968761cd053585 (дерево 56e39416...), ветка issue/477-optimizer-fixed-point
  • Заход: r1 · блокирующих циклов израсходовано 0/4
  • Вердикт: зелёный

Вердикт

Вердикт: зелёный · заход r1 · блокирующих циклов 0/4 · High: 0 · Medium: 0 → в задаче | · Документ: docs/reviews/SPEC-REVIEW-477-r1.md

Скоуп разбора

Первый заход, ТЗ полного трека. Разбирал целиком: docs/SCOPE.md, AGENTS.md, PROCESS.md целиком, тело issue #477 и все три комментария владельца (повторная аналитика после #478, поправка про off-grid мебель, ссылка на готовое ТЗ), сам файл docs/specs/477-editor-writer-fixed-point.md (492 строки) и его запись в docs/specs/README.md. Отдельно поднял канонические документы затронутых подсистем: docs/CANVAS.md (§9.4, §9.5), docs/WALL-THICKNESS.md (контракт #276/#281/#296), docs/USER-GUIDE.ru.md (терминология «цепочка», «Оптимизировать планы», Esc), docs/TOUCH-SUPPORT.md (контракт touch-редакторов), docs/ARCHITECTURE.md/docs/CONFIG-COMPATIBILITY.md (контракты #282, #314).

Владелец в третьем комментарии прямо попросил независимое ревью отдельно проверить три границы: seeded reconciliation, Undo/Redo после завершения wall-chain, исключение continuous transform мебели/изображений из Optimize — все три разобраны отдельно ниже (§6.1–6.2, §7, §9 документа ТЗ).

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

Это ревью ТЗ, не код-ревью: гейты typecheck/test/build/check-docs здесь не применяются — на branch нет продуктового кода (см. ниже), только документация. Основной метод — состязательная фактчек-проверка: каждое техническое утверждение ТЗ, которое можно спутать с догадкой, выданной за факт, я сверил прямым чтением текущего dev-кода (не поверил заявлению автора). Не поверил на слово ни одному месту, где заявлен текущий дефект («функция X сейчас не делает Y»), и перепроверил каждое такое место чтением тела функции. Список того, что сверено буквально — раздел «Что проверено» ниже.

Отдельно проверил состав ветки: git diff <merge-base d12b1f20>..HEAD — ветка добавляет только docs/specs/477-editor-writer-fixed-point.md и одну строку в docs/specs/README.md. Полный git diff origin/dev..HEAD показывает больше файлов (.github/workflows/validate.yml, scripts/mutation-gate.mjs, удаление docs/reviews/CODE-REVIEW-475-r*.md и др.) — это независимый прогресс dev после точки расхождения (origin/dev и эта ветка разошлись от общего предка d12b1f20, ни один не является предком другого), а не изменения этой задачи; branch не рибейзился на текущий dev, но это не создаёт конфликта содержимого для ревью ТЗ (класс C, документация, DoR ещё не пройден, кода нет). Отмечаю это для протокола, чтобы автор рибейзнул ветку перед переходом к S6-in-progress, если к тому моменту dev уйдёт ещё дальше — блокирующей находкой не считаю, т.к. на этапе ТЗ рибейз не требуется.

§7.1: обязательные разделы — все на месте

Раздел §7.1 Где в ТЗ
Сценарий (персона/поверхность/момент) §1
Что человек увидит до/после §2
Проблема §3
Скоуп и не-скоуп §5
Контракт поведения §6, §7, §8, §9
UX §13
Модель данных и миграция §12
i18n §13
AC1…ACn с доказательством §14
План автотестов §15
Риски §16
Откат §17
Release-артефакты §18

Плюс необязательный, но полезный раздел §19 «Принято предположительно» — именно то место, где по правилу §7.1 должны жить технические (не продуктовые) допущения автора, доступные для оспаривания ревьюером.

Продуктовая рамка (SCOPE.md)

Job J6 — «Keep the plan true as the home evolves» — явно упоминает drag/resize, merge/split, оптимистичную блокировку как часть этой работы. Задача устраняет технический долг ровно в этой job: план не должен «портиться» незаметно после обычного редактирования. Персона (§1 ТЗ) — Home admin, единственная персона, которая вообще видит редакторы (SCOPE.md: «View mode is the product», редакторы — admin-only). Сценарий и before/after (§1–2) отвечают на оба обязательных продуктовых вопроса без терминов реализации в разделе «что увидит» — придаточные предложения там пользовательские («завершение цепочки», «после удаления или объединения комнаты»), а не кодовые идентификаторы. Технические термины (_finishWallChain, mergeCollinearPartitions и т.п.) появляются только в §3 и далее — это правильное место для них.

Открытый вопрос? Разбор по существу

Инструкция ревью требует: «не бывает сложной задачи без единого открытого вопроса». Для этой задачи он был — и уже закрыт до написания ТЗ: владелец отработал открытые продуктовые вопросы в двух отдельных комментариях анализа («Повторная аналитика после #478» и «Поправка… off-grid геометрия мебели»), явно сузив скоуп (исключил room_drafts/room-acceptance, разобранные #478) и дав прямое продуктовое решение по мебели/изображениям («их transform должен сохраняться целиком»). Раздел §19 документирует остаточные технические развилки (не продуктовые), как и требует процесс: продуктовая неопределённость устранена перепиской до захода в S3-spec, техническая — явным блоком «поменять свободно». Отдельного неотвеченного продуктового вопроса в тексте ТЗ не осталось, и это не тихое замалчивание, а прослеживаемый результат.

Что проверено буквальным чтением кода (не поверил на слово)

Все ссылки — на исходники на SHA aa962ed2 (текущий dev, из которого ТЗ исходит: продуктовый код в этой задаче ещё не пишется).

  • _finishWallChain (src/houseplan-editor-runtime.ts:1474-1484) — подтверждено: тело только очищает _path, _activeWallChainId, _activeWallChainPartitionIds, _wallChainSegmentCms, _wallChainRedo, _closingWallCm, вызывает _clearPlanSnapHover(). Merge/reconcile не вызывается — центральное утверждение проблемы (§3) верно буквально.
  • _mergeSpacePartitions (houseplan-editor-runtime.ts:1458) — существует, принимает seedIds?: string[], оборачивает mergeCollinearPartitions; по всему src/*.ts вызывается только собственным определением/обёрткой в houseplan-card.ts — реально нигде не вызывается в рантайме. Утверждение «фактически не вызывается» — верно.
  • mergeCollinearPartitions (src/wall-merge.ts:148) — контракт seed-scope, epsilons, детерминированный survivor, цикл до fixed point — присутствуют в коде уже сейчас (задел под #229). ТЗ §6.1 описывает использование существующего контракта, а не выдумывает новый.
  • commitWallChainSegmentGeometry() (src/draft-live-commit.ts:60) — прочитан целиком: один local physical check, один junction pass, один _recordGeometry + один _saveConfig(); ни merge, ни reconcile, ни optimizePlans не вызываются. §11 верно буквально.
  • _commitPhysicalGeometry guard wallModelOffGridValueCount(after) <= wallModelOffGridValueCount(before, authored) — подтверждён (houseplan-editor-runtime.ts:2113-2114, тот же паттерн в draft-live-commit.ts:107-108). wallModelOffGridValueCount определена в src/wall-segment-model.ts:88.
  • _canCommitSpace (houseplan-card.ts:1475) — admin/authority guard, используется в путях commit — §12 «current writer не расширяет права» не голословно.
  • _confirmRoomDelete (houseplan-editor-runtime.ts:5485-5579) и _commitMerge (:6408-6430) прочитаны целиком: обе фильтруют sp.rooms, ни одна не трогает marker.room_id или marker.vacuum.segment_map. Починка orphan room_id сейчас существует только в space-reference-repair.ts, вызываемой из plan-optimizer.ts — то есть баг реален именно там, где заявлен (§3/§8): чинится только следующим Optimize, а не в момент удаления/слияния.
  • resizeFurnitureTransform (src/furniture.ts:524) — существует, комментарий в коде прямо говорит про continuous resize (#383).
  • alignAllToGrid (src/align-grid.ts:145) — decor-цикл не выделяет furniture/image в отдельную ветку и гонит их через общий box-snap, инкрementируя moved/maxShiftCm — подтверждает текущий баг, который §9 ТЗ должен закрыть.
  • docs/CANVAS.md §9.4 (строка 512) — «Furniture resize is the explicit exception to positional quantisation… Furniture placement and movement remain grid-bound» — дословно то же, что заявляет §9 ТЗ. Раздел пока не упоминает image, но §18 ТЗ обязывает обновить CANVAS.md этим же коммитом — не расхождение, а зафиксированный будущий DoD.
  • docs/WALL-THICKNESS.md (строка 512) — контракт #276/#281/#296 (proven section absorption, deterministic residuals, max(roomCm, partitionCm)) присутствует и совпадает с §6.2–6.3 ТЗ.
  • Performance-бюджеты (demo/benchmark_wall_draw_click.mjs:51-57) — medianMs: 150, maxMs: 250, remoteMedianMs: medianMs*1.5+20 совпадают с числами §11 ТЗ буквально — не выдуманы.
  • #282/#314 — реальные, ранее принятые контракты (ADR стабильной идентичности стен и rollback после отказа записи), многократно цитируются в docs/ARCHITECTURE.md, docs/CONFIG-COMPATIBILITY.md, комментариях кода — не выдуманные номера.
  • marker.vacuum.segment_map — поле есть в src/types.ts:140.
  • docs/specs/README.md и #478 — запись про #478 подтверждена, docs/specs/478-remove-room-drafts.md и docs/reviews/CODE-REVIEW-478-r*.md существуют: #478 реализована и прорецензирована, а не просто задумана — ТЗ #477 корректно опирается на закрытый факт, а не на обещание.
  • _routeDepartureHandled, hashchange-listener в houseplan-card.ts — подтверждают, что «route departure» и смена space через hash — существующие механизмы, на которые §6.4 ссылается как на finish-owners.
  • async_save_config_state/canonicalize_config_geometry (custom_components/houseplan/store.py:200,220) — вызов канонизации перед записью подтверждён; сам факт («числовая канонизация — на каждой записи», из тела issue) верен. Не искал отдельно, называется ли обёртка ровно async_save_config_state во всех путях сохранения — не критично для ТЗ, которое само по себе на эту функцию не опирается (это констатация уже закрытого класса, не предмет #477).

Отдельно проверил семантическую состоятельность формального инварианта §4: optimizePlans(P).changed === false ⇒ optimizePlans(W(P)).changed === false для одной операции W индукцией распространяется на последовательность операций (после W(P) условие снова выполнено, значит следующая W' сохраняет его же). Формулировка корректна и не скрывает индукционный шаг.

Три границы, которые владелец попросил проверить отдельно

  1. Seeded reconciliation (§6.1–6.2). Scope ограничен «связным merge-компонентом, к которому относится хотя бы один seed» — это осмысленная граница: incidental collinear соседи, физически примыкающие к только что дорисованной цепочке, обязаны мержиться (иначе шов остаётся), а несвязанные партиции плана трогать нельзя (иначе цена unbounded и это прямо запрещено §5.2 «merge partitions всего пространства на каждом клике»). §11 честно называет риск («если нельзя доказать без full-space sweep — назад в S3-spec») вместо того, чтобы тихо платить цену на каждом клике.
  2. Undo/Redo после finish (§7). Контракт различает активную цепочку (буквальный snapshot, посегментный Undo) и завершённую (canonical snapshot через тот же lossless finalizer на каждом шаге истории) без добавления скрытой history-команды — соответствует принципу «пользователь рисовал стены, а не запускал скрытую команду» (§6.3) и не открывает второй источник этой же геометрии.
  3. Furniture/image exclusion (§9). Формулировка «весь transform… byte- equivalent» и явное решение не пытаться отличить legacy off-grid transform от нового legal continuous resize (провенанс недоступен) — прямое повторение продуктового решения владельца из второго комментария, ничего не додумано.

Находки

Нет. High: 0, Medium: 0, Low: 0.

Единственное отмеченное выше по разделу «Как проверялось» (branch не рибейзнут на текущий dev) — не находка, а операционное замечание для следующего этапа; на этапе ТЗ (класс C, без кода) оно не блокирует переход в S5-ready.

Что не проверял

  • Не проверял python -m pytest / typecheck / build — на ветке нет продуктового кода, гейты неприменимы к этапу ТЗ.
  • Не проверял, что ровно эта функция называется async_save_config_state во всех путях сохранения бэкенда (см. пояснение выше) — не влияет на выводы ТЗ.
  • Не оценивал реализуемость perf-бюджета §11 количественно (насколько реалистичен «не более 1.5× median на удвоении unrelated партиций») — это то, что должен доказать сам код и его perf-witness на код-ревью, а не ТЗ; сам числовой контракт корректно унаследован от #461.
  • Не проводил самостоятельный код-ревью существующих wall-merge.ts / coincident-partitions.ts на предмет иных дефектов, не связанных с #477 — вне скоупа ревью ТЗ.
  • Единственная умышленно неполная иллюстрация — таблица §10 «минимальная матрица» не перечисляет явно Split Room. Это не пробел ТЗ: сама таблица названа «минимальной», а AC8 требует от реализации exhaustive-перечисления всех текущих structural/layout writers с тестом, который краснеет на недостающей строке — split, если у него найдётся долг, будет пойман тем же AC8 на код-ревью. Ни владелец, ни разбор автора не называли Split источником долга, и текущий код Split строит room-граничные стены, а не независимые партиции, так что источника такого класса проблем там структурно нет — но это не проверено построчным чтением _splitClick/ splitRoom, поэтому отмечаю как то, что не проверял, а не как принятое на веру утверждение.

Итог

ТЗ полное по §7.1, каждый AC проверяем и указывает способ доказательства, технические допущения явно обособлены в §19 и оспоримы, продуктовые вопросы закрыты владельцем до входа в этап ТЗ, а все проверенные технические утверждения о текущем состоянии кода подтвердились буквальным чтением — без единой догадки, выданной за факт. Три специально запрошенных владельцем границы (seeded reconciliation, Undo/Redo, furniture/image exclusion) разобраны предметно и непротиворечиво. Оснований для Medium/High нет.

Вердикт: зелёный.


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

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