Files
houseplan-card/docs/specs/261-white-wedges-root-cause.md
T
2026-08-23 15:41:55 +03:00

25 KiB
Raw Blame History

Issue #261 — белые клинья в T-стыках после ограниченного multi-wall bevel

  • Дата: 2026-08-23
  • Тип: bug · приоритет P1
  • Оценка: пользовательская ценность 8/10 · ценность для разработки 8/10 · сложность/риск 7/10
  • Issue: #261
  • Ветка: issue/261-white-wedges-root-cause
  • Статус ТЗ: реализовано, ожидает код-ревью
  • Реализация после rebase на текущий dev: 0dc6c39; проверочные артефакты и smoke registry: 2e53cb0.

Канонические документы: docs/SCOPE.md, docs/USER-GUIDE.ru.md, docs/ARCHITECTURE.md, docs/WALL-THICKNESS.md, docs/CONFIG-COMPATIBILITY.md, docs/TOUCH-SUPPORT.md и docs/TESTING.md.

Связанные, но не дублирующие задачи: #197, #249, #253 и #258.

1. Сценарий и персона

Администратор дома открывает в v1.67.0-beta.4 обычный сохранённый план с толстыми стенами разной толщины. В нескольких T-образных стыках вместо единого тела кладки видны белые треугольные клинья. Они остаются после reload и видны не только администратору в Plan, но также семье в View, kiosk и Static.

Это нарушает J1 из docs/SCOPE.md: план перестаёт правдиво показывать физические стены дома. Исправление также обслуживает J6: все поверхности должны получать одну каноническую геометрию, а не отдельную визуальную заплатку.

2. Подтверждённое воспроизведение

Один и тот же реальный экспорт владельца выполнен production-проекцией spaceModels() → prepareSpacePhysicalGeometryInputs() → wallBodiesGeometry() на v1.67.0-beta.3, v1.67.0-beta.4 и текущем dev. Вход содержит 8 комнат, 24 записи толщины, 3 виртуальных участка, 15 проёмов и 3 независимых физических тела.

Результат сравнения:

  • wallIntervals beta.3/beta.4 совпадает побайтово;
  • wallEdgeBodies beta.3/beta.4 совпадает побайтово;
  • buildMultiWallNodeMap() beta.4 находит 12 физических degree-3 узлов;
  • paperGeom beta.3 имеет площадь 727303.8201095833, а beta.4/current dev — 727248.4381651384;
  • из beta.4 исчез один реальный треугольник площадью 55.3819444449 render-unit² с вершинами около (887.5, 559.1667), (899.5833, 550) и (899.5833, 559.1667);
  • безопасная внутренняя точка исчезнувшего тела: (895.5, 556.0) в production render coordinates, или (0.8955, 0.5560) в сохранённой нормализованной конфигурации.

Этот узел уже присутствует в обезличенной фикстуре test/fixtures/197-junction-patch.json, поэтому пользовательский экспорт в репозиторий не добавляется. Фикстура #197 содержит 8 комнат, 25 wall entries и 3 virtual cuts; дополнительная ULP-запись не меняет сам проблемный узел.

3. Подтверждённая причина

Первый дефектный SHA — коммит #249 29904df (fix: bound multi-wall junction bevels). Уже на нём masonry теряет 495.579151 render-unit² относительно beta.3, а paper — указанный треугольник 55.381944. Изменения #253 на этом SHA ещё отсутствуют и причиной не являются.

У #249 есть два разных варианта одной bevel-формулы:

  1. multiWallBevelTriangles(map) оставляет физическое пересечение лучей до R = 1.25 × H и возвращает только малый excessive wedge за этим радиусом;
  2. внутренний multiWallBevelTrianglesAt(map, false) начинает cut от исходных offset-точек граней и возвращает весь треугольник до старой mitre-вершины.

bevelMultiWallPaper() применяет второй вариант, затем делает union(centre, beveled). Room centre возвращает только площадь внутри объединения комнат; наружная половина физической стены не возвращается. bevelMultiWallBody() использует тот же полный cut перед локальной реконструкцией, но reconstructed ray strips клипуются к centre, поэтому их внешняя часть также теряется. Алгоритм, предназначенный для удаления длинного mitre-зуба, вырезает допустимое пересечение двух положительных физических лучей.

Общая грань 0.620833,0.545833@1.5706 без толщины из исходной гипотезы не причастна: её half = 0, она не входит в degree-3 карту и находится у (620.833, 545.833), а подтверждённый клин — у (887.5, 550).

4. Что человек увидит до и после

