Files
houseplan-card/docs/reviews/SPEC-REVIEW-172-r1.md
2026-08-18 15:05:46 +00:00

26 KiB
Raw Permalink Blame History

SPEC-REVIEW-172-r1

  • Issue: https://github.com/Matysh/houseplan-card/issues/172
  • ТЗ под ревью: docs/specs/172-zero-divider-taper.md (коммит 4582628, ветка issue/172-zero-divider-taper)
  • Роль: ревьюер ТЗ (не автор), этап S4-spec-review
  • Трек: обычный (не small/trivial) — сложность/риск 6/7 из 10, задача задевает более одной поверхности (Plan, View/kiosk/static, hidden Iso, clean-floor/room fills, Glow/sun) и физическую геометрию, потребляемую всем рендером; критерии лёгкого трека (§5 PROCESS.md: одна поверхность, риск ≤3) не выполняются ни по одному пункту — полный трек и файл ТЗ выбраны верно.
  • Цикл: r1/4

Скоуп ревью

Проверялось соответствие ТЗ:

  • docs/SCOPE.md — попадание в Core user jobs (J4/J6), отсутствие расширения скоупа за пределы описанного дефекта;
  • PROCESS.md §2.4/§2.5 (DoR), §7.1 (обязательные разделы ТЗ), §5 (критерии лёгкого трека), §3/§12 (запреты, включая «догадка вместо решения»);
  • AGENTS.md — классы файлов, имя ветки, трейлеры коммита ТЗ;
  • каноническому документу подсистемы docs/WALL-THICKNESS.md (модель толщины, growth ±½, mitre/bevel-контракт, единый источник геометрии для всех потребителей);
  • docs/USER-GUIDE.ru.md — терминология инструмента «Split»;
  • фактическому коду src/wall-thickness.ts (insetContour(), outsetContour(), MITRE_LIMIT) — чтобы диагноз причины в ТЗ не оказался непроверенной догадкой, выданной за факт;
  • полному треду issue #172 — аналитика Codex, вопросы Q1–Q4 с default'ами, решение владельца, финальный комментарий автора со ссылкой на ТЗ.

