docs: review document for #249

Issue: #249
User-Visible: no
This commit is contained in:
claude[bot]
2026-08-23 02:18:37 +00:00
parent 8d2e00bbbf
commit 748d48424b
+261
View File
@@ -0,0 +1,261 @@
# SPEC-REVIEW-249-r1
- Issue: [#249](https://github.com/Matysh/houseplan-card/issues/249) — «Стык трёх стен разной толщины рисуется шипом наружу вместо соединённого узла»
- ТЗ: `docs/specs/249-multiwall-junction-bevel.md`, ветка `issue/249-multiwall-junction-bevel`, коммит `8d2e00bbbfbb9151ca2079eaf6e51f916cb59eff`
- Этап: spec (PROCESS.md §2.4), заход r1, трек обычный (не `small`)
- Вердикт: **жёлтый**
## Скоуп ревью
Первый заход — разбор полный. Разделов «Закрытие раунда» и «Унаследовано»
нет: PROCESS.md §2.10 применяется начиная со второго цикла, а вердикт
предыдущего раунда по этой задаче отсутствует (в комментариях issue есть
только этапы аналитики и продуктовых Q&A, ревью ТЗ ещё не проводилось).
Проверено по порядку из инструкции:
1. `docs/SCOPE.md` — сценарий закрывает J1 («Show the whole home... live
spatial overview»): дефект постоянно виден в View/kiosk, где View — сам
продукт для двух из трёх персон. Не найдено конфликтов с `TOUCH-SUPPORT.md`
— правка не создаёт и не меняет интеракций.
2. `AGENTS.md`, `PROCESS.md` §2.4, §2.5, §7.1, §4, §12 — обязательные разделы
ТЗ, критерии DoR, формат вердикта, лимит циклов.
3. Тело issue #249 и все пять комментариев: воспроизведение и корневая
причина автором ТЗ, продуктовые вопросы Q1/Q2 с предложенными default,
решения владельца, объявление о готовом ТЗ.
4. `docs/USER-GUIDE.ru.md` — термины «стык», «mitre/bevel» (строка 487)
уже существуют и согласуются со словарём ТЗ («митра», «фаска»); новой
пользовательской терминологии эта задача не вводит (чистый bugfix
геометрии, не новая UX-возможность).
5. Канонический документ подсистемы — `docs/WALL-THICKNESS.md` целиком
(277 строк): модель `walls`, рост ±½, единый structural pass, `MITRE_LIMIT`,
virtual-junction patches и их fail-isolation (#197), тестовый раздел §8.
## Как проверялось
Ревью состязательное: каждое техническое утверждение ТЗ сверялось с кодом на
`HEAD`, а не принималось на слово автора.
- `src/wall-thickness.ts` — подтверждены все процитированные в issue и ТЗ
места: `export const MITRE_LIMIT = 4` (строка 46); `insetContour()` со
сравнением `dist <= MITRE_LIMIT * maxO` (строка 1030, функция начинается на
967); `outsetContour()` с идентичным сравнением (строка 2238, функция
начинается на 2183); митра-лимит для independent-junction patches (строка
1853); лимит на строке 701. Все совпадают с номерами строк, названными в
теле issue и ТЗ §2.
- Строка 626: `same.halfDepth = Math.max(same.halfDepth, halfDepth)` —
подтверждает, что «совпадающие сонаправленные фрагменты берут максимальную
полутолщину» (ТЗ §6, `ray`) не изобретённое правило, а уже существующий в
коде паттерн дедупликации интервалов.
- `src/wall-merge.ts` — `junctionAt()` (строка 101) и `spaceMergeGeometry()`
(строка 278) существуют и, как верно указано в ТЗ §2/§6, работают с
independent partitions, а не с чисто комнатными узлами — что и объясняет,
почему баг не ловится существующим junction-кодом.
- Пересчитаны числа из ТЗ §2 самостоятельно: `cell_cm=30` даёт множитель
`wallCmToUnits`, при котором 50 см → 3.4722, 70 см → 4.8611 render units;
измеренная join-вершина `8.7312 / 4.8611 = 1.7958 ≈ 1.80×H`, что совпадает
с текстом ТЗ и подтверждением владельца в третьем комментарии issue. Числа
внутренне непротиворечивы и не выданы за факт без проверки.
- `docs/WALL-THICKNESS.md` §3 «Body render»: «Mitre joins; bevel when the
mitre spike exceeds `MITRE_LIMIT × thickness`» и §8 (тестовый охват) —
подтверждают, что описанный в ТЗ переход mitre→bevel является
существующим контрактом для степени 2, который ТЗ §7.3 явно сохраняет
без изменений.
- Сопоставлены заголовки шести последних полноформатных ТЗ (`248`, `244`,
`239`, `229`, `197`, `198`) — все имеют отдельные разделы «UX, i18n,
accessibility и touch», «План [реализации и] автотестов» и «Риски,
производительность и rollback/security». См. находки ниже.
## Разделы §7.1 — проверка полноты
| Требуемый раздел | В ТЗ #249 | Комментарий |
|---|---|---|
| Сценарий | §1 | ок |
| Что человек увидит до/после | §3 | ок, размещён после «проблемы» — порядок не регламентирован |
| Проблема | §2 | ок (воспроизведение + корневая причина) |
| Скоуп и не-скоуп | §5 | ок |
| Контракт поведения | §7 | ок по объёму, но см. Medium-находку M2 (внутреннее противоречие §7.2.1/7.2.2) |
| UX | §8 (одна строка: «Нет новых i18n, настроек, действий, жестов либо различий mouse/touch») | контента по существу достаточно (правка не создаёт новых интеракций), но отдельного раздела нет — см. Low ниже |
| Модель данных и миграция | §8 (первые пункты) | ок |
| i18n | §8 (та же строка) | ок, новых строк нет |
| AC1…ACn с доказательством | §9 | ок, см. таблицу ниже |
| План автотестов | распределён по «Доказательство:» в каждом AC | по содержанию покрыто, но не собран отдельным разделом — см. Low |
| **Риски** | **отсутствует** | **см. Medium-находку M1** |
| Откат | §11 | ок |
| Release-артефакты | §10 | ок |
## AC — однозначность и способ доказательства
| AC | Однозначен? | Доказательство названо и выполнимо? |
|---|---|---|
| AC1 | Да — конкретный узел, конкретные числа `H=4.8611`, `R=1.25×H`, инварианты связности | unit + новый fixture `test/fixtures/249-multiwall-junction.json` — файл ещё не существует (ожидаемо для ТЗ), путь и формат согласованы с существующим паттерном фикстур в `test/` |
| AC2 | Да по формуле, но см. M2 — сама граничная формула (§7.2.1 vs §7.2.2) внутренне противоречива в полосе `(R, R+epsilon]`, что делает пограничный случай матрицы недоказуемым однозначно | table-driven unit, численный bound + сравнение нормализованной геометрии — метод выполним для всех точек вне спорной полосы |
| AC3 | Да — «те же точки/paths», «без обновления двухлучевых expected values» — сильная, проверяемая гарантия регрессии | unit regression на существующих `insetContour()`/`outsetContour()` тестах |
| AC4 | Да — все поверхности через один smoke с path/geometry assertions и cache parity | `demo/smoke_multiwall_junction.mjs` — новый файл, обязателен к запуску локально перед `S7-code-review` (`AGENTS.md` «Гейты») |
| AC5 | Да — golden с semantic assertions на число лучей и габарит, что явно исключает пустой/неверно кадрированный PNG (защита от регресса #234) | `demo/golden/matrix.mjs` + `test/golden-matrix.test.mjs`, приёмка эталона только по `npm run golden:accept -- --reviewed` на полном Linux CI |
| AC6 | Да, стандартная формулировка deep-equality | unit + существующий зелёный набор без изменений |
| AC7 | Да, состав гейтов реализации соразмерен задаче (§8 PROCESS.md): typecheck/test/build/targeted smoke здесь, golden/полный smoke/perf — предрелизно | команды названы точно |
Ни один AC не содержит домысла о поведении, которого нет ни в одном
документе: контракт §7 — прямое продолжение уже существующего
`MITRE_LIMIT`/bevel-механизма из `docs/WALL-THICKNESS.md` §3, распространённое
на узлы степени 3+ по явному решению владельца (Q1/Q2 в комментариях issue),
а не изобретённое автором. Открытых продуктовых вопросов в тексте не
осталось — оба вопроса, заданные владельцу, получили ответ и дословно перенесены в §4 ТЗ.
## Находки
Блокирующих (High) находок нет.
### Medium — в скоупе задачи, чинится в этом же ТЗ
**M1. Обязательный раздел «Риски» отсутствует полностью.**
`PROCESS.md` §7.1 перечисляет «риски» как отдельный обязательный раздел ТЗ, а
DoR-чеклист §2.5 требует «риски перечислены» как блокирующий пункт готовности
к разработке. В ТЗ #249 такого раздела нет вовсе (проверено `grep -ni
"риск"` по всему файлу — единственное упоминание это унаследованная из
аналитики метка «Сложность/риск: 5/10 и 6/10» в шапке документа, без единого
слова разбора). Для сравнения — все шесть последних полноформатных ТЗ (`239`,
`244`, `248`, `229`, `197`, `198`) содержат отдельный раздел с конкретными
пунктами и мерами снижения.
Здесь эта пустота предметна, а не формальна: сама задача в issue названа
владельцем «риск 6/10» именно потому, что правка меняет каноническую
геометрию узла для **любого** узла из 3+ разных физических лучей — а такие
узлы (обычные T-стыки внутренних перегородок с наружной стеной) есть почти в
каждом сохранённом плане, не только в приложенном экспорте. Правка сужает
допустимый габарит join-вершины с `4×H` (старый `MITRE_LIMIT`) до `1.25×H`
для всех узлов степени 3+ — то есть потенциально меняет вид существующих,
сегодня визуально корректных T-стыков, чья текущая митра лежит в интервале
`(1.25×H, 4×H]`. AC2 проверяет только новые синтетические сценарии, AC6 явно
исключает лишь independent-partition junctions и двухлучевые углы — ни один
AC не требует прогона существующего golden-набора ради проверки именно этого
побочного эффекта на реальных (не связанных с #249) планах. Требование это
частично закрывается штатным пре-релизным гейтом (`golden:verify` на полном
наборе перед бетой, §8 PROCESS.md), но ТЗ обязано назвать этот риск явно:
«неизменность T-стыков неявно полагается на пре-релизный гейт» — это не то
же самое, что «названо в разделе рисков».
**Почему это делает ТЗ невыполнимым без правки:** без явного раздела рисков
DoR-чеклист (§2.5) формально не проходит («риски перечислены» — пункт
блокирующий), а автор реализации не получит явного указания, что перед
переводом в `S8-merged` нужно специально просмотреть diff полного golden-
прогона на предмет затронутых T-стыков за пределами фикстуры #249, а не
просто принять новый эталон.
**Предлагаемое закрытие:** добавить раздел «Риски», перенести и раскрыть уже
названные в аналитике риски («меняется каноническая геометрия узла; ошибка
способна дать щель, лишнюю кладку либо изменить обычные двухлучевые углы»),
явно указать risk «широкий blast radius: любой существующий T-стык степени 3+
может визуально сдвинуться» и назвать митигацию (полный `golden:verify` перед
бетой обязателен, а не опционален, именно из-за этого риска).
**M2. Внутреннее противоречие критерия mitre/bevel в §7.2, пункты 1 и 2.**
Текст:
> 1. существующее пересечение смещённых граней остаётся митрой, если его
> расстояние от node `<= R + epsilon`;
> 2. если пересечения нет, оно не конечное либо дальше `R`, join содержит две
> штатные offset-точки соседних граней — прямую фаску;
Для расстояния `d`, такого что `R < d <= R + epsilon`, оба условия истинны
одновременно: по правилу 1 вершина «остаётся митрой» (`d <= R + epsilon`), по
правилу 2 она же должна стать фаской (`дальше R`). Это не гипотетическая
придирка к формулировке — это прямое противоречие в самом контракте
поведения (§7 — обязательный раздел, самый важный для реализуемости),
из-за которого ни автор кода, ни автор unit-теста не могут детерминированно
решить, какой веткой идти для точки в этой полосе шириной `epsilon`. AC1 и
AC2 наследуют ту же двусмысленность, поскольку оба формулируют допуск как
`1.25 × H + epsilon`, то есть используют границу правила 1, но не оговаривают,
что при пересечении именно в этой полосе достаточно **любого** из двух
результатов (что было бы законным способом снять противоречие, если это
осознанный допуск на точность).
Практическое влияние ограничено полосой шириной `epsilon` (масштаб
машинной точности геометрии, `openEps(pitch, coordScale)`), то есть вряд ли
проявится на конкретном экспорте владельца — поэтому не блокирует, но делает
геометрический контракт не полностью однозначным, что прямо противоречит
задаче ревьюера «найти, где ТЗ не выполнимо или не проверяемо».
**Предлагаемое закрытие:** унифицировать порог — либо оба правила используют
`R` (эпсилон остаётся только допуском сравнения чисел с плавающей точкой, а
не расширением зоны «остаётся митрой»), либо явно указать, что при `d` в
полосе `(R, R + epsilon]` оба исхода (митра или фаска) считаются корректными
и не различаются ни одним AC/тестом.
### Low — не блокирует, снимается ревьюером с записью
1. **Раздел «UX, i18n, accessibility и touch» и «План автотестов» не
выделены отдельными заголовками**, в отличие от шести последних
полноформатных ТЗ (`239`, `244`, `248`, `229`, `197`, `198`), где это
устойчивый, повторяющийся шаблон. Контент по существу присутствует
(распределён по §8 и по полю «Доказательство:» каждого AC) и ничего не
пропускает по содержанию — в отличие от M1, здесь нет содержательной
пустоты, только структурное расхождение с шаблоном соседних задач.
Снимаю без требования правки: не создаёт риска для DoR и не влияет на
проверяемость AC. Автору стоит учесть при следующем ТЗ для единообразия
документов в `docs/specs/`.
## Что проверено и корректно
- Корневая причина (§2 ТЗ) подтверждена цитированием реального кода на
`HEAD`: `MITRE_LIMIT = 4`, независимые mitre-вычисления двух комнат в
`insetContour()`/`outsetContour()`, отсутствие покрытия со стороны
`junctionAt()`/`spaceMergeGeometry()` — не заявлена на веру.
- Численные значения (`H = 4.8611`, `R = 6.076...`, измеренный спайк `8.7312`,
`1.80×H`) внутренне согласованы и совпадают с независимым пересчётом.
- Продуктовые решения владельца (Q1 fallback = прямая фаска без округления;
Q2 лимит `1.25 × max half-depth` для узлов 3+ лучей, двухлучевые узлы без
изменений) перенесены в §4 ТЗ дословно, без искажений и без добавления
новых, не согласованных с владельцем решений.
- Scope/не-scope (§5) корректно исключает миграцию, изменение толщины стен,
редакторские инструменты, округлённые joins и не трогает
`linearWallJoinPatches()` — не расширяет и не сужает баг-скоуп в
рефакторинг.
- AC1–AC7 в целом именуют реальный, а не гипотетический тестовый
инструментарий: `test/wall-thickness.test.mjs`, `demo/golden/matrix.mjs`,
`test/golden-matrix.test.mjs` — все три файла существуют в текущем дереве
проекта и являются подходящим местом для новых кейсов.
- AC5 защищён от пустого/неверно кадрированного PNG semantic-assertions —
прямая мера против того же класса дефекта, что уже стоил регресса на
#234 (`docs`-джоб покраснел из-за пропущенной пересъёмки, здесь риск иного
рода — «зелёный, но бессодержательный» golden — предупреждён явно).
- Термины §6 ТЗ («ray», «node», «join vertex», дедупликация shared-интервала
между двумя комнатами) обоснованы существующим кодом (строка 626 —
`Math.max` при слиянии дублей), а не изобретены заново.
- i18n/touch/данные (§8): корректно константируют отсутствие изменений —
правка не создаёт новых строк, настроек, миграций или touch-специфичного
поведения; согласуется с характером задачи (bugfix геометрии рендера).
## Чего не проверял
- Не запускал никаких гейтов (`typecheck`/`test`/`build`) — на этапе
spec-review предмет проверки текст ТЗ, а класс A файлов в этом коммите не
менялся (дифф — только `docs/specs/249-multiwall-junction-bevel.md`).
- Не исполнял и не мог исполнить AC1–AC5 — упомянутые в них новые файлы
(`test/fixtures/249-multiwall-junction.json`,
`demo/smoke_multiwall_junction.mjs`, golden-сценарий узла) ещё не написаны;
это ожидаемо для стадии ТЗ и будет предметом код-ревью.
- Не проверял степень визуального совпадения нового «1.25×H» лимита с уже
существующими golden-эталонами, где встречаются T-стыки степени 3 —
именно это отсутствие и составляет находку M1: полный `golden:verify`
относится к предрелизному гейту, а не к этапу ТЗ, но должен быть назван
как явный риск в тексте самого ТЗ.
- Не пересчитывал вручную геометрию для матрицы AC2 (15/50/70 см, 4 луча) —
формула `R = 1.25 × max(halfDepth)` детерминирована и проверяема
тривиально на этапе код-ревью через unit-тест.
## Итог
ТЗ описывает реальный, подтверждённый по коду дефект и переносит явные
продуктовые решения владельца без искажений и без домыслов. Блокирующих
находок нет. Две находки Medium в скоупе задачи: полностью отсутствующий
обязательный раздел «Риски» (M1) и внутреннее противоречие порога
mitre/bevel в самом важном разделе — контракте поведения (M2). Обе чинятся
без переписывания ТЗ — точечным дополнением одного раздела и унификацией
одного порога, поэтому исправление не требует нового цикла продуктовых
вопросов владельцу.
**Вердикт: жёлтый · заход r1 · блокирующих циклов 1/4 · High: 0 · Medium: 2**