22 KiB
SPEC-REVIEW-477-r1
- Issue: #477 — «Оптимизировать планы» не должна требоваться повторно
- Этап: ТЗ на ревью (PROCESS.md §2.4), трек полный (не
small) - Материал:
docs/specs/477-editor-writer-fixed-point.mdна SHAaa962ed2c5a03bd2f50b4ae066968761cd053585(дерево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 верно буквально._commitPhysicalGeometryguardwallModelOffGridValueCount(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. Починка orphanroom_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'
сохраняет его же). Формулировка корректна и не скрывает индукционный шаг.
Три границы, которые владелец попросил проверить отдельно
- Seeded reconciliation (§6.1–6.2). Scope ограничен «связным merge-компонентом, к которому относится хотя бы один seed» — это осмысленная граница: incidental collinear соседи, физически примыкающие к только что дорисованной цепочке, обязаны мержиться (иначе шов остаётся), а несвязанные партиции плана трогать нельзя (иначе цена unbounded и это прямо запрещено §5.2 «merge partitions всего пространства на каждом клике»). §11 честно называет риск («если нельзя доказать без full-space sweep — назад в S3-spec») вместо того, чтобы тихо платить цену на каждом клике.
- Undo/Redo после finish (§7). Контракт различает активную цепочку (буквальный snapshot, посегментный Undo) и завершённую (canonical snapshot через тот же lossless finalizer на каждом шаге истории) без добавления скрытой history-команды — соответствует принципу «пользователь рисовал стены, а не запускал скрытую команду» (§6.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— ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет. - Дерево материала:
56e39416687f5bb6bd1242ed4c5722ca2ba34cc4git log --all --format='%H %T' | grep 56e39416687f - ТЗ
docs/specs/477-editor-writer-fixed-point.md, блобa07a7248e008229e6e865ef793b213fdb524d5e4git log --all --find-object=a07a7248e008229e6e865ef793b213fdb524d5e4 -- docs/specs/477-editor-writer-fixed-point.md