Как проверялось

  1. Прочитан весь тред issue #172: исходный баг-репорт пользователя (Г-образная комната, Split из внутреннего угла с отклонением 0,5–1° от нормали, один конец разделителя — толщина 0, другой — толщина примыкающей стены), аналитика Codex (воспроизведение на origin/dev a05aa5d при углах 0°, 0,477°, 0,955°, 1,909°, 9,462°, 26,565°; при точном 0° дефекта нет), явные вопросы Q1–Q4 с предложенными default'ами, ответ владельца «принимаю все defaults» и финальная публикация ТЗ.
  2. Сверены обязательные разделы ТЗ (§7.1 PROCESS.md) построчно — таблица ниже.
  3. Прочитан код src/wall-thickness.ts и построчно сверен диагноз §3 ТЗ:
    • insetContour() (:773-832): при collinearJoint(uA, uB) (:808) — специальная ветка, которая кладёт обе точки pa/pb (offset-точка И исходная вершина нулевой грани при коллинеарном стыке) — совпадает с утверждением ТЗ §7.2 «строго коллинеарный переход сохраняет ступень»;
    • при не-коллинеарном стыке (реальный fixture отклонён на доли/единицы градуса) код идёт в mitre/bevel-ветку (:817-829); при удалённом mitre (dist > MITRE_LIMIT × maxO, :821) срабатывает bevel (:827-829): if (oA > 0) out.push(...); if (oB > 0) out.push(...). Если один из offset'ов равен нулю (наш случай: положительная наружная стена ↔ нулевой разделитель), в вывод попадает только одна точка — офсетная точка толстой грани; исходная вершина нулевой грани не добавляется нигде. Это ровно механизм, который ТЗ §3/§4 (комментарий-анализ) описывает как причину клина: «bevel-ветка сохраняет только смещённую точку толстой грани и теряет исходную вершину нулевой грани». Диагноз точен, не является догадкой.
    • outsetContour() (:1912-1967) зеркально воспроизводит ту же структуру (:1962-1963) — подтверждает утверждение ТЗ §8.2 о необходимости симметричного исправления в inset и outset.
    • Геометрически прослежен путь клина: в узле стыка (толстая стена → нулевой разделитель) кольцо после bevel соединяет офсетную точку толстой грани напрямую со следующей вершиной вдоль нулевого разделителя (у которой оба соседних offset = 0, значит она остаётся исходной вершиной), образуя прямую от «почти нулевого» смещения до нуля на другом конце — то самое сечение «растёт от 0 до полной глубины стены», описанное в баг-репорте и §3 ТЗ.
  4. Прочитан docs/WALL-THICKNESS.md целиком: подтверждён контракт growth ±½, union колец по комнатам, единый источник геометрии для full/static/hidden-isometric и light occlusion (раздел 2–4) — ТЗ §7.4/§8.6 продолжает существующую модель, а не изобретает новую. Раздел 8 документа («Independent partitions… same joined set used by Glow, sun and source placement») также согласуется с требованием ТЗ единого физического тела для всех потребителей.
  5. Проверено, что #172 не дублирует #123 (наружный фасад/выход толщины через вершину при Split) и #150 (breakpoint между коллинеарными внешними интервалами разной толщины): прочитан docs/specs/123-corner-split-wall.md — там баг про экстерьерный bbox и наружный зуб от острого митра, здесь — про внутреннюю нулевую границу и bevel, теряющий вершину. Разные механизмы, разные условия срабатывания (там — вершина исходной комнаты, здесь — стык offset>0 / offset=0 в bevel-ветке). Не дубликат.
  6. Проверена терминология: «Split» в ТЗ совпадает с docs/USER-GUIDE.ru.md:340 («Split | Делит комнату путём от одной стены до другой»); термины «masonry», «mitre», «bevel», «cap» — это уже принятая в docs/WALL-THICKNESS.md английская терминология подсистемы, не изобретены автором ТЗ.
  7. Проверен явно фактический фикстур §3: полигон [100,100]–[900,100]–[900,800]–[600,800]–[600,400]–[100,400], Split [600,400]→[900,402.5] даёт atan(2.5/300) ≈ 0,477°, а [900,405] даёт atan(5/300) ≈ 0,955° — числа в ТЗ внутренне согласованы, не выдуманы.
  8. Проверено соответствие docs/CONFIG-COMPATIBILITY.md: задача не создаёт нового compatibility-случая (не меняет persisted-представление RoomCfg/ WallEntry), что подтверждено и содержанием реестра (нет полей, относящихся к разделителям/толщине, требующих отдельной миграции).
  9. Проверен явный технический блок §16 «Принятые технические предположения»: все пять пунктов — про место реализации, эпсилон в тестах, отсутствие отдельной post-render маски, переиспользование fixture #123 и границу с возможным отдельным багом boolean-библиотеки — технические, не продуктовые, корректно не эскалированы владельцу (PROCESS.md §7.1: «владельцу — только продуктовые вопросы»).
  10. Проверена трассируемость: docs/specs/README.md:94 обновлён тем же коммитом 4582628; ссылка issue → ТЗ и ТЗ → issue двусторонняя. git diff --stat origin/dev...HEAD показывает только docs/specs/172-zero-divider-taper.md и docs/specs/README.md (класс C, ни одного файла класса A — правило №1 не нарушено на этапе ТЗ). Коммит несёт Issue: #172, User-Visible: no — верно для документа ТЗ, который сам не меняет поведение продукта.
  11. Проверено существование файлов, которые ТЗ называет предполагаемыми: test/wall-thickness.test.mjs, demo/smoke_split_corner_wall.mjs, demo/smoke_wall_junctions.mjs, demo/smoke_wall_thickness.mjs — все существуют; новый demo/smoke_zero_divider_taper.mjs не пересекается по смыслу с существующим demo/smoke_split_nonsnap.mjs (тот проверяет Split на не-grid-aligned полигоне, а не переход толщины на нулевой границе) — не дублирует существующее покрытие.

Обязательные разделы (§7.1 PROCESS.md)

Раздел Есть Комментарий
Сценарий (персона/поверхность/момент) ✅ §1 — администратор дома, desktop Plan editor, момент завершения Split почти вдоль плеча угла
Что человек увидит до/после ✅ §2, «До:»/«После:» — см. Low-1
Проблема (с подтверждённой причиной) ✅ §3, причина проверена построчно по коду (см. «Как проверялось» п.3)
Скоуп / не-скоуп ✅ §5 / §6, явные границы (без snap, без нового UX, без изменения MITRE_LIMIT для двух положительных толщин, без миграции)
Контракт поведения ✅ §7 (геометрия) + §8 (архитектурные ограничения реализации)
Модель данных и миграция ✅ §9 — явное «форматы не меняются», «читается исправленно, без записи»
UX, i18n, accessibility, touch ✅ §10, явно Touch editor: best effort / intentionally degraded — буквальная канон-метка docs/TOUCH-SUPPORT.md присутствует
AC1…ACn с доказательством ✅ §11, 11 штук, каждый помечен unit/smoke/golden/«ревью кода»
План автотестов ✅ §12, разбит на unit / browser smoke / golden и pre-release / implementation loop
Риски ✅ §13, таблица риск → мера, плюс performance/security
Откат ✅ §14
Release-артефакты ✅ §15, конкретный список: оба changelog, WALL-THICKNESS.md, тесты, golden, три копии бандла