До: у T-стыка положительных стен разной толщины штрихованное тело и бумага теряют треугольный сектор между двумя реальными лучами. Через него виден белый фон/пол, словно в кладке есть отверстие.

После: те же сохранённые комнаты и толщины образуют одно непрерывное тело без белого клина. Старый чрезмерный mitre-зуб по-прежнему ограничивается прямой фаской за пределом 1.25 × H; исправление не возвращает выступ #249 и не округляет стык. Результат одинаков в Plan, View, kiosk, Static и скрытой изометрии, а свет не проходит сквозь восстановленную кладку.

Новых сообщений, настроек и ремонтных действий нет. План исправляется при чтении без Save или Optimize.

5. Scope

Входит

  • устранение полного destructive cut для положительных ray strips в degree-3+ room-wall nodes;
  • сохранение физического пересечения лучей вплоть до действующего R = 1.25 × H внутри и снаружи room-centre union;
  • согласованные roomGeom, final masonry и paperGeom без клина;
  • Full/Plan/View/kiosk, Static, hidden Iso, clean floor/room fill/hover и Glow/sun/light-barrier consumers одной геометрии;
  • regression на существующей обезличенной фикстуре #197;
  • усиление unit, browser smoke, golden semantic gate и mutation gate;
  • обновление канонических документов, пользовательского руководства и двух changelog.

Не входит

  • изменение R = 1.25 × H, MITRE_LIMIT = 4 или прямой формы фаски #249;
  • возвращение старых неограниченных mitre-зубов beta.3;
  • изменение wall-key/stored-endpoint ремонта #258;
  • добавление толщины нулевой общей грани у (620.833, 545.833);
  • изменение interval classification, shared/outer/virtual semantics, толщины стен, openings, partitions, drafts или columns;
  • изменение сохранённых rooms/walls/open spans либо новый Optimize-pass;
  • новый UI, i18n, backend, schema/model/store version или WebSocket API.

6. Геометрический контракт

6.1 Физическая область multi-wall node

Для каждого canonical node степени 3+ используются существующие уникальные положительные rays. Повторы shared interval и сонаправленные дубликаты остаются схлопнутыми; zero-thickness и virtual intervals материала не добавляют.

Локальное физическое тело node определяется union конечных полос всех rays. Из него разрешено удалить только ту часть pairwise mitre-overlap, которая лежит за R = 1.25 × H, где H — максимальная incident half-depth.

Полный треугольник от offset origins до старой mitre-вершины не является допустимым cut: его часть до R принадлежит физическим полосам и должна остаться заполненной. Это правило одинаково по обе стороны room-centre union.

6.2 Masonry и exterior

В bounded mask проблемного node canonical room masonry:

  1. сохраняет complete positive ray strips;
  2. удаляет только excessive area за R;
  3. остаётся в допустимой exterior/paper envelope;
  4. имеет ненулевое заполнение в node centre и area-connected связь с каждым incident ray;
  5. не зависит от rooms/walls order, interval direction и room winding.

Room-centre union не может быть единственным clip для repaired local strips, потому что физическая outer half-wall по контракту растёт наружу. Одновременно local reconstruction не вправе выходить за каноническую ограниченную exterior envelope и возвращать старый фасадный spike.

6.3 Paper

paperGeom обязан покрывать room centre и всю каноническую room masonry, которой нужен фон плана. Bevel paper удаляет только область за R; он не вырезает bounded overlap положительных rays. Внутри node не образуется новый hole/component.

Открытия и independent physical bodies сохраняют нынешний порядок: openings режут masonry после canonical roomGeom, а independent bodies не расширяют room paper.

6.4 Clean floor и physics

roomGeom остаётся единственным pre-opening/pre-independent wall authority. Clean-floor/room-fill/hover subtract именно его; восстановленный wall probe не может стать полом соседней комнаты. Final masonry, hidden Iso и light barriers получают тот же node: визуальная щель не заменяется невидимым occluder и наоборот.

Структурные caches и fingerprints сохраняют нынешнюю зависимость только от геометрии. Theme, hover и HA state tick не перестраивают topology.

6.5 Failure isolation

Каждый node остаётся локальной транзакцией по контракту #197: invalid/nonfinite candidate не удаляет успешную геометрию других nodes и не превращает весь план в null. Обязательные exterior/body/opening failures продолжают fail-dark; исправление не маскирует их как успешный результат.

