Files
houseplan-card/docs/reviews/SPEC-REVIEW-275-r2.md
T
2026-08-23 22:14:48 +00:00

26 KiB
Raw Blame History

SPEC-REVIEW-275-r2

  • Issue: #275 — «Multi-wall bevel beta.6 вырезает реальные полосы стен: белые уголки, потеря толщины и крупные провалы»
  • Этап: ТЗ на ревью (PROCESS.md §2.4)
  • Артефакт ТЗ: docs/specs/275-multiwall-strip-containment.md, коммит 62dd02d8234a8d5b9b629e2fd77b1c09f00932ab (ветка issue/275-multiwall-strip-containment)
  • Предыдущий заход: r1, docs/reviews/SPEC-REVIEW-275-r1.md, вердикт зелёный, на коммите 762e9f4b323d9d00ae4b230e419b281cd3d82a9a
  • Заход: r2 · блокирующих циклов израсходовано 0 из 4 (зелёный вердикт r1 не образует цикла, PROCESS.md §4/#227)
  • Ревьюер: свежая сессия, без устных пояснений автора

Почему это r2, хотя r1 был зелёным

r1 одобрил формулу requiredStripUnion(node) − roomGeom = ∅ как единый инвариант для всех multi-wall nodes. Сразу после зелёного вердикта автор сам исполнил её буквально на неортогональном fixture #249 и получил контрпример: точка discardedWedgeProbe (330.3808442725, 148.8560107825) — уже утверждённо пустая по #249 — обязана вернуться в masonry по формуле r1, потому что лежит внутри union двух raw incident rectangles. Автор без ожидания вердикта вернул issue в S3-spec, переписал контракт и снова запросил независимое ревью. Это не отменяет r1 (там не было пропущенной ошибки при тогдашнем объёме проверки — формула была корректно сверена с кодом и работала для всех репортнутых случаев), это новое знание, полученное после него.

Дельта контракта — не редакционная, а замена инвариант для всех узлов → инвариант только для rectilinear узлов, неортогональные остаются на контракте #249. Это смена контракта поведения в смысле PROCESS.md §2.10 («разбор остаётся полным, если... смена контракта поведения»), поэтому я провёл полный технический разбор нового раздела 6 и связанных AC, а не только сверку «что изменилось в тексте». Разделы, которых дельта не касается по существу (персона/сценарий §1, compatibility/i18n/security §7 кроме одной переформулированной строки, список файлов §9, release-артефакты §10, rollback §12), не разбирались повторно — см. «Унаследовано из r1».

Дельта (git diff 762e9f4..62dd02d)

Только docs/specs/275-multiwall-strip-containment.md (140 строк: +93/−47) плюс публикация docs/reviews/SPEC-REVIEW-275-r1.md (191 строк, добавление автоматическим шагом, не автором). Изменены: §Статус, §3 (причина — добавлен абзац о контрпримере и rectilinear-классе), §4 (что видит человек), §5 (Входит), §6.1–6.7 (весь геометрический контракт: новый §6.1 классификация, §6.2/6.3 переименованы и сужены до rectilinear, §6.4 переписан как «неортогональный bounded bevel» вместо общей формулы, §6.5–6.7 перенумерованы без изменения смысла), §7 (одна строка переформулирована, смысл не изменён), AC1/AC2/AC3/AC5/AC6/AC7 (сужены до rectilinear/non-rectilinear), §11 (первая строка риска), §13 (пункт 2 переписан). Продуктовый код не тронут: git diff origin/dev...HEAD за весь issue — два файла в docs/, оба документного класса C.

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

  • Перечитан диапазон 762e9f4..62dd02d построчно (см. выше) и весь текущий docs/specs/275-multiwall-strip-containment.md.
  • Перечитан docs/reviews/SPEC-REVIEW-275-r1.md целиком, включая его находки и то, что он уже сверил с кодом.
  • Комментарии issue #275 прочитаны целиком: хендофф r1, зелёный вердикт r1, «Авторский контрпример», хендофф r2.
  • Заново, от первых принципов, проверена геометрия нового §6.1–6.4 против src/wall-thickness.ts:2016–2219 (multiWallBevelCutsAt, bevelMultiWallBody), а не только против текста r1:
    • для точно перпендикулярной пары лучей с полутолщинами hA=hB=H пересечение офсетных линий (hit) лежит в точке (H, H) относительно узла — на границе объединения двух strip-прямоугольников, а не за её пределами. Треугольник (pA, pB, hit), который безусловно вычитается сейчас, — это ровно тот угол, который у настоящего прямоугольного T/X обязан быть заполнен настоящей кладкой, а не «избыточный mitre-spike» в смысле #249. Отсюда и системный характер бага: sqrt(hA²+hB²) = √2·H ≈ 1.414H > R = 1.25H для любых двух равных или сопоставимых толщин — обычный T теряет материал всегда, независимо от конкретных чисел из репорта.
    • для «настоящего» избыточного mitre-spike в смысле #249 (острые/тупые углы, не 90°/0°/180°) расстояние до hit растёт неограниченно по мере уменьшения угла между лучами и не привязано к сумме полутолщин — это структурно другой случай, для которого bounded bevel #249 остаётся необходимым. Формально: multiWallBevelCutsAt пропускает пары с gap ≈ 0 или gap ≈ π (строка if (!(gap > 1e-9) || gap >= Math.PI - 1e-9) continue;), но не пары с gap ≈ π/2, что и создаёт дефект.
    • вывод: разделение «rectilinear — никогда не режем» / «non-rectilinear — режем как раньше» технически обосновано для случаев, которые я могу проверить по формуле, а не является произвольной подгонкой под два репортнутых файла.
  • Проверен реальный fixture test/fixtures/249-multiwall-junction.json: три инцидентных луча из узла [0.329166667, 0.141666667] имеют направления с углами приблизительно 102.3°, 45° и −27.7° (вычислено из координат a/b трёх стен) — ни одна пара не параллельна/перпендикулярна в пределах разумного эпсилон. AC2 корректно утверждает «Fixture #249 классифицируется как non-rectilinear» — это не догадка, а проверяемый факт о существующем фикстуре.
  • Сверено, что формулировка issue #275 («белые треугольные вырезы в обычных T-стыках») уже до ревью характеризует затронутые узлы как ортогональные — новая классификация не придумывает продуктовое требование, а называет техническим термином то, что репортёр (владелец) описал словами.
  • Проверено, что список файлов §9, release-артефакты §10, rollback §12, поверхности §7 (кроме перефразированной строки про кэш) не изменились по существу — сверка с r1 не переделывалась.
  • Гейты typecheck/test/build/check-docs не запускались: диапазон origin/dev...HEAD за весь issue содержит только два файла в docs/reviews/ и docs/specs/ (документный класс C), продуктовый код не менялся ни в r1, ни в r2 — прогон не даёт сигнала о качестве ТЗ, то же основание, что в r1.

Закрытие раунда r1

r1 был зелёным без High/Medium — в строгом смысле §2.10 «находок предыдущего раунда» для закрытия нет. Но раунд объявил контрпример к собственному одобренному инварианту, и это единственное содержательное изменение, которое требует явного прослеживания:

Что было в r1 Чем закрыто в r2 Где это видно
Одобрен единый инвариант requiredStripUnion(node) − roomGeom = ∅ для всех multi-wall nodes (r1, §6.2–6.3) Инвариант сужен до rectilinear nodes; неортогональные явно исключены и сохраняют контракт #249 docs/specs/275-...md §6.1 (классификация), §6.3 (заголовок «Rectilinear containment»), §6.4 («Неортогональный bounded bevel»)
AC2 проверял только «area вне strips и bound R» безусловно AC2 теперь явно требует классификации fixture #249 как non-rectilinear и сохранения его вероятностей/геометрии AC2, строки 279–285
AC6-мутант отключал «безусловное вычитание pairwise cut из ray-strip union» Мутант теперь конкретно отключает rectilinear guard AC6, строки 320–327
Low-1 (канал доказательства AC3) и Low-2 (план тестов не отдельным заголовком), обе снял ревьюер без правки документа Не затронуты дельтой — обе снятые находки остаются в силе без изменений docs/reviews/SPEC-REVIEW-275-r1.md, разделы «Low — 1/2»

Унаследовано из r1

Принято без повторной проверки — не задевается дельтой, проверено на 762e9f4b323d9d00ae4b230e419b281cd3d82a9a в docs/reviews/SPEC-REVIEW-275-r1.md:

  • полнота обязательных разделов §7.1 PROCESS.md кроме изменённых §3/§4/§5/§6/AC1-2-3-5-6-7/§11/§13 (персона/сценарий §1, i18n/compat/security/touch §7, план тестов §8+9, release-артефакты §10, риски кроме первой строки §11, rollback §12);
  • существование инструментов, названных в §8–9: scripts/mutation-gate.mjs, demo/smoke_multiwall_junction.mjs, scripts/smoke-select.mjs, demo/golden/harness.mjs, demo/golden/matrix.mjs — ни один не выдуман;
  • статусы связанных issue (#271/#272/#273 CLOSED, #270 OPEN вне процесса) и точность их упоминания;
  • точность переноса числовых данных из тела issue в раздел 2 ТЗ (координаты, cell_cm, число rooms/walls/nodes/samples) — не менялся дельтой;
  • приватность данных: анонимизированные fixtures в Git, полные backups вне репозитория — тот же контракт, AC7 лишь добавил один негативный кейс;
  • Low-1 и Low-2 из r1 (см. таблицу выше) — сняты ревьюером r1, дельта их не касается, повторно не поднимаю.

Находки

Medium — 1: контракт не определяет узлы, где ray-пары смешивают rectilinear и non-rectilinear

docs/specs/275-multiwall-strip-containment.md, §6.1/§6.3/§6.4. Классификация — свойство узла целиком: rectilinear только если каждая пара лучей параллельна/перпендикулярна (§6.1: «Node является rectilinear, если для каждой пары направлений...»). Если хотя бы одна пара не проходит тест, узел целиком становится non-rectilinear и получает весь старый bounded-bevel #249 — включая пары, которые сами по себе перпендикулярны.

Сценарий отказа: 4-лучевой узел — прямая внутренняя перегородка (север 0°, восток 90°) плюс диагональная стена под 45° (эркер, лестничный марш, скошенный угол). Пары (север, восток) = 90° (rectilinear-пара), (север, диагональ) = 45°, (восток, диагональ) = 45° — обе non-rectilinear. По §6.1 весь узел классифицируется non-rectilinear → пара (север, восток) получает старый pairwise cut → для равных полутолщин sqrt(hA²+hB²) ≈ 1.414H > R = 1.25H → ровно тот же белый треугольник/потеря толщины, из-за которого заведён #275, воспроизводится на этом узле, хотя весь остальной фикс формально «работает».

Это не гипотетическая придирка к формуле: docs/WALL-THICKNESS.md явно допускает узлы «с тремя или более различными инцидентными лучами» без ограничения на то, что все они должны быть либо все ортогональны, либо все под общим углом — контракт файла не запрещает смешанные узлы, а значит их не запрещает и продукт. AC1 покрывает только чисто rectilinear T/X, AC2/AC3 — только целиком non-rectilinear fixture #249 и репортнутые узлы (которые issue называет «обычными T-стыками», то есть предположительно чисто rectilinear). Ни один AC не называет смешанный узел, а §5 «Не входит» не исключает его явно.

Почему в скоупе, а не отдельный issue: это тот же контракт §6, который сейчас переписывается — расширить классификацию до попарной (exempt только rectilinear-пары, а не весь узел) или явно исключить смешанные узлы с обоснованием (например, «не встречаются в репортнутых backups, доказано по факту N узлов и их углам») — техническое решение автора, а не соседняя подсистема.

Что нужно: либо (a) сузить проверку §6.3 до уровня пары, а не узла — «pairwise cut не строится для пары, которая сама parallel/perpendicular, независимо от классификации остальных пар того же узла», с соответствующей правкой AC1/AC6/AC7, либо (b) явно исключить смешанные узлы в §5/§13 с доказательством по фактическим backups (AC3 может это подтвердить: «ни один из 12 репортнутых узлов не смешанный» — при условии, что это действительно так, и добавлением негативного теста на смешанный синтетический fixture, документирующего оставшееся поведение как известное ограничение).

Без High-находок это делает вердикт жёлтым: находка в скоупе задачи, чинится в текущем issue (PROCESS.md §2.4/§4), отдельный issue не заводится.

Low — 1 (снимаю без правки документа): величина angle-epsilon в §6.1 не названа числом

§6.1 вводит «единый angle-epsilon» для теста |dot| ≈ 1/|dot| ≈ 0, не называя конкретное значение или формулу (в отличие от именованных констант кода — MITRE_LIMIT = 4, MULTI_WALL_JOIN_LIMIT = 1.25, или уже существующего допуска openings «max(4% × grid pitch, 1e-9)» в docs/WALL-THICKNESS.md). Технически это решение реализации (§13.5 ТЗ прямо оставляет технические детали за исполнителем), и AC7 прямо требует «негативную angle-boundary matrix», которая обязана уронить тест при слишком большом эпсилон — то есть непроверяемости здесь нет, только отложенное числовое решение. Снимаю без правки текста: то же основание, на котором r1 принял оставление алгоритма бевела как «эквивалентная реализация допустима».

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

  • Новая rectilinear-классификация (§6.1) технически обоснована не только текстом ТЗ, но и независимым пересчётом геометрии для перпендикулярного случая (точка hit лежит на границе union strip-прямоугольников, а не за её пределами) и для острого/тупого случая (расстояние до hit растёт неограниченно с уменьшением угла и структурно отличается от rectilinear случая) — это не подгонка под два репортнутых файла, а разделение двух разных геометрических режимов существующей формулы.
  • AC2 корректно и проверяемо утверждает, что fixture #249 — non-rectilinear: три инцидентных луча этого фикстура образуют углы, ни один из которых не близок к 0/90/180° (вычислено из фактических координат стен в test/fixtures/249-multiwall-junction.json), это не догадка.
  • §6.7 закрывает конкретную дыру, которую наивная реализация могла бы оставить: failure-fallback не может тихо вернуть rectilinear-узел на старый pairwise-cut путь — если бы этой строки не было, unit-тест на failure isolation не имел бы, что проверять именно для этого случая.
  • §6.3 явно запрещает частичное применение классификации внутри уже rectilinear узла («недостаточно пропустить только одну 90° пару и оставить соседний cut того же T/X») — устраняет один класс наивной реализации, отличный от Medium-находки выше (та — про узлы, которые НЕ прошли классификацию целиком, эта — про узлы, которые прошли).
  • Продуктовых вопросов владельцу нет и не должно быть: единственный продуктовый факт (сохранённая стена не может быть вырезана renderer-bevel) зафиксирован владельцем в теле issue, различение rectilinear/non-rectilinear — техническое уточнение уже одобренного продуктового требования, а не новый вопрос ему.
  • Дельта не расширяет и не сужает scope искусственно: «Не входит» (§5) не изменился по смыслу, никакая соседняя подсистема (i18n, backend, миграция, UI) не затронута новым текстом.
  • Числа/координаты, перенесённые из issue в разделы 2–3, не искажены и не изменены дельтой r1→r2 (сверено построчно).

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

  • Не запускал npm run typecheck/npm test/npm run build/node scripts/check-docs.mjs — весь диапазон origin/dev...HEAD за issue #275 (r1 и r2) состоит из двух файлов docs/**, продуктовый код не менялся; прогон этих гейтов не даёт сигнала о качестве ТЗ. То же основание, что в r1.
  • Не запрашивал и не пытался воспроизвести приватные 1.json/2.json — они не коммитятся по условию задачи; принял утверждение «все 12 измеренных потерь лежат в rectilinear T/X nodes» на тех же основаниях, на которых процесс уже принимал числовые данные владельца в #272/#275-r1 («Чего не проверял», SPEC-REVIEW-275-r1). Если это утверждение неточно (среди 12 узлов есть смешанные), Medium-находка выше становится не гипотетической, а актуальной для самих репортнутых файлов — но это не меняет того, что находка должна быть закрыта независимо от того, встречается она в этих двух конкретных файлах или нет.
  • Не оценивал сложность или производительность конкретного алгоритма классификации, который напишет исполнитель — §13.5 прямо и правильно оставляет это реализации, это её законное место.
  • Не проверял повторно docs/ARCHITECTURE.md/docs/USER-GUIDE.ru.md/docs/TESTING.md — не тронуты дельтой, r1 уже подтвердил, что #275 не меняет их текущий контракт.
  • Не проверял docs/reviews/SPEC-REVIEW-275-r1.md как предмет ревью (это не артефакт этого раунда, а отчёт предыдущего) — только использовал его как источник для «Унаследовано» и «Закрытие r1».

Вердикт

Жёлтый. High-находок нет. Одна Medium-находка в скоупе задачи (контракт §6 не определяет поведение для multi-wall узлов, у которых часть ray-пар rectilinear, а часть — нет, и молча возвращает такие узлы на старый баг-путь для их ортогональных пар) — чинится в текущем issue без отдельного issue (PROCESS.md §2.4/§4/#202). Одна Low-находка снята решением ревьюера без правки документа (эпсилон — техническая деталь, защищённая требуемым AC7 негативным тестом). Новая rectilinear/non-rectilinear классификация в остальном технически обоснована и проверена как против кода (src/wall-thickness.ts), так и от первых принципов геометрии, а не является повторением прежней догадки под новым именем.