Все обязательные разделы присутствуют и содержательны. Дополнительно есть раздел решений владельца (§4, фиксирует принятые Q1–Q4) и явный блок принятых технических предположений (§16) — соответствует требованию PROCESS.md §7.1 отделять продуктовое решение от технического и не выдавать догадку за факт.

Находки

Находок уровня High и Medium нет — новых issue не требуется.

Low-1 — «что человек увидит» длиннее одной фразы

Файл: docs/specs/172-zero-divider-taper.md:26-34 (§2)

PROCESS.md §7.1 требует «одной фразой, без терминов реализации». Раздел написан двумя-тремя предложениями на «До:»/«После:» (например, «После:» содержит два предложения). По существу требование выполнено — язык исключительно визуальный («клин», «толщина», «стык»), без имён функций или внутренних терминов, — но формально это не «одна фраза». Тот же класс находки уже фиксировался как Low и не блокировал приёмку в SPEC-REVIEW-141-r1 (Low-2) и SPEC-REVIEW-137-r1.

Решение ревьюера: Low, не блокирует. Косметическая правка на усмотрение автора при следующей редакции.

Low-2 — эпсилон/half-depth границы локального cap не формализованы числом

Файл: docs/specs/172-zero-divider-taper.md:124-125, 338-339 (§7.1, §16.2)

Контракт требует, чтобы локальная область примыкания «ограничена физической half-depth примыкающей стены и геометрическим epsilon» и «не может расти пропорционально длине D», но конкретная формула эпсилон (как, например, max(4% × grid pitch, 1e-9) для «effectively collinear» в docs/WALL-THICKNESS.md) не приведена — §16.2 явно оставляет её тестовой стратегии автора кода. В src/wall-thickness.ts уже есть несколько устоявшихся эпсилон-констант (openEps(pitch, coordScale), pitch * coordScale * 0.02, 1e-9), так что это не белое пятно, а осознанно оставленная техническая свобода, корректно помеченная как предположение, которое ревьюер вправе оспорить, но переносить в продуктовый вопрос владельцу нет оснований — граница «не растёт пропорционально длине» уже достаточна как проверяемый инвариант для AC2.

Решение ревьюера: Low, не блокирует. Код-ревью должно убедиться, что выбранная константа действительно не масштабируется с длиной грани (что уже явно требует AC2), а не что она равна конкретному числу.

