mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-29 03:09:36 +00:00
@@ -0,0 +1,246 @@
|
||||
# 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); объектом ревью является
|
||||
исполнимость и проверяемость ТЗ, а не корректность отсутствующей реализации.
|
||||
|
||||
## Как проверялось
|
||||
|
||||
1. Прочитаны `docs/SCOPE.md`, `AGENTS.md`, `PROCESS.md` (полностью, включая §2.10,
|
||||
§4, §7.1, §8) — это первый цикл (r1), правило «по дельте» не применяется.
|
||||
2. Прочитано тело issue #229 и оба комментария (аналитика → `S3-spec`, автор ТЗ
|
||||
→ `S4-spec-review`), продуктовые решения владельца от 2026-08-21 (§4 ТЗ).
|
||||
3. Прочитан сам файл ТЗ целиком (`docs/specs/229-merge-collinear-partitions.md`).
|
||||
4. Проверены построчно все фактические ссылки на код, которые ТЗ приводит как
|
||||
подтверждение причины и контракта: `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` (два разных допуска коллинеарности/смежности).
|
||||
Все ссылки, кроме одной, точны.
|
||||
5. Прочитан canonical `docs/WALL-THICKNESS.md` §9 (Independent partitions,
|
||||
drafts and columns) и `docs/CONFIG-COMPATIBILITY.md` (Independent-wall opening
|
||||
host, #132) — они определяют существующий контракт хостов проёмов, с которым
|
||||
должно согласовываться §8.4 ТЗ.
|
||||
6. Прочитан прецедентный код записи, который уже пересчитывает материализованную
|
||||
проекцию проёма при геометрическом изменении хозяина: перетаскивание
|
||||
перегородки (`houseplan-card.ts:7809-7826`) и прямое редактирование проёма
|
||||
(`:11996-12007`) — оба вызывают `materializePartitionOpening` после
|
||||
`resolvePartitionOpeningCompat`.
|
||||
7. Проверена корректность строки `Touch editor: not exposed` по
|
||||
`docs/TOUCH-SUPPORT.md` (документационное правило, строка 153) — значение из
|
||||
допустимого набора, обоснование («не добавляет жестов») соответствует тому, что
|
||||
вся фича живёт только на десктопном приёме цепочки в режиме «Стены».
|
||||
8. Проверена согласованность с `docs/USER-GUIDE.ru.md` (раздел 8, «Комнаты и
|
||||
стены») — существующее описание поведения («сегменты сохранятся обычными
|
||||
независимыми стенами») не противоречит будущему («меньше независимых стен»),
|
||||
план обновления документации (§15 ТЗ, одна строка) достаточен.
|
||||
9. Код не запускался, гейты не гонялись — реализации нет, оценивать нечего;
|
||||
на этапе 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_cm` 1–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).
|
||||
Reference in New Issue
Block a user