7. Данные, compatibility, touch, security и performance

  • Config/layout и model_version не меняются; чтение и render не мутируют вход.
  • Обе сохранённые wall-key формы #258 продолжают давать одну геометрию; Optimize не требуется для визуального исправления.
  • Legacy key-only records и нулевые/virtual intervals сохраняют действующие правила.
  • Новых UI, focus, ARIA, клавиатурных, pointer или touch-контрактов нет. View и kiosk получают исправление через общий render path; редакторы остаются desktop-first.
  • Новых данных, HTML, URL, service calls, permissions и security surfaces нет.
  • Node map не строится повторно. Локальный repair остаётся в существующем cached structural pass; запрещён новый полный O(E²) обход на HA state tick. Отдельный performance budget не нужен, но prerelease performance smoke остаётся обязательным.

8. Acceptance criteria и доказательства

AC1. Реальный T-узел сохраняет физический сектор

На test/fixtures/197-junction-patch.json при coordScale = 1000:

  • wallBodiesGeometry() возвращает непустые roomGeom, geom и paperGeom;
  • probe (895.5, 556.0) находится внутри roomGeom, final geom и paperGeom с устойчивым ненулевым покрытием, а не только на границе;
  • probe не находится ни в одном clean-floor результате соседних rooms;
  • локальное тело у (887.5, 550) связано со всеми тремя incident rays;
  • повторный расчёт детерминирован.

Доказательство: unit в test/wall-thickness.test.mjs, использующий существующую fixture #197 и polygon-area/point coverage, а не только общую непустоту или brittle SVG-строку.

AC2. Ограничение #249 не ослаблено

На fixture #249 и table-driven 3/4-ray cases:

  • old excessive wedge остаётся пустым;
  • join не выходит дальше 1.25 × H + epsilon;
  • ordinary two-ray joins сохраняют MITRE_LIMIT = 4 и прежние expected values;
  • равные/неравные толщины, permutations, reversed intervals, room winding и normalized/production scale дают эквивалентный результат.

Доказательство: существующие и усиленные unit test/wall-thickness.test.mjs; прежние expected values #249 не переписываются ради прохождения нового теста.

AC3. #197, #258 и zero-depth regressions остаются закрыты

  • forced failure одного optional junction candidate сохраняет остальные nodes и непустую canonical geometry;
  • affected/canonical wall-key variants #258 дают равные masonry/paper paths;
  • нулевая общая грань (620.833, 541.667..550) не становится физической и не увеличивает degree;
  • render не мутирует rooms, walls, open spans, openings или extra bodies.

Доказательство: unit #197/#258, deep equality и mutation gate. Новый mutant возвращает full-origin bevel cut в paper/body; AC1 обязан на нём краснеть.

AC4. Все browser-поверхности видят один заполненный node

В demo/smoke_junction_patch_resilience.mjs fixture #197:

  • Plan/View/kiosk wall path и paper path содержат normalized probe (0.8955, 0.5560) через реальный SVG isPointInFill() либо эквивалентный geometry probe;
  • Static получает тот же canonical wall path и заполненный probe;
  • hidden Iso source masonry и light-barrier masonry содержат тот же probe;
  • clean-floor/hover не присваивают probe полу;
  • theme и HA state tick сохраняют path/fingerprint и повторно используют cache.

Доказательство: targeted browser smoke. Проверка одной лишь непустоты path недостаточна.

AC5. Golden защищает видимый клин семантически

Сценарии junction-patch-resilience-plan-dark и junction-patch-resilience-view-dark используют существующую полную fixture и получают явный retainedWedgeProbe: [0.8955, 0.5560]. Golden capture до записи PNG проверяет, что wall и paper SVG действительно заполняют probe; пустая, неверно кадрированная или сохранившая клин сцена завершается ошибкой.

Изменившиеся Linux CI PNG и diff просматриваются до принятия baseline. Локальный Windows raster не является каноном.

Доказательство: demo/golden/matrix.mjs, demo/golden/harness.mjs, demo/golden/run.mjs, test/golden-matrix.test.mjs и полный npm run golden:verify.

AC6. Данные и производительность не меняются

  • до/после render входной config побайтово одинаков;
  • schema/backend/i18n отсутствуют в diff;
  • structural cache строит node map один раз; state/theme ticks его используют;
  • node scripts/model-invariants.mjs --config <реальный экспорт> не получает новых нарушений.

Доказательство: unit/smoke, review diff и model-invariants на исходном экспорте владельца.

