Files
houseplan-card/docs/reviews/SPEC-REVIEW-310-r2.md
T
2026-08-25 20:10:38 +00:00

81 lines
19 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# SPEC-REVIEW-310-r2
Issue: #310 «Парный острый стык: вернуть полное остриё, зубец торца толстой стены срезать по граням острия (follow-up #309)»
Этап: ревью ТЗ (PROCESS.md §2.4)
Документ ТЗ: `docs/specs/310-pair-apex.md` (ветка `issue/310-pair-apex`, ревизия 2, коммит `8d41651b`)
Заход: r2 · блокирующих циклов израсходовано 1 из 4 (потрачен r1 — жёлтый; сам r2 бюджет не тратит, если вердикт зелёный, §227)
Метка: `small` не установлена (labels: `bug`, `P2`, `polish`, `S4-spec-review`) → полный трек подтверждён, файл ТЗ обязателен и на месте — без изменений с r1.
## Скоуп
Раунд r1 (`docs/reviews/SPEC-REVIEW-310-r1.md`, база `c8d56b6c`) поставил один блокирующий High: AC5 и Риск №2 называли единственным гейтом `junctionContractHoles`, а тот структурно не видит узел-пару (`buildMultiWallNodeMap` отбрасывает узлы с < 3 канонических лучей — `src/wall-thickness.ts:1984,2032`), то есть главный риск задачи («дыры на границе нового среза») оставался без рабочей проверки именно там, где #310 вводит новую операцию.
Дельта r1→r2 — коммит `8d41651b` (`docs: spec #310 revision 2 — grid contract for two-ray nodes per review r1`), `docs/specs/310-pair-apex.md`, **+4/-4 строки**, единственный файл в диапазоне:
```
git diff c8d56b6c..8d41651b -- docs/specs/310-pair-apex.md
```
Затронуты ровно 4 места: строка статуса, Риск №2 (§5), AC5 (§7) и строка плана тестов (§8). Ни один другой раздел ТЗ не менялся — сценарий, решения владельца, контракты §2.1/§2.2/§2.3, скоуп/не-скоуп, UX/данные/i18n, риски 1 и 3, release-артефакты, AC1–AC4/AC6/AC7, откат идентичны байт-в-байт коммиту `c8d56b6c`, уже проверенному в r1.
Дельта затрагивает ровно тот AC, который r1 назвал дефектным, и ничего за его пределами — ни ребейза на ушедший вперёд `dev`, ни смены контракта поведения (§1/§2.1–2.3 не тронуты), ни новой подсистемы. Условие §2.10 «разбор по дельте, а не заново» применимо без оговорок: полный повторный разбор не требуется, разбор ограничен closure High-находки и проверкой, что правка не расшатала соседние AC.
Изменений продуктового кода в диапазоне нет (весь коммит — docs). Гейты `tsc`/`test`/`build`/`invariants` не запускал — на этапе ТЗ код не менялся, гонять нечего (то же основание, что и в r1).
## Как проверялось
1. Найден вердикт r1 и SHA, на котором он получен: `docs/reviews/SPEC-REVIEW-310-r1.md`, база `c8d56b6c` (сама r1 явно называет коммит в шапке — не находка, в отличие от типового случая, который описывает эта роль).
2. `git diff c8d56b6c..8d41651b -- docs/specs/310-pair-apex.md` — дельта объявлена и процитирована выше целиком (4 хунка).
3. По High-находке r1: прочитан новый текст §5 Риск №2 и §7 AC5 построчно, сверен с двумя вариантами правки, которые предлагала r1 («(а) отдельный способ доказательства для узла-пары — сэмплирование окрестности без фильтра по числу лучей» или «(б) расширить вход `junctionContractHoles`»). Определено, какой вариант выбран и закрывает ли он именно то, что было названо дефектом.
4. Перечитан код `buildMultiWallNodeMap` (`src/wall-thickness.ts:1984,2032`) и `linearWallJoinPatches` (парная ветка) повторно — не потому что он изменился (не менялся), а чтобы независимо проверить корректность нового текста AC5: действительно ли формула «полоса A ∪ полоса B ∪ mitre-патч − клинья» отражает то, как тело узла-пары фактически строится, и не является ли обоснование «клин лежит строго снаружи наружной грани соседа» голословным.
5. Проверена нотация: `h` в новом AC5 («радиус 3·max(h), шаг ≤ h/4») сверена с уже установленным в `docs/WALL-THICKNESS.md:207` употреблением «`VISUAL_MITRE_LIMIT = 1.5` maximal half-depths» (`h` = полутолщина) — та же нотация, что и в AC1 ТЗ («вылет больше 1.5·h»), не новое обозначение и не расходится по документу.
6. Проверено логически, различает ли новая эквивалентность (⇔, а не ⊇ как в общем инварианте `docs/WALL-THICKNESS.md:201`) все три мутанта §7.7 (a — фаска возвращена, b — торцевой трим отключён, c — трим не ограничен окрестностью): для (a)/(c) реальное тело теряет точки, которые формула требует как кладку → нарушение направления «формула ⇒ реальность»; для (b) реальное тело сохраняет зубец, которого формула (за вычетом клина) не предусматривает → нарушение направления «реальность ⇒ формула». Оба направления содержательны, эквивалентность — не избыточное усиление и не пропуск.
7. Прочитано тело issue #310 и последний комментарий владельца/автора — подтверждено, что ревизия 2 запушена именно в ответ на r1 и что автор просит перезапуск ревью, а не оспаривает находку.
8. Перепроверено, что низкая (Low) находка r1 (блок «принято предположительно») была снята самой r1 без требования правки — ревизия 2 её не касается, повторной проверки не требует.
## Закрытие раунда r1
| Находка r1 | Чем закрыта | Где это видно |
|---|---|---|
| **[High]** AC5/Риск №2 называют детектором `junctionContractHoles`, который структурно не видит узел-пару (< 3 лучей отбрасываются `buildMultiWallNodeMap`) — главный риск задачи без рабочей проверки. | Риск №2 переписан: явно называет причину («детектор #302 узлы-двойки не видит: `buildMultiWallNodeMap` требует ≥3 лучей») и вводит раздельные гейты по типу узла. AC5 переписан: для узлов ≥3 лучей остаётся прежний `junctionContractHoles`; для узлов-двоек вводится независимый **парный сеточный контракт** — сэмплирование квадратной окрестности узла (радиус `3·max(h)`, шаг `≤ h/4`) на фактическом `physicalBodySet.geometry`, сверяемое с декларативной формулой `(полоса A ∪ полоса B ∪ mitre-патч) − клинья #310`, явно на узле `spike` фикстуры `309-junction-teeth` и на синтетических парах (острая/90°/почти-параллельная). Это в точности вариант (а), который r1 предлагала как приемлемое закрытие («прямая проверка через сэмплирование окрестности узла на `physicalBodySet.geometry`… без фильтра по числу лучей»). | `docs/specs/310-pair-apex.md` §5 (Риск №2) и §7 AC5, коммит `8d41651b`, `git diff c8d56b6c..8d41651b` (хунки 3 и 4). |
| **[Low]** Технические решения (фазировка нового трима) не оформлены явным блоком «принято предположительно» — снято самой r1 без требования правки. | Правки не требовалось; ревизия 2 раздел не касается. | Не применимо — находка закрыта решением ревьюера в r1, автор ничего не менял. |
## Унаследовано из r1
Без повторной проверки в r2 принято (документ и SHA: `docs/reviews/SPEC-REVIEW-310-r1.md`, база `c8d56b6c` — соответствующий текст ТЗ в этих разделах байт-в-байт идентичен `8d41651b`):
- **Продуктовая рамка (§0 сценарий, «до/после»).** Персона (Home admin, десктоп-редакторы), связь с `docs/SCOPE.md`, отсутствие расширения скоупа — точечный визуальный баг-фикс, follow-up #309.
- **Решения владельца перенесены точно (§1).** Буквальное совпадение с текстом обоих комментариев владельца в issue.
- **Контракт §2.1 (парная ветка mitre).** Построчно сверен с `wall-thickness.ts:1193-1197`; `MITRE_LIMIT`/`chamferApex` действительно не участвуют в парной ветке до этой задачи.
- **Контракт §2.2 (торцевой трим).** Обоснован реальной геометрией `linearWallBody` (строка 1062), а не догадкой.
- **Ручной пересчёт узлов фикстуры `309-junction-teeth`.** `step` — 4 луча, `spike` — 2, `hump` — 3; заявление «step/hump не трогаются» подтверждено.
- **Перечень потребителей парных патчей (§3).** Три вызывающих места `linearWallJoinPatches` и четыре сайта `physicalBodyParts(...)` в `houseplan-card.ts`/`space-render.ts` — полны.
- **Не-скоуп корректен.** `virtualJunctionPatches`/`unionJunctionPatches` и механизм #249 — независимы от узла-пары, исключение обоснованно.
- **AC1, AC3, AC4, AC6, AC7 однозначны**, называемые идентификаторы/тесты/golden-сцены существуют в коде.
- **Риск №1** (возврат «хвостов») — осознанное и уже принятое владельцем поведение.
- **Риск №3** (интерференция #271+#310) — адекватно закрыт юнитом на коротком толстом саппорте пары; степени узла (#271 требует ≥3 луча, #310 — ровно 2) взаимоисключающие, кроме случая разных концов одного отрезка — учтено тест-планом.
- **Release-артефакты, UX/данные/i18n, откат (§4, §6, §9)** — полны, без миграций и новых полей.
- **Трейлеры и класс изменения.** `Issue: #310`, `User-Visible: no` на месте и в `8d41651b`; класс C (документация) корректен для обоих коммитов ветки.
## Что проверено заново и корректно (дельта r2)
- **AC5 закрывает названный риск, а не переформулирует его.** Новый гейт для узла-пары сэмплирует реальную (`physicalBodySet.geometry`), а не повторно вычисленную кодом под тестом, геометрию — сравнение с независимо заданной формулой (полосы — простая прямоугольная геометрия стены, mitre-патч — то же `[node, pA, hit, pB]`, что и в AC1, клин — декларативное полупространство из §2.2) не тавтологично: ошибка именно в операции вычитания (риск №2) проявится как несовпадение формулы и факта, а не спрячется за общим кодом.
- **Направление проверки (⇔, не ⊇).** В отличие от общего инварианта `docs/WALL-THICKNESS.md:201` (`body ⊇ strips ∪ fans`, допускающего лишний материал у вееров), для пары выбрана более строгая эквивалентность — обоснованно: она одновременно ловит и недостачу (дыра, риск №2, мутанты a/c), и избыток (зубец не срезан, мутант b). Ослабление до ⊇ пропустило бы мутант (b).
- **Нотация `h` и радиус сэмплирования.** `h` = полутолщина, употребление совпадает с уже принятым в AC1 и `docs/WALL-THICKNESS.md:207`; радиус `3·max(h)` заведомо превышает зону вмешательства трима (`2·halfDepth(своей)` из §2.2) с запасом — окно сэмплирования покрывает всю область, где дыра в принципе может появиться, а не только формально названную.
- **Явное закрытие узла `spike`.** В отличие от r1-версии AC5 (обезличенная формулировка «на фикстуре и сценах»), новая формулировка называет именно узел `spike` фикстуры `309-junction-teeth` — тот самый двухлучевой узел, который r1 показала исключённым из `map.nodes`.
- **§8 план тестов согласован с §7 AC5** без противоречий: юнит парного сеточного контракта на `spike`-узле и синтетике плюс отдельный юнит детектора #302 на фикстуре для узлов ≥3 — разделение по типам узлов проведено последовательно во всех трёх местах (Риск №2, AC5, план тестов).
- **Дрейф формулировки «юнит + смок» → «юнит».** В r1-версии AC5 стояло «юнит + смок»; в r2 упоминание смока пропало у обеих ветвей гейта. Это не регрессия: `smoke_junction_holes` (`docs/WALL-THICKNESS.md`) — существующий wiring-проб именно для инварианта вееров ≥3-лучевых узлов, который в этой задаче не меняется (§2.3), а не для новой парной ветки; отдельного смока для пар никогда не существовало, и golden-сцена `junction-309-spike-dark` (AC2) закрывает ту же цель — «форма дошла до реального потребителя рендера» — визуально, через настоящий пайплайн. К AC5 претензий нет; выбор конкретных browser-смоков для запуска в этой задаче (если появятся) остаётся за код-ревью через `scripts/smoke-select.mjs` по фактическому диффу кода.
## Чего не проверял
- Полный обход `demo/golden/matrix.mjs` на прочие 2-лучевые сцены за пределами трёх названных `junction-309-*` — унаследовано из r1 (там же не проверялось), причина та же: код не менялся, гонять `npm run golden:verify` не на чем.
- Численную корректность формул (`chamferApex`, поведение при `sin ≈ 0` в парной ветке) — унаследовано из r1, это предмет код-ревью реализации.
- `npx tsc --noEmit` / `npm test` / `npm run build` / `node scripts/check-docs.mjs` — не запускал; диапазон `c8d56b6c..8d41651b` не содержит правок `src/**` и тестов, гонять нечего на этапе ТЗ.
- `npm run invariants` — задача не трогает модель данных (`layout`, записи толщины, `marker.space`, `open_spans`), только вычисляемую геометрию рендера; инвариант о ключе решёточного ребра неприменим — унаследовано из r1.
- Реализацию AC5 как теста (существует только как описание в ТЗ; тест ещё не написан) — предмет код-ревью, когда появится диапазон с кодом.
## Итог
Дельта r1→r2 — 4 строки в `docs/specs/310-pair-apex.md`, целиком внутри Риска №2, AC5 и строки плана тестов, то есть ровно там, где r1 поставила блокирующий High. Новый AC5 вводит для узла-пары независимый сеточный контракт «формула vs факт» на реальной геометрии, явно называет проблемный узел `spike`, использует установленную нотацию без новых неоднозначностей и по построению (проверено логически, направление ⇔) закрывает и риск «дыра», и все три мутанта §7.7 — то есть реализует вариант (а), который r1 сочла приемлемым. Низкая находка r1 закрыта ранее и ревизии не касалась. Новых находок нет.
**Вердикт: зелёный.**