Что проверено и корректно

  • Соответствие docs/SCOPE.md: задача закрывает J4 («от нуля до плана без Inkscape/YAML» — Split не должен молча создавать кладку, которую пользователь не задавал) и J6 («план остаётся правдивым по мере развития» — сохранённые планы должны отображаться корректно без ручной переработки). Обе строки Closed, это исправление дефекта внутри принятой функциональности, а не расширение продукта. Общая физическая геометрия также поддерживает согласованность J1/J2/J3 между View и Plan, как верно указано в §1 ТЗ.
  • Легитимность полного трека: сложность/риск 6/7 из 10, минимум пять затронутых поверхностей (Plan, View/kiosk/static, hidden Iso, floor/room fills, Glow/sun barriers) — критерии small (§5 PROCESS.md, одна поверхность, риск ≤3) не выполняются; полный трек и отдельный файл ТЗ выбраны верно, лёгкий трек владелец и автор корректно не применили.
  • Продуктовые вопросы закрыты по процессу: Q1–Q4 заданы одним пакетным комментарием, каждый с предлагаемым default, задача корректно ушла в blocked+S3-spec до ответа и вышла из blocked сразу после решения владельца (все четыре ответа — «да»). Ни один технический вопрос не был ошибочно вынесен владельцу — §16 явно и полностью перечисляет технически свободные решения.
  • Технический диагноз не голословен. Причина клина (bevel-ветка insetContour()/outsetContour() теряет исходную вершину нулевой грани при близком, но не точном коллинеарном стыке) построчно проверена по исходному коду src/wall-thickness.ts:773-832, 1912-1967 и совпадает с описанием в ТЗ — см. «Как проверялось» п.3. Числовые фикстуры (углы 0,477°/0,955°) внутренне согласованы с геометрией §3.
  • Не дубликат #123/#150: механизм и условие срабатывания различны, проверено по docs/specs/123-corner-split-wall.md; ТЗ корректно разграничивает три задачи в одном абзаце §3.
  • Не-скоуп (§6) корректно отсекает соседние соблазны: snap почти перпендикулярного/коллинеарного Split, изменение допустимости или диалога Split, автоназначение толщины, переработка модели данных, изменение MITRE_LIMIT для пар двух положительных offset'ов, публикация изометрии как отдельной фичи — все явно исключены с указанием причины.
  • Регрессионные гарантии сформулированы явно: AC3/AC9 поимённо защищают точный коллинеарный/ортогональный переход, пары 1↔15/15↔100, corner Split, wall junctions, wall thickness, opening tunnels — то есть именно те сценарии, которые уже используют ту же insetContour()/outsetContour() и могли бы негласно пострадать от исправления.
  • Модель данных и миграция (§9): корректно заявлено «форматы RoomCfg и WallEntry не меняются», «чтение и рендер не записывают конфигурацию», без прямой/обратной миграции — сверено с docs/CONFIG-COMPATIBILITY.md, задача не создаёт нового compatibility-случая.
  • UX/touch (§10): буквально использует канон-формулировку docs/TOUCH-SUPPORT.md («Touch editor: best effort / intentionally degraded») и отдельно фиксирует safety floor: View/kiosk/static — блокирующие поверхности, сохранённая геометрия не зависит от типа указателя.
  • Release-артефакты (§15) перечисляют оба changelog в одном implementation-коммите, docs/WALL-THICKNESS.md, конкретные unit/smoke/golden-файлы и три синхронные копии бандла — соответствует §7.1/правилу 11 PROCESS.md. Golden корректно ограничен только npm run golden:accept -- --reviewed по полному Linux-артефакту на предрелизном этапе (§12, согласуется с PROCESS.md §8/§11.4).
  • Дисциплина «тест должен уметь падать»: AC1 прямо требует, чтобы фикстура §3 краснела на исходном dev из-за taper-клина, и явно передаёт эту проверку на код-ревью («Ревьюер фиксирует эту проверку в code review») — соответствует требованию PROCESS.md §2.7/§18.
  • Трассируемость: docs/specs/README.md:94 обновлён тем же коммитом 4582628; ветка issue/172-zero-divider-taper и трейлеры (Issue: #172, User-Visible: no) корректны для документа класса C, который сам не меняет поведение. git diff --stat origin/dev...HEAD не содержит ни одного файла класса A — продуктовый код не тронут до S5-ready (правило №1).

Чего не проверял

  • Не проверял реализуемость конкретного алгоритма из §8.2 («сохранить обе точки в детерминированном порядке») как единственно возможного решения — это техническая свобода автора кода (§16), а не предмет ревью ТЗ; код-ревью должно будет проверить сам факт отсутствия taper, а не конкретный порядок вставки точек.
  • Не запускал автотесты, golden, browser-смоки или performance-профили — на этапе spec это не требуется; существование названных в ТЗ файлов (test/wall-thickness.test.mjs, demo/smoke_split_corner_wall.mjs, demo/smoke_wall_junctions.mjs, demo/smoke_wall_thickness.mjs) и структура insetContour()/outsetContour() проверены чтением кода, а не исполнением.
  • Не проверял корректность конкретных числовых оценок аналитики (8/10 · 6/10 · 7/10 · P2) по существу — это поле владельца (PROCESS.md §2.2), уже принято явным решением владельца до написания ТЗ.
  • Не проверял связанные issue #123/#150 по существу за пределами того, что понадобилось для верификации отсутствия дублирования (различие механизма и условий срабатывания) — они не входят в предмет этого ревью.
  • Не проверял, обнаружится ли в ходе реализации отдельный дефект boolean-библиотеки (упомянутый как риск в §16.5) — это явно вынесено в будущий отдельный issue, если случится, и не влияет на приёмку ТЗ сейчас.

Вердикт

Зелёный. High: 0, Medium: 0. Две находки Low (раздел «что человек увидит» длиннее одной фразы; численная граница локального cap оставлена технической свободой без явной формулы) — ни одна не блокирует приёмку, обе либо правятся косметически при следующей редакции, либо снимаются этой записью без нового цикла.