From fe7b28f3a715bfb1985cd6eee285fb7c2a3bf10e Mon Sep 17 00:00:00 2001 From: Sergey Matyunin Date: Wed, 19 Aug 2026 12:15:55 +0300 Subject: [PATCH] docs: specify junction patch resilience Issue: #197 User-Visible: no --- docs/specs/197-junction-patch-fail-dark.md | 409 +++++++++++++++++++++ docs/specs/README.md | 1 + 2 files changed, 410 insertions(+) create mode 100644 docs/specs/197-junction-patch-fail-dark.md diff --git a/docs/specs/197-junction-patch-fail-dark.md b/docs/specs/197-junction-patch-fail-dark.md new file mode 100644 index 00000000..4a40b484 --- /dev/null +++ b/docs/specs/197-junction-patch-fail-dark.md @@ -0,0 +1,409 @@ +# Issue #197 — один junction-патч не гасит кладку всего плана + +- **Issue:** https://github.com/Matysh/houseplan-card/issues/197 +- **Редакция:** первая редакция для независимого ревью; статус определяется только метками issue +- **Тип / приоритет:** bug / P2 +- **Оценка:** пользовательская ценность 9/10; ценность для разработки 9/10; + сложность 6/10; риск 8/10 +- **Область:** каноническая геометрия стен, Plan, View/киоск, static card, + hidden Iso, clean floor/paper, Glow и солнце +- **Модель данных:** без изменений и миграции +- **Связано:** #141, #150, #198, #199, `docs/WALL-THICKNESS.md`, + `docs/ARCHITECTURE.md`, `docs/TOUCH-SUPPORT.md` + +## 1. Сценарий и персона + +**Персона:** домашний администратор, который уже построил подробный план и +периодически обслуживает его через «Общие настройки → Оптимизировать планы». + +**Поверхность и момент:** после оптимизации или обычного открытия ранее +сохранённого много-комнатного плана человек переходит в View либо Plan. В одном +виртуальном T-стыке сходятся реальные стены соседних комнат с ненулевой +толщиной. + +Задача поддерживает J1, J4 и J6 из `docs/SCOPE.md`: план должен правдиво +показывать пространственную структуру, штатно обслуживаться через GUI и не +терять физическую геометрию от одного локального численного отказа. View, +киоск и static card — блокирующие поверхности; Plan editor остаётся +desktop-first, но не может создавать или показывать иную физику. + +## 2. Что человек увидит до и после + +**До:** из-за одного стыка во всём пространстве исчезает штрихованная кладка; +между отступившими заливками комнат остаются белые полосы полной или половинной +толщины, как будто все стены одновременно стали прозрачными. + +**После:** весь план продолжает показывать стены. Проблемный T-стык строится +обычно; даже если его дополнительный соединительный фрагмент численно +необрабатываем, отказ ограничивается этим фрагментом и не удаляет остальную +кладку, бумагу и физические препятствия. + +## 3. Подтверждённая проблема и причина + +Дефект воспроизведён на `origin/dev` `19e92e0` с анонимизированным fixture из +issue и production-параметрами: + +- 8 комнат, 25 wall-записей, 3 `open_spans`; +- комнаты переведены в render coordinates через `NORM_W = 1000`; +- `pitch = GRID_STEP_N`, `cell_cm = 5`, `gridPitch = GRID_PITCH`, + `coordScale = 1000`; +- `wallIntervals()` успешно разрешает значения `15/20/22/28/29/33 см`; +- `wallBodiesGeometry(...)` возвращает `null`. + +Причинная цепочка: + +1. `virtualJunctionPatches()` создаёт один прямоугольный patch около + `[620.8333…, 550]`. +2. Две математически совпадающие координаты приходят разными IEEE-754 числами: + `612.5` и `612.5000000000001`; аналогичный шум есть по Y. +3. `polyclip-ts` не может завершить output ring при `union(body, patch)` и + выбрасывает исключение. +4. Room rings и atomic wall-edge bodies имеют per-piece `try/catch`, а цикл + junction patches в `src/wall-thickness.ts:1760–1761` — нет. +5. Исключение достигает общего `catch`, `wallBodiesGeometry()` возвращает + `null`, и все потребители закономерно включают общий fail-dark. + +Дополнительные исполняемые проверки отделяют причину от корреляций: + +- удаление 15-см микро-интервала оставляет результат `null`; +- слияние микро-интервала с соседним участком 22 см оставляет результат `null`; +- удаление open cuts, которое отключает virtual-junction pass, даёт валидную + geometry; +- округление только координат patch с технической точностью от `10⁻⁹` до + `10⁻¹²` render unit даёт валидную geometry; итоговая площадь после canonical + clipping совпадает с вариантом, где единственный отказавший patch локально + пропущен. + +Это не дефект #150: atomic thickness profile и exterior transition исправны. +Короткий вне-сеточный интервал — отдельная находка #198. Общая проверка результата +Optimize перед записью — отдельный защитный барьер #199. + +## 4. Зафиксированное продуктовое решение + +Открытых продуктовых вопросов нет. Действующий контракт уже задан +`docs/WALL-THICKNESS.md` и ТЗ #141: + +1. локальный independent/virtual junction не может удалить валидную кладку + остальных комнат; +2. канонический результат един для рисунка, пола и световых препятствий; +3. при невозможности обработать дополнительный patch допустима локальная + консервативная деградация без записи config; +4. основной structural boolean failure по-прежнему отличим от успешной пустой + geometry и остаётся fail-dark; #197 не превращает любой сбой в оптимистичный + raw-ring fallback. + +## 5. Скоуп + +В задачу входят: + +1. численная стабилизация координат, вычисленных для virtual junction patches, + перед передачей в boolean engine; +2. изоляция `union` каждого patch: один отказ не откатывает ранее построенные + room rings, edge bodies и успешные patches; +3. сохранение валидной основной geometry, `paperGeom`, `depthUnits` и + `openingIndex`, когда отказал только дополнительный patch; +4. одинаковый исправленный structural result для Plan, View/киоска, + `houseplan-space-card`, hidden Iso/floor footprint, clean floor/area, Glow, + source guard и солнца; +5. неизменность opening cuts, exterior shell, независимых partitions/columns и + порядка, в котором они входят в canonical body; +6. существующие сохранённые планы без миграции и фоновой записи; +7. unit regression на полном fixture, матрица численных вариантов, targeted + production-bundle browser smoke, visual regression и release-документация. + +## 6. Не входит в задачу + +- удаление, слияние или изменение минимальной длины wall interval — #198; +- geometry self-check в Optimize preview/apply/undo — #199; +- изменение алгоритма `normalizeWallIntervals()`, `degradeWalls()` или + `alignAllToGrid()`; +- общая замена `polyclip-ts` или настройка его глобальной точности; +- новый persisted junction/node, schema version или migration; +- привязка offset/mitre-вершин к пользовательской сетке; +- изменение толщины, `MITRE_LIMIT`, типов стыка или правил #141; +- исправление произвольного основного exterior/ring/opening boolean failure; +- новые controls, предупреждения, диагностический toast или настройка fallback; +- публикация скрытой изометрии. + +## 7. Контракт поведения + +### 7.1. Численная стабилизация patch + +1. Координаты patch должны быть конечными; patch с не-конечным значением либо + площадью не больше действующего geometry tolerance не передаётся в union. +2. Математически совпадающие результаты операций с общей вершиной и offsets + приводятся к одной детерминированной координате с технической точностью, + многократно меньшей общего geometry epsilon. +3. Стабилизация не использует шаг пользовательской сетки: half-depth 15–33 см и + диагональные mitre вправе находиться между grid nodes. +4. Сдвиг любой вершины от исходного конечного значения не превышает выбранный + numeric tolerance и не меняет видимую толщину, bounded-mitre envelope или + association с исходным узлом. +5. Результат не зависит от представления `x` против `x ± ulp`, направления + segment и порядка эквивалентных wall records. + +### 7.2. Изоляция локального отказа + +1. Каждый junction patch объединяется независимо, как room-ring и edge-body + pieces в соседних canonical loops. +2. Перед попыткой сохраняется последняя успешная `body`. Если union patch + выбрасывает исключение, эта `body` остаётся результатом следующего шага. +3. Ошибка одного patch не пропускает последующие patches, exterior clipping, + shell union, opening cuts и independent extra-body union. +4. Если до patches существует валидная непустая structural body, функция не + возвращает `null` только из-за patch. `paperGeom` также не теряется. +5. Если основная exterior/ring/opening geometry не может быть построена либо + финальный обязательный pass падает, сохраняется действующий общий `null` и + fail-dark; raw per-room rings не воскрешаются. +6. Fallback ничего не записывает, не меняет входные массивы и не создаёт + различающуюся физику между render consumers. + +### 7.3. Канонические потребители + +Один возвращённый `wallBodiesGeometry()`/structural cache определяет: + +- masonry path полного Plan/View и киоска; +- masonry path static card; +- hidden Iso footprint и wall faces; +- clean-floor subtraction, room fill и displayed area; +- paper footprint; +- Glow barriers, source-inside-body guard и spill; +- солнечные препятствия. + +Ни один consumer не получает отдельный SVG-only обход либо собственную +нормализацию patch. Presentation-различия вроде hatch suppression при малой +экранной толщине остаются допустимыми; физическое множество совпадает. + +### 7.4. Совместимость существующей геометрии + +- Исправный virtual T продолжает получать прежний bounded patch. +- Room L/T/nested joins, corner Split #123, thickness transition #150 и + zero-divider #172 не меняют внешний outline. +- Openings по-прежнему режут room masonry после junction pass; совпавший + independent body не режется room opening. +- Partitions, drafts и columns не увеличивают Stage floor footprint. +- Перестановка комнат/стен может менять внутренний порядок boolean operations, + но не геометрическое множество и не способность функции завершиться. + +## 8. Архитектурный контракт реализации + +1. Исправление живёт в общей geometry-логике `src/wall-thickness.ts`, рядом с + построением/union virtual junction patches, а не в отдельном renderer. +2. Numeric normalization является чистым локальным helper либо эквивалентной + операцией и применяется только к вычисляемым patch vertices. +3. Tolerance масштабируется согласованно с `coordScale`, чтобы normalized и + render-space вызовы описывали одну физическую форму; жёсткое округление до + сантиметра, grid step или фиксированного числа видимых знаков запрещено. +4. Per-patch fallback сохраняет `body` транзакционно: присваивание происходит + только после успешного `union`. +5. Structural cache key не меняется: стабилизация является детерминированной + функцией уже входящих в fingerprint координат, cuts и толщин. +6. Логирование, если добавляется, не содержит config/названий комнат и не + спамит каждый HA state tick. Новый публичный diagnostic API не требуется. + +Предполагаемые файлы: + +- `src/wall-thickness.ts`; +- `test/wall-thickness.test.mjs`; +- `demo/smoke_junction_patch_resilience.mjs` либо узкое расширение существующего + wall-thickness smoke с однозначной связью с #197; +- visual fixture в golden matrix, если текущая wall-junction сцена не доказывает + сохранение полного много-комнатного плана; +- `docs/WALL-THICKNESS.md`, `docs/ARCHITECTURE.md`, + `docs/USER-GUIDE.ru.md`, `docs/TESTING.md`, `docs/STATUS.md`; +- `docs/CHANGELOG.md`, `docs/CHANGELOG.ru.md`; +- три синхронные поставляемые копии bundle. + +## 9. Модель данных, compatibility и миграция + +Форматы `RoomCfg`, `WallEntry`, `open_spans`, openings, partitions и columns не +меняются. Новых ключей, aliases и version markers нет. + +- старые и уже оптимизированные планы исправляются вычисляемо при чтении; +- render, preview и cache warm-up не выполняют config/layout/storage write; +- Optimize не запускается автоматически и его отчёт не меняется; +- import/export и backend validation не меняются; +- откат возвращает прежнее поведение без преобразования данных; +- future/unknown fields не затрагиваются. + +## 10. UX, i18n, accessibility и touch + +Новых controls, текстов, focus/keyboard semantics, ARIA, animation и locale keys +нет. RU/EN i18n не меняется. + +Plan editor остаётся desktop-first. Touch editor — best effort / intentionally +degraded, но один сохранённый plan не может получить другую masonry geometry из-за +pointer type. View, kiosk и static card полностью поддерживаются и обязаны +оставаться читаемыми. + +Светлая/тёмная тема, forced colours, `prefers-reduced-motion`, hatch color и wall +opacity не меняют fallback. Цвет не используется как единственное доказательство: +unit проверяет геометрическое множество, browser smoke — реально отрисованный +path на двух темах. + +## 11. Критерии приёмки + +- **AC1 (`unit`):** полный анонимизированный fixture из issue при production + scale возвращает ненулевой объект, непустые `geom` и `paperGeom`; тест красный + на исходном `dev`, где результат равен `null`. +- **AC2 (`unit`):** fixture создаёт ровно один ожидаемый virtual-junction patch; + варианты координат `x`, `x ± ulp` и стабилизированный эквивалент дают одно + geometric set в пределах numeric tolerance и не меняют bounded envelope. +- **AC3 (`unit`):** принудительный throw на первом из нескольких patch unions + сохраняет последнюю успешную body, позволяет обработать следующий patch и не + меняет `paperGeom`; основной обязательный boolean throw по-прежнему даёт + `null`. +- **AC4 (`unit`):** удаление и слияние микро-интервала не используются как + лечение: regression проходит с исходными 25 wall records; входные `rooms`, + `walls`, `openCuts`, `openings` и `extraBodies` после вызова побайтно + эквивалентны исходным. +- **AC5 (`unit`):** перестановка room/wall records, обратное направление + эквивалентных segments и повторный вызов дают одно множество без исключения; + geometry сравнивается symmetric difference/area, а не строковым порядком rings. +- **AC6 (`unit`):** существующие матрицы #123/#141/#150/#172, nested/courtyard, + openings, open spans и independent extras остаются зелёными; валидные обычные + junction fixtures геометрически не меняются за пределами numeric tolerance. +- **AC7 (`smoke`):** targeted production-bundle smoke загружает полный fixture и + подтверждает непустой masonry path в Plan, View, kiosk/static и hidden Iso, + одинаковый structural fingerprint и отсутствие write после render. +- **AC8 (`smoke`):** тот же smoke проверяет clean floor/paper и Glow/source/sun + consumers: кладка не исчезает при переключении HA state, светлой/тёмной темы и + режима отображения; topology не пересчитывается от обычного state tick. +- **AC9 (`golden`):** детерминированная сцена полного плана показывает кладку и + T-стык в Plan и View минимум в dark theme. Baseline принимается только из + просмотренного полного Linux artifact через `golden:accept -- --reviewed`. +- **AC10 (`ревью кода`):** отсутствуют renderer-specific fallback, grid snapping + физических offsets, schema/backend/i18n изменения и исправления #198/#199; + локальный catch не скрывает основной structural failure. +- **AC11 (`typecheck` + `unit` + `build` + `smoke`):** локальный gate зелёный, + целевой smoke выполнен до S7, три bundle-копии после build побайтно одинаковы. + +## 12. План автотестов и гейтов + +### Unit + +В `test/wall-thickness.test.mjs` либо отдельном узком pure-geometry файле: + +1. добавить полный issue fixture без ручного сокращения и вызвать production + signature `wallBodiesGeometry()`; +2. проверить patch count/finite vertices/bounds и numeric-equivalent variants; +3. внедрить контролируемый отказ одного patch union через узкий pure helper либо + другую тестируемую границу, не подменяя production algorithm; +4. проверить продолжение после отказа и отдельно общий structural fail-dark; +5. проверить input immutability, repeatability и permutation invariance; +6. прогнать существующие wall junction/thickness/opening/floor/light regressions. + +Минимум один новый тест обязан уметь падать: возврат старого незащищённого цикла +должен снова дать `null` на полном fixture. Ревьюер проверяет эту мутацию или +эквивалентное доказательство. + +### Targeted browser smoke + +`node demo/smoke_junction_patch_resilience.mjs` либо эквивалентный явно названный +scenario: + +1. загружает поставляемый production bundle и fixture issue; +2. снимает signatures masonry/paper/floor в Plan, View, static и hidden Iso; +3. проверяет dark/light theme и HA light-state update без исчезновения paths; +4. проверяет Glow/source guard и доступный sun-occluder contract; +5. подтверждает отсутствие config/layout write. + +Этот targeted smoke выполняется локально перед `S7-code-review`. Полный набор +smoke не запускается в implementation loop. + +### Golden и pre-release + +Golden-сцена может расширить существующий wall-junction scenario, если полный +fixture и глобальное сохранение кладки читаются однозначно. Локальное принятие +baseline ради зелёной ветки запрещено. Full smoke, golden capture/verify, +performance и Linux Validate выполняются перед бетой на точном SHA по +`PROCESS.md`. + +### Обязательный implementation loop + +```text +npm run typecheck +npm test +npm run build +сравнение трёх bundle-копий +node demo/smoke_junction_patch_resilience.mjs +``` + +Python/HA harness не нужен для локального gate: backend не меняется; полный +Linux harness остаётся release gate. + +## 13. Производительность и безопасность + +Numeric normalization и один локальный `try/catch` не меняют асимптотику. +Количество patches, boolean passes и structural cache invalidation остаются +прежними. HA states, theme, hover и animation tick не входят в geometry key. + +Отдельный budget не добавляется. Перед бетой существующие large-house и Full +Performance должны пройти без ослабления порогов; точный fixture #197 добавляется +в performance только если измерение покажет отдельный значимый профиль. + +Security/privacy влияние отсутствует: нет HTML/CSS ввода, сетевых запросов, HA +services, новых permissions или пользовательских данных. Полный fixture уже +анонимизирован; diagnostic output не должен печатать исходный config. + +## 14. Риски и снижение + +| Риск | Вероятность / ущерб | Снижение | +|---|---|---| +| Слишком грубая нормализация изменит толщину или mitre | средняя / высокий | tolerance значительно меньше geometry epsilon; exact bounds и symmetric-difference tests | +| Grid rounding сломает диагонали и half-depth | средняя / высокий | явный запрет grid snapping; matrix разных `coordScale` и толщин | +| Catch скроет основной отказ и вернёт опасную geometry | средняя / высокий | catch только вокруг одного optional patch; отдельный core-failure test | +| Plan исправится, а light/Iso продолжат использовать другой body | низкая / высокий | один canonical result и consumer smoke AC7/AC8 | +| Перестановка records снова вызовет polyclip failure | средняя / высокий | permutation + ulp matrix на полном fixture | +| Golden примет массовый anti-aliasing diff | низкая / средний | узкая сцена, просмотр полного Linux artifact, без локального auto-accept | +| Попутно изменится optimizer | низкая / высокий | явный non-scope и отдельные #198/#199 | + +## 15. Откат + +Откат — revert одного user-visible implementation commit вместе с тестами, +документацией, changelog и bundle-копиями. Persisted schema и данные не меняются, +поэтому migration/cleanup не нужны. После отката возвращается риск полного +исчезновения кладки на исходном fixture. + +Feature flag не добавляется: это восстановление обязательного fail-closed +контракта общей физической геометрии, а не экспериментальная функция. + +## 16. Release-артефакты + +Изменение пользовательское. Implementation-коммит имеет `User-Visible: yes` и +в том же коммите обновляет: + +- `docs/CHANGELOG.md` и `docs/CHANGELOG.ru.md` со ссылкой на #197; +- `docs/WALL-THICKNESS.md` — per-piece junction failure и numeric normalization; +- `docs/ARCHITECTURE.md` — граница optional patch против core structural failure; +- `docs/USER-GUIDE.ru.md` — диагностика исчезнувшей кладки после Optimize; +- `docs/TESTING.md` — unit/smoke/golden coverage #197; +- `docs/STATUS.md` — фактическая реализованная release-линия; +- unit fixture, targeted smoke и golden scenario/candidate; +- `dist/houseplan-card.js`, + `custom_components/houseplan/frontend/houseplan-card.js`, + `demo/srv/assets/houseplan-card.js` после build. + +Screenshots вне golden не требуются. Backend, migration, i18n, отдельный security +report и отдельный performance budget не требуются. Issue закрывается только +после включения в опубликованную бету. + +## 17. Принятые технические предположения — можно менять без продуктового ревью + +1. Рекомендуемый numeric step — величина порядка `coordScale × 10⁻¹²` либо + эквивалентная relative/ULP-нормализация, доказанная AC2; точное имя helper и + коэффициент не являются продуктовым контрактом. +2. Нормализуются только junction patch vertices непосредственно перед boolean + boundary; persisted rooms/walls/cuts и основные room profiles не округляются. +3. Patch с не-конечными координатами или ничтожной площадью считается локально + непригодным и проходит тот же isolated fallback. +4. Если нормализованный patch всё равно вызывает исключение, он пропускается; + последующие patches и обязательные passes продолжаются. +5. Для текущего полного fixture пропуск и успешный стабилизированный union после + final exterior clipping дают одинаковую площадь; тест всё равно проверяет + успешную нормализацию, чтобы catch не был единственным лечением. +6. Инъекция отказа в unit может быть реализована через небольшой pure helper, + test-only boolean adapter или эквивалент без публичного runtime API. +7. Имена smoke/golden scenarios и раскладка test fixture свободны, если связь с + AC и возможность красного прогона сохраняются. +8. Нет открытых продуктовых вопросов; смежные решения вынесены в #198 и #199. diff --git a/docs/specs/README.md b/docs/specs/README.md index 365af267..b083da62 100644 --- a/docs/specs/README.md +++ b/docs/specs/README.md @@ -99,6 +99,7 @@ GitHub Issues и GitHub Projects (v2) остаются единственным | [#173](https://github.com/Matysh/houseplan-card/issues/173) Единый инструмент рисования стен и предложение комнаты по замыканию | [173-unified-wall-tool.md](173-unified-wall-tool.md) | | [#174](https://github.com/Matysh/houseplan-card/issues/174) Связанный виртуальный источник следует реальному контроллеру | [174-linked-virtual-light-controller.md](174-linked-virtual-light-controller.md) | | [#178](https://github.com/Matysh/houseplan-card/issues/178) Выбор сущности для действия «Переключить состояние» | [178-toggle-entity.md](178-toggle-entity.md) | +| [#197](https://github.com/Matysh/houseplan-card/issues/197) Один junction-патч не гасит кладку всего плана | [197-junction-patch-fail-dark.md](197-junction-patch-fail-dark.md) | ## P3