diff --git a/docs/specs/218-glow-floor-geometry.md b/docs/specs/218-glow-floor-geometry.md new file mode 100644 index 00000000..58c9410c --- /dev/null +++ b/docs/specs/218-glow-floor-geometry.md @@ -0,0 +1,337 @@ +# Issue #218 — floating-point шум одной комнаты не гасит Glow всего пространства + +- **Issue:** https://github.com/Matysh/houseplan-card/issues/218 +- **Связанные контракты:** #19, #55, #61, #65–#67, #197 +- **Тип:** bug, обычный полный трек +- **Приоритет:** P1 +- **Пользовательское изменение:** да + +## 1. Сценарий и персона + +**Персона:** домочадец или гость, который смотрит состояние освещения в View или +kiosk, и администратор, проверяющий тот же план в редакторе устройств. + +**Сценарий:** в пространстве есть несколько комнат и включённый источник света. +После обычных операций редактора две координаты общей границы могут отличаться +на один ULP (`5.55e-17` или `1.11e-16`). На таком плане polyclip не завершает +объединение полов. Сейчас одна комната с этим численным шумом делает пустым +clipPath каждого источника пространства, поэтому Glow не виден ни при каких +настройках, хотя источник включён и visibility polygon построен. + +## 2. Что человек увидит до и после + +До исправления одна численно нестабильная комната может молча погасить все +световые пятна пространства. После исправления обычный floating-point шум не +влияет на свет, а действительно некорректная комната в худшем случае исключает +только свою часть пола: остальные комнаты продолжают освещаться. + +## 3. Подтверждённая причина + +Текущая цепочка: + +```text +_renderGlowLayer + → intersectionPaths([visibilityPolygon], floor всех комнат) + → unionBodies(floor) + → polyclip union с исходными double-координатами + → exception → null → lit=[] → пустой clipPath +``` + +`src/physical-geometry.ts` передаёт координаты в polyclip без численной +стабилизации. `unionBodies()` проглатывает исключение и возвращает `null`, а +`intersectionPaths()` не отличает сбой одной комнаты от отсутствующего пола. +В результате fail-dark, нужный против утечки света за пределы дома, применяется +к целому пространству вместо минимального проблемного фрагмента. + +Реальный экспорт владельца локализовал две пары координат с расхождением в +последнем бите. Экспериментальное квантование входа union до `1e-6` единицы +плана восстановило непустой clipPath. Экспериментальный патч в код не вошёл. + +## 4. Нормативные источники и приоритет + +При расхождении применяются: + +1. решения владельца и приёмка в #218; +2. это ТЗ после зелёного SPEC-REVIEW; +3. действующий fail-dark контракт Glow и smoke `smoke_glow_fail_dark`; +4. контракт #197: локальный сбой булевой операции не удаляет валидную геометрию + всего пространства; +5. текущая реализация как compatibility baseline для явно не изменяемого + поведения. + +## 5. Цели + +1. Сделать булевы операции пола устойчивыми к невидимому floating-point шуму. +2. Ограничить отказ одной комнаты этой комнатой, не всем пространством. +3. Сохранить fail-dark там, где свет действительно не должен рисоваться. +4. Сделать остаточную деградацию диагностируемой и не заспамить консоль. +5. Не менять сохранённые координаты и вид валидных планов. + +## 6. Scope + +В задачу входят: + +- численная стабилизация входных колец общего boolean-слоя + `src/physical-geometry.ts` непосредственно перед polyclip; +- устойчивое пересечение visibility fan с набором комнат; +- покомнатная деградация при исключении после стабилизации; +- диагностический callback из pure geometry и дедуплицированный `console.warn` + на уровне карточки с id пространства и id проблемной комнаты; +- минимизированная regression fixture, сохраняющая проблемные координаты из + реального экспорта без пользовательских имён, entity ids и остального конфига; +- проверка full View, kiosk/тот же renderer и Glow-base; +- unit, targeted smoke, mutation gate, golden verification и документация. + +## 7. Не входит в задачу + +- изменение радиуса, цвета, opacity, falloff, blend или анимации Glow; +- изменение visibility algorithm, occluders, проёмов или кладки; +- ослабление защиты источника внутри стены и луча вне пола; +- исправление произвольной самопересекающейся комнаты или автоматический repair; +- изменение/сохранение/миграция координат пользователя; +- предупреждение в UI, repair issue или новая настройка точности; +- логирование названий комнат, координат, entity ids или полного конфига; +- изменение статических room fills, если наблюдение Glow-base не связано с той + же численной причиной; +- принятие golden baseline на Windows или выпуск без команды владельца. + +## 8. Численный контракт + +### 8.1. Стабилизация + +- каждое конечное `x/y`, передаваемое из `unionBodies()` в polyclip, + нормализуется с quantum `1e-6` единицы плана; +- квантование применяется только к вычисляемой копии; входные массивы не + мутируются; +- `-0` канонизируется в `0`; +- соседние дубликаты, возникшие после квантования, удаляются, замыкающая точка + добавляется ровно один раз; +- кольцо, у которого после нормализации меньше трёх различных точек или нулевая + площадь в пределах quantum, считается непригодным и не передаётся polyclip; +- результат детерминирован, не зависит от порядка тел и повторного вызова; +- смещение любой точки не превышает половины quantum и многократно меньше + физической/экранной точности плана. + +Quantum является техническим контрактом этой задачи. Его изменение после +ревью требует доказать те же ULP-fixtures, raster parity и отсутствие слипания +реально различных геометрических деталей. + +### 8.2. Пересечение с полом + +Нормальный путь сохраняет объединение валидных комнат до intersection, чтобы +перекрытия legacy-планов не превращались в evenodd-дыры. Если объединение или +пересечение всего набора всё же падает, применяется покомнатный путь: + +1. visibility fan остаётся ограничивающей геометрией и никогда не возвращается + без floor clipping; +2. комнаты обрабатываются независимо; +3. успешные результаты объединяются в итоговый набор path fragments; +4. комната, для которой boolean operation упала, пропускается и сообщает свой + индекс/id через diagnostic callback; +5. отсутствие всех валидных комнат возвращает `[]`. + +Fallback не может вернуть raw visibility fan или осветить область вне пола. + +## 9. Диагностика + +Pure-функции геометрии не вызывают `console` напрямую. Они предоставляют +структурированный сигнал о покомнатной ошибке. `_renderGlowLayer()` связывает +индекс пола с `room.id` и пишет предупреждение формата, эквивалентного: + +```text +HOUSEPLAN GLOW GEOMETRY FALLBACK: #218, space , room +``` + +Требования: + +- нет room name, entity id, координат и текста polyclip exception; +- одинаковая ошибка логируется не чаще одного раза на сочетание + `space id + geometry fingerprint + room id`; +- новый fingerprint может дать новое предупреждение, чтобы ремонт геометрии и + повторный дефект были различимы; +- валидный ULP-шум, исправленный квантованием без fallback, не создаёт warning; +- warning не влияет на render result и не требует i18n. + +## 10. Glow-base + +Наблюдение `0 из 6` проверяется на той же минимизированной fixture при условиях, +когда текущий контракт действительно требует тёмную базу: Glow включён, а +effective room fill отсутствует. Для каждой пригодной комнаты должен +существовать `.glow-base`. + +Если тест уже зелёный, код Glow-base не меняется и результат фиксируется в +implementation evidence. Если тест красный из-за того же raw-coordinate +boolean path, исправление выполняется общей стабилизацией без отдельного +renderer. Иная причина является отдельной задачей и не расширяет #218. + +## 11. UX, accessibility и touch + +Новых элементов UI и взаимодействий нет. View/kiosk, mouse, touch и pen получают +один и тот же исправленный render. Pointer targets, focus, tooltip, reduced +motion и editor gestures не меняются. Ошибка не показывается домочадцу: для +администратора остаётся redacted console warning только при реальной деградации. + +## 12. Данные, migration, privacy и i18n + +Config, layout, backend schema, localStorage и сериализация не меняются. +Миграция не нужна. Исходные координаты остаются байт-в-байт прежними после +Open → Save без пользовательского изменения. Regression fixture хранит только +минимальную анонимизированную геометрию. Новых строк и i18n-ключей нет. + +## 13. Архитектура и зоны изменений + +Ожидаемая цепочка: + +```text +saved room coordinates (immutable) + → normalized boolean input copy + → normal union + intersection + → per-room fail-dark fallback only on exception + → path fragments + structured diagnostics + → existing Glow clipPath +``` + +Ожидаемые зоны: + +- `src/physical-geometry.ts` — normalization, robust union/intersection API; +- `src/houseplan-card.ts` — room ids, deduplicated redacted warning и cache key; +- `test/physical-geometry.test.mjs` — ULP, malformed-room, immutability, + permutation и fail-dark unit tests; +- targeted browser smoke/fixture для реального шестикомнатного случая и + Glow-base assertion; +- `scripts/mutation-gate.mjs` и его registry test; +- `docs/TESTING.md`, `docs/CHANGELOG.md`, `docs/CHANGELOG.ru.md`; +- generated bundles после build. + +Нельзя создавать второй Glow renderer или применять квантование при записи +данных. + +## 14. Performance и security + +- нормальный render path выполняет не больше одного дополнительного линейного + прохода по вершинам перед уже существующей boolean operation; +- покомнатная обработка запускается только после исключения/невалидного общего + результата и не является постоянным per-frame N×boolean path; +- существующий LRU clip cache сохраняется; diagnostic dedupe ограничен текущими + space/fingerprint keys и очищается/ограничивается вместе с жизненным циклом + карточки; +- нельзя добавлять таймеры, observers, network calls или persisted telemetry; +- warning redacted согласно §9; +- fail-dark security boundary не ослабляется. + +## 15. Acceptance criteria + +1. **AC1 — ULP union.** Два прямоугольника с общей гранью, отличающейся на один + ULP, после `unionBodies()` дают валидную объединённую геометрию. + **Доказательство:** unit, красный при mutant без normalization. +2. **AC2 — реальная regression fixture.** Анонимизированная шестикомнатная + fixture с точными проблемными парами координат даёт непустой `lit` и + непустой DOM clipPath включённого источника. **Доказательство:** unit + + targeted browser smoke; тест красный на коде до #218. +3. **AC3 — локальная деградация.** Самопересекающаяся/намеренно непригодная + комната не удаляет пересечение с валидными комнатами. Проблемная комната не + получает свет. **Доказательство:** unit с injected/deterministic failure. +4. **AC4 — диагностический след.** Fallback сообщает space/room id один раз на + fingerprint и не раскрывает names, entities, coordinates или exception. + **Доказательство:** unit/browser console capture и повторный render. +5. **AC5 — fail-dark.** Источник в кладке, visibility fan мимо пола, пустой floor + и полный отказ всех комнат остаются тёмными. **Доказательство:** существующий + `smoke_glow_fail_dark` без ослабления плюс unit. +6. **AC6 — неизменность валидных планов.** Обычные Glow-сцены дают ту же площадь + с допуском boolean quantum и те же pixels в reviewed golden; входные arrays + и persisted config не мутируются. **Доказательство:** unit, golden verify и + config round-trip/source review. +7. **AC7 — Glow-base.** На regression fixture каждая eligible-комната имеет + текущую Glow-base; если observation не воспроизводится, implementation + evidence прямо это фиксирует без лишнего продуктового изменения. + **Доказательство:** targeted browser smoke. +8. **AC8 — deterministic geometry.** Повторный вызов и перестановка room bodies + дают эквивалентную площадь/path coverage; `-0`, adjacent duplicates и + quantum-degenerate rings обрабатываются явно. **Доказательство:** unit. +9. **AC9 — performance/privacy.** Fast path остаётся O(vertices + текущие + boolean operations), fallback не работает постоянно, warning redacted и + deduplicated. **Доказательство:** source review и targeted performance smoke. +10. **AC10 — release artifacts.** Оба changelog, testing docs, mutation registry, + generated bundles и screenshot/golden fingerprints актуальны. + **Доказательство:** docs check, build parity и review diff. + +## 16. План автотестов + +### 16.1. Цикл реализации + +```bash +npm run typecheck +npm test +npm run build +``` + +Unit matrix: + +- 1 ULP и `±1e-12/±1e-9` на общей вертикальной/горизонтальной границе; +- exact six-room minimized fixture; +- invalid room среди двух валидных; +- empty/missed floor и source-outside-floor; +- input immutability, repeatability, reversed/permuted bodies; +- structured callback и diagnostic dedupe. + +### 16.2. Targeted smoke перед S7 + +```bash +node demo/smoke_glow.mjs +node demo/smoke_glow_blending.mjs +node demo/smoke_glow_fail_dark.mjs +``` + +Добавляется отдельный узкий smoke либо детерминированное расширение +`smoke_glow` для ULP-fixture, DOM clipPath, Glow-base и console warning. + +### 16.3. Mutation gate + +| id | Мутация | Обязанный guard | +|---|---|---| +| `union-quantization-removed` | убрать normalization перед polyclip | AC1/AC2 unit | +| `union-failure-kills-space` | вернуть общий отказ всего floor | AC3 unit/smoke | +| `union-failure-silent` | убрать structured diagnostic/warning | AC4 console test | +| `glow-fail-dark-weakened` | вернуть raw fan или пропустить source guard | существующий fail-dark smoke | + +Перед S7 выполняются `node scripts/mutation-gate.mjs --check` и целевые мутанты, +если их runtime укладывается в локальный review gate. + +### 16.4. Golden и предрелиз + +Golden verify выполняется на существующих Glow-сценах: валидные планы не должны +получить видимый raster diff. Если для #218 добавляется новый visual scenario, +его baseline принимается только через `npm run golden:accept -- --reviewed` по +полному Linux CI artifact перед бетой. Полный smoke/golden/performance — +предрелизный гейт по PROCESS, не цикл реализации. + +## 17. Release-артефакты + +В user-visible implementation commit обязательны: + +- `docs/CHANGELOG.md` и `docs/CHANGELOG.ru.md` — одна комната с численным шумом + больше не гасит Glow пространства; +- `docs/TESTING.md` — ULP-fixture, покомнатный fail-dark fallback и диагностика; +- targeted smoke/mutation registry; +- generated `dist`, demo и integration bundles; +- `docs/images/screenshots.json`, только если build/visual harness этого требует. + +User Guide не меняется: настройки и пользовательский сценарий остаются прежними. + +## 18. Откат + +Откат выполняется одним user-visible implementation commit вместе с tests, +bundles и changelog. Persisted rollback/migration не нужен. При откате вернётся +старый полный fail-dark, но сохранённые планы останутся совместимыми. + +## 19. Принятые предположения + +Можно свободно изменить на ревью ТЗ без вопроса владельцу: + +1. quantum `1e-6` применяется ко всем входам `unionBodies()`, а не только Glow, + потому что это общий boolean boundary и эксперимент #218 подтвердил его; +2. pure geometry сообщает индексы, а room/space ids добавляет renderer; +3. warning дедуплицируется по space + fingerprint + room; +4. приватный реальный экспорт не коммитится — только минимальная геометрия с + сохранёнными проблемными double-значениями; +5. наблюдение Glow-base считается частью #218 только при той же корневой причине. diff --git a/docs/specs/README.md b/docs/specs/README.md index c855b387..ff5d5c18 100644 --- a/docs/specs/README.md +++ b/docs/specs/README.md @@ -57,6 +57,7 @@ GitHub Issues и GitHub Projects (v2) остаются единственным | [#210](https://github.com/Matysh/houseplan-card/issues/210) Фиксированное пространство экземпляра карточки | [210-fixed-floor-card.md](210-fixed-floor-card.md) | | [#211](https://github.com/Matysh/houseplan-card/issues/211) Визуальное соответствие маркеров дизайн-пакету #179 | [211-device-icons-visual-parity.md](211-device-icons-visual-parity.md) | | [#217](https://github.com/Matysh/houseplan-card/issues/217) Внешняя рамка Text-маркера должна быть капсулой | [217-text-shell-outline.md](217-text-shell-outline.md) | +| [#218](https://github.com/Matysh/houseplan-card/issues/218) Floating-point шум комнаты не гасит Glow пространства | [218-glow-floor-geometry.md](218-glow-floor-geometry.md) | | [#205](https://github.com/Matysh/houseplan-card/issues/205) Продолжение следа после короткой остановки пылесоса | [205-vacuum-trail-resume-grace.md](205-vacuum-trail-resume-grace.md) | ## P2