24 KiB
SPEC-REVIEW-229-r1
- Issue: https://github.com/Matysh/houseplan-card/issues/229
- ТЗ:
docs/specs/229-merge-collinear-partitions.md - Ветка / SHA:
issue/229-merge-collinear-partitions@81210f7 - Этап: spec (PROCESS.md §2.4) · трек: обычный (не
small) - Заход: r1 · блокирующих циклов израсходовано 0/4 до этого вердикта
- Ревьюер: Claude, роль «ревьюер ТЗ» (отдельная от роли аналитика/автора)
Скоуп
Оценивалось только ТЗ issue #229 — сращивание коллинеарных независимых перегородок одинаковой толщины при завершении цепочки стен, плюс аналогичное слияние в «Оптимизировать планы» для уже нарисованных планов. Код не менялся и не оценивался (правильный этап — S4, а не S7); объектом ревью является исполнимость и проверяемость ТЗ, а не корректность отсутствующей реализации.
Как проверялось
- Прочитаны
docs/SCOPE.md,AGENTS.md,PROCESS.md(полностью, включая §2.10, §4, §7.1, §8) — это первый цикл (r1), правило «по дельте» не применяется. - Прочитано тело issue #229 и оба комментария (аналитика →
S3-spec, автор ТЗ →S4-spec-review), продуктовые решения владельца от 2026-08-21 (§4 ТЗ). - Прочитан сам файл ТЗ целиком (
docs/specs/229-merge-collinear-partitions.md). - Проверены построчно все фактические ссылки на код, которые ТЗ приводит как
подтверждение причины и контракта:
houseplan-card.ts:6538(_finishWallChain, реальный номер строки метода — 6538, не 6558 как в теле issue; в самом файле ТЗ строка не называется, см. находку L1),wall-thickness.ts:1258(normalizeWallIntervals),plan-optimizer.ts:402-530(wallsMerged,spansMerged, отчёт),align-grid.ts:262(только snap независимых перегородок, без слияния),partition-openings.ts:43-90(resolvePartitionOpening,host.t,missing-partition),wall-thickness.ts:743и:2256(два разных допуска коллинеарности/смежности). Все ссылки, кроме одной, точны. - Прочитан canonical
docs/WALL-THICKNESS.md§9 (Independent partitions, drafts and columns) иdocs/CONFIG-COMPATIBILITY.md(Independent-wall opening host, #132) — они определяют существующий контракт хостов проёмов, с которым должно согласовываться §8.4 ТЗ. - Прочитан прецедентный код записи, который уже пересчитывает материализованную
проекцию проёма при геометрическом изменении хозяина: перетаскивание
перегородки (
houseplan-card.ts:7809-7826) и прямое редактирование проёма (:11996-12007) — оба вызываютmaterializePartitionOpeningпослеresolvePartitionOpeningCompat. - Проверена корректность строки
Touch editor: not exposedпоdocs/TOUCH-SUPPORT.md(документационное правило, строка 153) — значение из допустимого набора, обоснование («не добавляет жестов») соответствует тому, что вся фича живёт только на десктопном приёме цепочки в режиме «Стены». - Проверена согласованность с
docs/USER-GUIDE.ru.md(раздел 8, «Комнаты и стены») — существующее описание поведения («сегменты сохранятся обычными независимыми стенами») не противоречит будущему («меньше независимых стен»), план обновления документации (§15 ТЗ, одна строка) достаточен. - Код не запускался, гейты не гонялись — реализации нет, оценивать нечего; на этапе spec-review это не требуется (PROCESS.md §2.4/§8 говорят о коде).
Находки
Medium-1 — момент слияния при завершении цепочки шире, чем решение владельца о старых планах
Файл: docs/specs/229-merge-collinear-partitions.md, §17 «Принятые
предположения», п.4.
Формулировка ТЗ: «Слияние при завершении цепочки применяется ко всему пространству, а не только к новым записям: цепочка могла примкнуть к нарисованному ранее отрезку, и шов на стыке — тот же самый случай.»
Проблема. Владелец явно разделил два случая (§4 ТЗ, решения 2026-08-21): (1) слияние новой цепочки — сразу; (2) слияние уже нарисованных планов — только явным действием «Оптимизировать планы», с отчётом и отменой. Пункт 4 раздела «Принятые предположения» помечен как техническое решение («менять свободно»), но по факту расширяет действие правила 1 на данные, подпадающие под правило 2: он не ограничивает слияние компонентой, связанной с только что нарисованной цепочкой, а сканирует всё пространство.
Сценарий воспроизведения. В пространстве уже лежат 9 перегородок из экспорта
владельца (#228, 1.json), среди них коллинеарная пара #0/#4 — то есть ровно
тот случай, который ТЗ само приводит как мотивирующий пример и который решение
владельца п.2 явно откладывает до «Оптимизировать планы». Администратор рисует
новую, никак не связанную стену в другом углу того же пространства и завершает
цепочку. По формулировке п.4 слияние пройдёт по всему пространству и сотрёт пару
#0/#4 тоже — без отчёта и без выделенной отмены, которые п.2 обещает именно
для этого случая. Единственная защита — общая история Ctrl+Z, которая отменит
весь акт завершения цепочки (включая саму новую стену), а не только неожиданный
побочный эффект.
Почему это находка, а не варьируемая деталь реализации. Оправдание в тексте
(«цепочка могла примкнуть к нарисованному ранее отрезку») описывает узкий случай
— новый сегмент касается старого и продолжает его. Формулировка реализации шире
оправдания: она не привязана к связности с новой цепочкой. Это меняет объём
видимых изменений (§7.1 PROCESS.md: «какой объём видимых изменений входит в этот
issue» — ровно тот класс, который решает владелец), а не техническую деталь типа
имени модуля или выбора выжившего id.
Что нужно. Одно из двух: (a) сузить п.4 до компоненты связности, содержащей концы только что завершённой цепочки (транзитивно через общие концы), тогда поведение полностью укладывается в решение владельца п.1 и вопрос снимается без эскалации; (b) если задуман действительно широкий охват (он и полезнее — план чище), явно спросить владельца одним вопросом с предложенным дефолтом («слияние при завершении цепочки задевает не только новый сегмент, а всё пространство — ок?»), как требует §7.1.
Medium-2 — допуск «есть причина оставить узел» не определён для трёх из четырёх причин
Файл: docs/specs/229-merge-collinear-partitions.md, §8.2 и §8.3.
Проблема. §8.3 заявляет «один источник истины» для допусков, но называет
только два: EPS_ANGLE (коллинеарность) и EPS_JOIN (совпадение концов двух
перегородок). §8.2 перечисляет четыре причины оставить узел: третья
перегородка, ребро комнаты, колонна, конец черновика. Для первой
причины допуск неявно тот же EPS_JOIN (стык двух перегородок и есть предмет
§8.1.3). Для трёх остальных ТЗ не говорит, каким допуском и по какому
геометрическому критерию («точка на точке» vs «точка на отрезке» для ребра
комнаты) устанавливается совпадение.
Почему это блокирует однозначность AC. AC2 требует четыре отдельных unit-теста
(«ребро комнаты», «колонна», «конец черновика», третья перегородка) и заявлен как
доказываемый unit, но без зафиксированного критерия автор теста выбирает допуск
по своему усмотрению — один из тех самых «плавающих» допусков, от которых §8.3
явно старается защититься («иначе на границе слияние станет непредсказуемым»).
Конкретный риск: если для колонны или ребра комнаты возьмут допуск строже
EPS_JOIN, легитимный ULP-шум (риск, явно описанный в самом ТЗ со ссылкой на
#218/#223/#224) заставит узел остаться там, где физически есть только шум
координат, а не колонна; если допуск шире EPS_JOIN, слияние может пройти сквозь
примыкание, которое реально существует, но чуть-чуть не долетело до совпадения
концов — прямое нарушение риска №3 из §11.
Что нужно. Одна фраза в §8.2 или §8.3: причины «ребро комнаты», «колонна»,
«конец черновика» проверяются тем же EPS_JOIN (точка-в-допуске к общему концу
перегородки), либо явно называется другой, но такой же единый допуск с
обоснованием, почему он должен отличаться.
Medium-3 — §8.4 не упоминает материализованную legacy-проекцию проёма (x/y/angle)
Файл: docs/specs/229-merge-collinear-partitions.md, §8.4, §9, AC3.
Проблема. docs/CONFIG-COMPATIBILITY.md («Independent-wall opening host,
#132») документирует контракт: у хостованного проёма легacy-поля x/y/angle
— это материализованная проекция для старых фронтендов, которая должна
оставаться синхронной с host.id/host.t. Это не гипотеза, а действующий
прецедент в коде: перетаскивание перегородки (houseplan-card.ts:7809-7826) и
прямое редактирование проёма (:11996-12007) оба вызывают
resolvePartitionOpeningCompat → materializePartitionOpening сразу после
изменения геометрии хозяина, специально чтобы не оставить проекцию устаревшей.
§8.4 ТЗ описывает пересчёт host.t и переписывание host.id, но ни словом не
упоминает пересчёт x/y/angle. §9 («Данные...») утверждает «формат перегородки
не меняется; меняется их количество и host.id части проёмов» — и тоже не
называет legacy-проекцию. AC3 проверяет «координаты центра проёма в единицах
плана» и host.t ∈ [0,1] — то есть резолвленную, а не материализованную
позицию; тест по AC3 в его текущей формулировке может стать зелёным при устаревших
x/y/angle.
Сценарий воспроизведения. Слияние пересчитывает host.t двери на новую длину
хозяина, но материализованные x/y/angle остаются от старой перегородки.
Дверь физически на месте для текущего фронтенда (он всегда резолвит через
host), но откат на старый фронтенд (или любой код, который по прецеденту
CONFIG-COMPATIBILITY.md вправе читать x/y/angle напрямую) увидит дверь там, где
она была до слияния — ровно тот класс проблемы, для которого прецедентный код
уже существует и который #132 называет явно.
Что нужно. Добавить в §8.4 шаг «для каждого пересчитанного проёма
материализовать x/y/angle тем же вызовом, что использует перетаскивание
перегородки», и явно упомянуть это в §9. Не обязательно расширять сам AC3 (там
всё ещё резолвленная позиция — правильный инвариант), но стоит добавить это
как отдельную строку либо в AC3, либо отдельным AC, иначе ревьюер кода не будет
знать, что искать в реализации.
Что проверено и корректно
- Все требуемые по §7.1 разделы по существу присутствуют: сценарий и персона
(§1), видимое изменение (§2), причина/проблема (§3), скоуп/не-скоуп (§6–7),
контракт поведения (§8), данные/i18n/a11y (§9), performance (§10), риски
(§11), AC1…AC9 с доказательством (§12), план автотестов (§13), мутационный
гейт (§14), release-артефакты (§15), откат (§16), явный блок предположений
(§17). Заголовки не совпадают буквально с шаблоном, но это стандартная для
репозитория практика (см.
223-optimize-coordinate-canonicalization.md,226-entity-parent-dedup.md) — не находка. - Продуктовая рамка (§1–2) отвечает на оба обязательных вопроса — какая персона на какой поверхности и что человек видит без терминов реализации.
- Заявленная причина и асимметрия (§3) — не догадка: обе половины (наличие
normalizeWallIntervals/wallsMergedдля стен комнат и его отсутствие для независимых перегородок) подтверждены чтениемwall-thickness.ts,plan-optimizer.ts,align-grid.ts; номера строк точны, кроме одной (см. выше — L1, не блокирует, разобрано отдельно как низкая находка). - Три продуктовых решения владельца (§4) взяты из issue буквально, не переинтерпретированы — кроме расширения решения №2 через техническое предположение №4 (Medium-1 выше).
- AC1, AC4, AC5, AC6, AC7, AC8, AC9 однозначны, проверяемы и указывают способ доказательства; AC3 явно требует «тест красный до реализации §8.4» — то самое требование «тест должен уметь падать», зафиксированное заранее, а не постфактум.
- Допуски (§8.3) для собственно решения «сращивать/не сращивать» две
перегородки заданы через единые именованные константы, выраженные в долях шага
сетки (а не абсолютно) — правильная реакция на разброс
cell_cm1–25 (#230) и на ULP-шум (#218/#223/#224). - Границы применения (§8.5): транзитивное слияние до стабилизации и независимость от порядка обхода — оба явно проверяются отдельными AC (входят в AC1/AC6), а не декларируются без доказательства.
- Мутационный гейт (§14) покрывает по одной репрезентативной мутации на каждый из четырёх названных рисков (§11) — стандартный для репозитория уровень строгости, не исчерпывающий по всем под-случаям AC2, и это нормально (не находка).
Touch editor: not exposed— верно выбранное значение изdocs/TOUCH-SUPPORT.md§153 при отсутствии новых жестов.- Не входит в задачу (§7): стены комнат, колонны, черновики контуров, промахи примыкания (#228 п.2) — верно отнесены к соседним причинам/задачам, не присвоены этой.
- Release-артефакты (§15) называют оба changelog, обновление
docs/USER-GUIDE.ru.mdи явно отмечают отсутствие влияния на golden — сверено с текущим содержанием гайда (раздел 8), противоречия нет.
Мелкая находка (Low, снимаю с записью, не блокирует)
L1. Тело issue цитирует _finishWallChain как houseplan-card.ts:6558; в
файле метод объявлен на 6538. Разница в 20 строк — комментарии/боевой код
сдвинулись между моментом анализа и моментом чтения. Не влияет на понимание или
проверяемость ТЗ (сам файл ТЗ конкретную строку не называет), исполнитель найдёт
метод по имени. Снимаю без действия.
Продуктовые вопросы владельцу
Формально ни один из вопросов выше не обязан идти владельцу — оба фиксируемых пункта (Medium-1, Medium-2) решаются техническим сужением формулировки в ТЗ, без изменения того, что видит пользователь. Единственная точка, где владелец мог бы захотеть высказаться, — Medium-1, если авторы предпочтут вариант (b) (широкий охват всего пространства) вместо сужения (a); в этом случае вопрос будет сформулирован автором ТЗ во втором заходе с предложенным дефолтом, как того требует §7.1. Ревьюер вопросов владельцу сейчас не задаёт (согласно инструкции — такие вопросы снимаются и решаются по существу в вердикте).
Чего не проверял
- Реализацию — её не существует на этом этапе; код-ревью будет отдельной сессией
после
S5-ready→S6-in-progress→S7-code-review. - Содержимое
1.jsonиз #228 (конкретные координаты пары#0/#4) — принято на веру как результат анализа предыдущего этапа (аналитика уже это подтвердила чтением экспорта), пересчёт координат вручную не входит в задачу ревью ТЗ. docs/specs/README.mdрегистрацию — автор заявил, что запись добавлена; не влияет на вердикт содержания ТЗ, отдельно не сверял.- Существующие смоки (
smoke_subareaи смоки рисования, упомянутые в §13) — их код не читал; ТЗ верно указывает, что они должны быть прогнаны без правок на этапе реализации, это забота код-ревью, не ТЗ-ревью.
Вердикт
Медиум-находки все в скоупе задачи и чинятся правкой ТЗ, без блокирующих (High) находок. Итог — жёлтый: возврат автору на правку ТЗ, фикс проходит повторный цикл ревью (лимит для обычного трека — 4, израсходовано после этого раунда — 1).