24 KiB
SPEC-REVIEW-197-r1
- Issue: https://github.com/Matysh/houseplan-card/issues/197
- ТЗ под ревью:
docs/specs/197-junction-patch-fail-dark.md(коммитfe7b28f), обычный трек — неsmall/trivial - Роль: ревьюер ТЗ (не автор), этап
S4-spec-review - Трек: обычный, лимит циклов ревью ТЗ — 4 (§4 PROCESS.md)
- Цикл: r1/4
Скоуп ревью
ТЗ #197 предлагает исправление: один вырожденный virtualJunctionPatches()
patch (шум IEEE-754 в координатах, из-за которого union() в polyclip-ts
бросает исключение) обнуляет весь результат wallBodiesGeometry(), поскольку
цикл добавления junction-патчей (src/wall-thickness.ts:1760-1761) — в отличие
от соседних циклов room-ring и wall-edge — не изолирует отказ одного элемента.
Заявленное решение: численная стабилизация координат patch + per-patch
try/catch, без изменения persisted-схемы, i18n, touch-контракта, оптимизатора
и без правки #198/#199 (сознательно вынесены отдельно).
Не в скоупе ревью: продуктовый код. На ветке issue/197-junction-patch-fail-dark
лежит один коммит (fe7b28f, Issue: #197 · User-Visible: no) — только
ТЗ и правка docs/specs/README.md, реализации нет. Гейты
(typecheck/test/build) не прогонялись — на этапе ревью ТЗ продуктового
кода не существует, что вне скоупа этапа (PROCESS.md §2.4/§8). Вместо этого
проверялась исполнимость фактических утверждений ТЗ о текущем поведении
src/wall-thickness.ts на origin/dev, поскольку именно они — единственное
обоснование, почему решение верно и почему AC1/AC2 будут доказывать то, что
заявлено.
Как проверялось
- Прочитаны целиком
docs/SCOPE.md,AGENTS.md,PROCESS.md(действующая редакция, §1, §2.2, §2.4, §5, §7.1, §8) иdocs/WALL-THICKNESS.md. - Прочитано тело issue #197 и все три комментария (аналитика владельца —
ценность 9/9, сложность 6, риск 8,
P2,bug, обычный трек; занятие; хендофф на ревью). - Прочитан весь текст ТЗ (
docs/specs/197-junction-patch-fail-dark.md, 409 строк) и сверены обязательные разделы §7.1 PROCESS.md — присутствуют все: сценарий и персона (§1), что человек увидит до/после (§2), проблема и причина (§3), скоуп/не-скоуп (§5/§6), контракт поведения (§7), UX/i18n/touch (§10), модель данных и миграция (§9), AC1…AC11 с доказательством (§11), план автотестов (§12), риски (§14), откат (§15), release-артефакты (§16). Плюс продуктовые разделы (персона/поверхность/момент; фраза «что увидит» без терминов реализации) — на месте. - Прочитан код
src/wall-thickness.ts(wallBodiesGeometry,virtualJunctionPatches,wallIntervals,openEps) и подтверждено построчно: room-ring loop (:1730-1739) и wall-edge loop (:1744-1756) имеют per-piecetry/catch; junction-patch loop (:1760-1761) — нет, и любое исключение из егоunion()долетает до общегоcatchна:1790, обнуляя весьwallBodiesGeometry(). Совпадает с диагнозом ТЗ дословно. - Прочитан
docs/specs/141-wall-junctions.md(git fetchтело issue #141 черезgh issue viewпоказало не то, что ожидалось — актуальный заголовок issue #141 на GitHub («Стыки перегородок под углом остаются с зубцами») не про virtual-junction fail-dark; проверка ушла на уровень спецификации, которая шире исходного тикета). Раздел 7.7 «Ошибки вычисления» этой спецификации действительно фиксирует общий архитектурный контракт: «Malformed legacy input не должен превращать видимую стену в прозрачность… Boolean failure не разрешается маскировать исчезновением кладки». Ссылка ТЗ #197 на #141 как источник продуктового контракта обоснована — это не догадка, выданная за решение, а корректная опора на уже принятый архитектурный принцип, просто зафиксированный в спеке, а не в теле issue. - Ключевая проверка — самостоятельное исполнение заявленной причинно-следственной
цепочки (см. «Находки», High-1). Собран
test-build/(npx tsc -p tsconfig.test.json && node scripts/fix-test-build.mjs), написан скрипт, воспроизводящий ровно вызов, который ТЗ приводит в §3: «комнаты ×NORM_W, затемwallBodiesGeometry(rooms, walls, openCuts, [], GRID_STEP_N, 5, GRID_PITCH, NORM_W, [])» — с полным fixture из issue (проверено: 8 комнат, 25 walls, 3 open_spans, совпадает с заявленным). Результат разошёлся с заявленным; подробности и повторяемая команда — в находке. - Проверена структурная целостность ссылок ТЗ: #198 и #199 существуют,
открыты, не дубликаты (
gh issue view 198/199), вS1-new, что соответствует «смежные находки, аналитика ещё впереди» — согласуется с текстом ТЗ, который явно выносит их из скоупа. - AC1–AC11 прочитаны на однозначность и способ доказательства: у каждого
указан метод (
unit/smoke/golden/ревью кода/сочетание), формулировки не допускают двух прочтений, кроме зависимости AC1/AC2 от fixture, разобранной в находке High-1.
Находки
High-1 — центральное фактическое утверждение ТЗ (единственный патч →
крах union() → null) не воспроизводится точным сценарием, который сам ТЗ
описывает как «подтверждено исполнением»
Файл: docs/specs/197-junction-patch-fail-dark.md, §3 «Подтверждённая
проблема и причина» (и производные от неё AC1, AC2 в §11).
Что заявлено. §3 ТЗ и аналитический комментарий владельца в issue
утверждают: на origin/dev 19e92e0, с анонимизированным fixture из issue и
вызовом wallBodiesGeometry(rooms, walls, openCuts, [], GRID_STEP_N, 5, GRID_PITCH, NORM_W, []) (комнаты предварительно умножены на NORM_W = 1000),
virtualJunctionPatches() создаёт ровно один patch, а
wallBodiesGeometry(...) возвращает null — то есть дефект «подтверждён
исполнением», а не выведен логически. На этом стоят AC1 («тест красный на
исходном dev, где результат равен null») и AC2 («fixture создаёт ровно один
ожидаемый virtual-junction patch»).
Что получено при независимом повторении. HEAD этой ветки (fe7b28f) не
содержит изменений в src/ относительно 19e92e0
(git diff 19e92e0 fe7b28f --stat -- src/ — пусто), то есть повторение идёт
на том же коде, который тестировал автор. Собран test-build/ тем же
способом, каким его собирает npm test (npx tsc -p tsconfig.test.json && node scripts/fix-test-build.mjs). Взят фикстур дословно из тела issue
#197 (проверено программно: 8 rooms, 25 walls, 3 open_spans — совпадает
с заявленным), выполнено ровно то преобразование и тот вызов, которые
описывает §3 ТЗ:
import { wallBodiesGeometry, virtualJunctionPatches } from './test-build/wall-thickness.js';
import { GRID_STEP_N, GRID_PITCH, NORM_W } from './test-build/space-geometry.js';
const coordScale = 1000;
const rooms = fixture.rooms.map(r => ({ ...r, poly: r.poly.map(([x, y]) => [x * coordScale, y * coordScale]) }));
const walls = fixture.walls.map(w => ({ ...w, a: [w.a[0] * coordScale, w.a[1] * coordScale], b: [w.b[0] * coordScale, w.b[1] * coordScale] }));
const openCuts = fixture.open_spans.map(s => [s.a[0] * coordScale, s.a[1] * coordScale, s.b[0] * coordScale, s.b[1] * coordScale]);
virtualJunctionPatches(rooms, walls, openCuts, GRID_STEP_N, fixture.cell_cm, GRID_PITCH, coordScale);
// -> [] (0 патчей, не 1)
wallBodiesGeometry(rooms, walls, openCuts, [], GRID_STEP_N, fixture.cell_cm, GRID_PITCH, coordScale, []);
// -> { geom, paperGeom, depthUnits, openingIndex } — НЕ null, area(geom) ≈ 99518.06
Результат воспроизводится детерминированно (запускался многократно, всегда
одинаково): ноль virtual-junction patches вместо заявленного одного, и
валидная непустая geometry вместо null. Проверены дополнительные
вариации масштаба (координаты в конфиг-пространстве coordScale = 1, разные
сочетания pitch/gridPitch) — ни один вариант, соответствующий описанным в
ТЗ или в коде конвенциям масштабирования (комментарий src/wall-thickness.ts:115-116
«coordScale = NORM_W with pitch = GRID_STEP_N… config-space edges use
coordScale = 1»), не воспроизводит null.
Расследование, почему patch не строится. У узла [620.833…, 550] (реальный
T между стенами 20 см — тот самый, что описан в issue как место дефекта)
virtualJunctionPatches() требует ⩾2 «касающихся» интервалов с half > 0.
Прямая проверка thicknessCmAt()/lookupWall() на этом fixture показала: сама
стена {"key":"0.754167,0.550000@0.0000","cm":20,"a":[0.6208…,0.55], "b":[0.8875,0.55]} корректно разрешается lookupWall() на весь пролёт, но
thicknessCmAt() в его середине после атомарного разбиения на подынтервалы
(разбиение возникает из-за частичного перекрытия room_6 и room_8 вдоль этого
же y = 0.55) возвращает 0, а не 20/22. Из-за этого у узла остаётся только
один интервал с half > 0 (вертикальная стена), патч не формируется вовсе —
крах union(), который заявлен как причина, физически не может произойти,
потому что до него код не доходит. Это отдельный, самостоятельно
воспроизводимый дефект (заведён как #201, bug/P2/S1-new, со ссылкой на
#197) — сам по себе вне скоупа #197, но именно он «съедает» патч, который ТЗ
считает существующим и падающим.
Почему это блокирует, а не просто интересное наблюдение. AC1 и AC2 — не
частности, они физически формируют regression-тест, который должен «уметь
падать» на незащищённом коде (обязательное условие §12 ТЗ и §2.7 PROCESS.md).
Если фикстур, как описано в §3, не даёт ни patch, ни null, разработчик не
сможет написать red-тест по AC1/AC2 буквально по тексту ТЗ — а раз этот тест не
может упасть на исходном коде, он не доказывает то, ради чего задача
существует (PROCESS.md §2.7: «ревьюер убедился, что тест умеет падать»).
Дальше есть три равно вероятных объяснения, и без ответа на них разработка
рискует чинить не тот код:
- reproduction procedure в ТЗ (деление исходных production-координат на
NORM_Wпри анонимизации → обратное умножение наNORM_Wв тесте) не гарантирует битовое совпадение с исходными числами: деление и умножение на 1000 не обязаны быть точным round-trip для произвольногоdouble, а весь сюжет issue — именно про чувствительностьunion()к разнице в 1 ULP. Тогда нужен другой способ передать fixture в тест (например, без промежуточного деления/анонимизации черезNORM_W, а с координатами, зафиксированными на исходном масштабе); - #201 (тихое обнуление толщины на частичном перекрытии трёх комнат) — самостоятельная причина, из-за которой на ЭТОМ конкретном fixture заявленный патч не возникает; после её исправления сценарий ТЗ может воспроизвестись как заявлено — но тогда порядок работ («что делаем первым») из продуктового становится техническим вопросом, который ТЗ #197 не рассматривает вовсе;
- диагноз §3 ТЗ в целом требует уточнения на заново подтверждённом fixture
(не обязательно неверен по существу — код действительно не изолирует
union()одного patch, это подтверждено чтением кода независимо от fixture, — но опора именно на «дефект воспроизведён исполнением» с этими конкретными числами недостоверна).
Рекомендация. Прежде чем issue уходит в S5-ready, автор обязан:
заново подтвердить null/один-patch на fixture так, чтобы это мог повторить
кто угодно (например, приложив ровно тот код воспроизведения, который
использовался, а не только текстовое описание шагов), либо скорректировать
§3/AC1/AC2 под fixture, который действительно падает на origin/dev.
Технический характер причины (round-trip координат) не делает находку
продуктовым вопросом владельцу — это ровно тот случай, который ревью решает
сам (PROCESS.md §7.1): «Всё, чего пользователь не наблюдает, агенты решают
сами». Продуктовый контракт §4/§7 ТЗ (patch fail не должен гасить кладку) не
оспаривается и, по прочтению кода независимо от fixture, обоснован верно —
речь только о доказательной базе AC1/AC2 на этом конкретном наборе данных.
Что проверено и корректно
- Диагноз механизма отказа (без привязки к конкретному fixture).
Отсутствие per-piece
try/catchвокруг junction-patch loop (src/wall-thickness.ts:1760-1761) в отличие от двух соседних циклов — подтверждено чтением кода дословно, независимо от вопроса, воспроизводит ли именно этот fixture крах. Это реальный, объективно существующий разрыв в изоляции отказа, и предложенное исправление (§7.2 ТЗ: transactional per-patch fallback) адресует именно его. - Продуктовый контракт. Ссылка на
docs/specs/141-wall-junctions.md§7.6/§7.7 как источник уже принятого архитектурного решения («boolean failure не маскирует исчезновение кладки») подтверждена чтением этого документа — не выдумана и не выдана вслепую за факт. - Полнота обязательных разделов ТЗ. Все разделы §7.1 PROCESS.md на месте, однозначны, у каждого AC указан способ доказательства.
- Скоуп/не-скоуп. Границы с #198 (вне-сеточный micro-interval) и #199 (self-check Optimize) проведены чётко и не пересекаются с #197.
- Трек. Обычный трек обоснован верно (сложность 6, риск 8, несколько
поверхностей) —
small/trivialбыли бы нарушением критериев §5/§5.1 PROCESS.md. - Технические предположения (§17). Помечены явно как «assumed, change freely» в соответствии с PROCESS.md §7.1 — никакой техническое решение не выдано за продуктовый факт без пометки.
- Модель данных, i18n, touch, миграция, откат, release-артефакты — корректно отражают «без изменений», согласуется с характером задачи (чистая геометрическая правка без нового UX).
- Открытых продуктовых вопросов к владельцу не осталось — единственный найденный пробел (High-1) технический, а не продуктовый: ни «что видит пользователь», ни «объём видимых изменений» здесь не решаются, значит, по PROCESS.md §7.1 он не эскалируется владельцу, а решается автором и ревью.
Чего не проверял
- Численная стабилизация (§7.1 контракт, §17.1 tolerance). Не проверялось
экспериментально, действительно ли округление patch-координат с точностью
coordScale × 10⁻¹²(или альтернатива) устраняет крахunion()в реальном крашащем сценарии — поскольку у меня на fixture из issue крах не наступил вовсе (High-1), проверить «лечит ли предложенный numeric step именно эту причину» было нечем. Требует повторной проверки после разбора находки High-1. - AC3–AC8, AC10, AC11 на уровне реального прогона — не запускал
npm test/npm run build/browser smoke: на этапе ревью ТЗ продуктового кода нет (только два doc-файла в коммите), прогон гейтов реализации вне скоупа этапа (PROCESS.md §2.4/§8). Формулировки этих AC оценивались только на однозначность текста и адекватность заявленного метода доказательства. - Полный набор regression-фикстур #123/#141/#150/#172 (AC6) — не
прогонялся; полагался на то, что они уже зелёные на
dev(не тронуты этим ТЗ) и что их перечисление в AC6 корректно охватывает риск регрессии. - golden/визуальная сцена (AC9) — не создавалась и не оценивалась визуально: на этапе ТЗ сцены не существует.
- Полный causal chain #201 — заведённый по итогам этого ревью issue
зафиксирован на уровне voспроизведённого факта (
thicknessCmAtвозвращает 0 для корректно объявленного интервала при частичном перекрытии трёх комнат), но не диагностирован до конкретной строки/функции-виновника; это аналитика #201, не эта работа.
Вердикт
High: 1 (центральное фактическое обоснование AC1/AC2 не подтверждено независимым исполнением точно описанного в ТЗ сценария) · Medium: 1 → #201 (тихое обнуление толщины на частичном перекрытии трёх комнат — самостоятельный воспроизводимый дефект, обнаруженный в процессе проверки High-1, заведён отдельным issue со ссылкой на #197, не блокирует #197 по существу) · Low: 0.
Красный. ТЗ возвращается автору для повторной проверки причинно-следственной
цепочки на исполняемом fixture (см. рекомендацию в High-1) и корректировки
§3/AC1/AC2 по результату — либо доказательством, что первоначальный fixture
всё же воспроизводит null иным путём вызова, либо заменой fixture/подхода на
тот, что действительно падает на origin/dev.