25 KiB
SPEC-REVIEW-249-r1
- Issue: #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, ревью ТЗ ещё не проводилось).
Проверено по порядку из инструкции:
docs/SCOPE.md— сценарий закрывает J1 («Show the whole home... live spatial overview»): дефект постоянно виден в View/kiosk, где View — сам продукт для двух из трёх персон. Не найдено конфликтов сTOUCH-SUPPORT.md— правка не создаёт и не меняет интеракций.AGENTS.md,PROCESS.md§2.4, §2.5, §7.1, §4, §12 — обязательные разделы ТЗ, критерии DoR, формат вердикта, лимит циклов.- Тело issue #249 и все пять комментариев: воспроизведение и корневая причина автором ТЗ, продуктовые вопросы Q1/Q2 с предложенными default, решения владельца, объявление о готовом ТЗ.
docs/USER-GUIDE.ru.md— термины «стык», «mitre/bevel» (строка 487) уже существуют и согласуются со словарём ТЗ («митра», «фаска»); новой пользовательской терминологии эта задача не вводит (чистый bugfix геометрии, не новая UX-возможность).- Канонический документ подсистемы —
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 exceedsMITRE_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.
Текст:
- существующее пересечение смещённых граней остаётся митрой, если его расстояние от node
<= R + epsilon;- если пересечения нет, оно не конечное либо дальше
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 — не блокирует, снимается ревьюером с записью
- Раздел «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