AC7. Локальные гейты реализации зелёные

  • npx tsc --noEmit;
  • npm test;
  • npm run build с действующей проверкой shipped bundle copies;
  • node scripts/check-docs.mjs;
  • node scripts/smoke-select.mjs --base origin/dev --head HEAD с явным решением по каждой предложенной строке;
  • targeted node demo/smoke_junction_patch_resilience.mjs и node demo/smoke_multiwall_junction.mjs;
  • npm run golden:verify для видимого изменения;
  • node scripts/model-invariants.mjs --config C:\\Temp\\houseplan-space-1-2026-08-23_02-30-42.json.

Полный smoke и performance остаются prerelease gates; Python/backend suite не требуется при отсутствии Python diff. Canonical HA harness остаётся Linux CI.

9. Затронутые файлы и модули

Ожидаемый product diff:

  • src/wall-thickness.ts — canonical multi-wall body/paper repair.

Ожидаемые доказательства:

  • test/wall-thickness.test.mjs;
  • demo/smoke_junction_patch_resilience.mjs;
  • при необходимости demo/smoke_multiwall_junction.mjs;
  • demo/golden/matrix.mjs, demo/golden/harness.mjs, demo/golden/run.mjs, test/golden-matrix.test.mjs;
  • scripts/mutation-gate.mjs.

Документация:

  • docs/WALL-THICKNESS.md, docs/ARCHITECTURE.md, docs/TESTING.md;
  • docs/USER-GUIDE.ru.md и синхронный docs/USER-GUIDE.md;
  • docs/CHANGELOG.md и docs/CHANGELOG.ru.md;
  • docs screenshots/manifest, если обязательный check-docs требует их обновления для текущего src/** fingerprint.

Новых i18n keys нет.

10. Release artifacts

  • User-visible bugfix записывается в оба changelog в том же коммите, что src/**.
  • Каноническая wall/architecture/testing документация обновляется тем же коммитом.
  • Golden baseline меняется только после просмотра полного Linux CI artifact и npm run golden:accept -- --reviewed.
  • Если source fingerprint делает docs screenshots stale, кандидаты снимаются только workflow Docs screenshots и принимаются из его полного artifact; локальная Windows-пересъёмка не коммитится.
  • Перед бетой обязательны полный smoke, golden и performance/Validate exact-SHA по действующему release process.

11. Риски и меры снижения

R1. Возврат фасадного spike #249

Если просто убрать full-origin cut, старый длинный mitre может вернуться.

Митигация: AC2 сохраняет bound 1.25 × H, discarded-wedge probe и перестановочную матрицу #249; меняется область cut, а не лимит.

R2. Ложная кладка снаружи дома

Если разрешить rebuilt strips без exterior envelope, child-room ray может расширить фасад.

Митигация: §6.2 требует clip к канонической bounded paper/exterior envelope, corner-Split и concave-facade тесты остаются неизменными.

R3. Paper, masonry и physics расходятся

Исправление только SVG path скроет белый клин, но оставит ложный clean floor или проход света.

Митигация: AC1/AC4 проверяют roomGeom, final masonry, paper, clean floor, Iso и light barrier одной точкой. Отдельный render-only patch запрещён.

R4. Существующая fixture снова проверит только непустоту

#197 уже содержала проблемный node, но её unit/smoke принимали общую площадь и непустой path, поэтому регрессия прошла.

Митигация: обязательные point-coverage assertions, semantic golden field и mutant полного cut. Тест должен быть продемонстрирован красным на мутанте.

R5. Local boolean repair погасит соседние nodes

Изменение order union/intersection может вернуть failure mode #197.

Митигация: node-by-node transaction сохраняется; forced-failure test и полная fixture проверяются вместе с новым probe.

12. Откат

Откатывается локальная multi-wall body/paper correction вместе с unit, smoke/golden и документацией. Данные не мигрируются, поэтому отдельного data rollback нет. Golden baseline откатывается только вместе с product geometry; частичный откат оставит ложный зелёный визуальный гейт.

13. Принятые технические предположения

  1. Наблюдаемый пользовательский контракт — непрерывная физическая кладка с сохранённым bound #249; точное разбиение функции на helpers не является продуктовым решением.
  2. Existing fixture #197 является достаточной обезличенной regression fixture; полный реальный export используется только локально для cross-version и model-invariants проверок.
  3. Probe (895.5, 556.0) выбран внутри измеренного исчезнувшего треугольника с запасом от его границ; он не является brittle vertex equality.
  4. Paper может отличаться от beta.3 на малую область excessive bevel за R: задача требует физический контракт, а не полное byte/area равенство старой версии.
  5. Нулевая общая грань остаётся отдельным корректным zero-depth interval. Её материализация для маскировки клина была бы исправлением симптома и выходит из scope.