diff --git a/.github/workflows/process.yml b/.github/workflows/process.yml index 9d9be65e..04226fae 100644 --- a/.github/workflows/process.yml +++ b/.github/workflows/process.yml @@ -978,64 +978,52 @@ jobs: ${{ needs.prepare.outputs.spec_body_changed == 'true' && format('ТЗ в теле issue менялось после зелёного ревью ТЗ ({0}, записанный хеш {1}). GitHub хранит правки тела без diff — дельту показать нельзя, поэтому AC сверяются с ТЕКУЩИМ текстом целиком, а не по дельте, и находка называется в вердикте (#517).', needs.prepare.outputs.spec_body_doc, needs.prepare.outputs.spec_body_recorded) || '' }} - **Если цикл не первый — объём разбора по дельте, а не заново** - (PROCESS.md §2.9, issue #214). Раньше промпт был одинаковым для - всех раундов, и повторный цикл заново выводил продуктовую рамку и - перепроверял AC, которых правка не касалась: r2 по #150 стоил - полного прогона ради одной строки в тестовой фикстуре. - - Порядок для r2 и дальше: - 1. найди вердикт предыдущего раунда в комментариях issue и SHA, - на котором он получен. SHA в вердикте не назван — это находка; - 2. объяви дельту: `git diff <тот SHA>..HEAD` для кода, дифф файла - ТЗ или тела issue для spec. Дельта — предмет этого раунда; - 3. по каждой находке предыдущего раунда покажи, чем именно она - закрыта: строка кода или текста, а не заявление автора; - 4. заново проверяй только те AC, чьё доказательство дельта - задевает. Остальные наследуй; - 5. в документе обязателен раздел «Унаследовано из r»: что - принято без повторной проверки, со ссылкой на документ того - раунда и SHA, на котором вывод получен. Без этого перечня - сокращение — молчаливое доверие, а такой тихий успех уже - дважды стоил дня (#171, #207). - - Разбор остаётся ПОЛНЫМ, если дельта не локальна: ребейз на ушедший - вперёд dev (после ребейза это другой код, §7.2), смена контракта - поведения, задета новая подсистема, либо объём дельты сопоставим с - исходной задачей. Сомневаешься — разбирай полностью и скажи почему. - - Сокращается объём РАЗБОРА, а не строгость: правка по замечанию - способна сломать AC, который предыдущий раунд признал выполненным — - так появилась регрессия #102. Поэтому граница не «только находки», а - «находки плюс всё, до чего дотягивается дельта». + Правила ревью в этом промпте не повторяются (#634): их канон — + PROCESS.md, выжимка для ревьюера — docs/process/REVIEWER.md, где + каждый пункт ссылается на свой раздел канона. Ниже — только то, что + относится к этому прогону, и требования, которые нельзя пропустить. Прочитай в этом порядке, прежде чем судить: 1. docs/SCOPE.md — зачем продукт существует и для кого. Он ограничитель: «features are built, improved and accepted only if they serve a job listed here». Первый вопрос к задаче — какую строку Core user jobs она закрывает. - 2. AGENTS.md и PROCESS.md — процесс, классы изменений, трейлеры, - лимит циклов, формат вердикта. - 3. Тело issue #${{ github.event.issue.number }} и все комментарии. - 4. Если меняется видимое поведение — docs/USER-GUIDE.ru.md: + 2. docs/process/REVIEWER.md — обязанности ревьюера: позиция, + ревью ТЗ, код-ревью, объём гейтов, повторный раунд, находки и + вердикт. Раздел PROCESS.md по ссылке открывай, когда пункт + касается твоего решения; при расхождении прав PROCESS.md. Если + файла в материале нет — читай PROCESS.md §2.4, §2.7, §2.10, + §4, §7.2, §8, §12. Задача правит сам конвейер, гейты или + процесс — PROCESS.md целиком, §10 в первую очередь. + 3. AGENTS.md — классы изменений, трейлеры, гейты, формат вердикта. + 4. Тело issue #${{ github.event.issue.number }} и все комментарии. + 5. Если меняется видимое поведение — docs/USER-GUIDE.ru.md: терминология интерфейса берётся оттуда, а не изобретается. - 5. Канонический документ затронутой подсистемы: docs/SUN.md, + 6. Канонический документ затронутой подсистемы: docs/SUN.md, LIGHT.md, CANVAS.md, WALL-THICKNESS.md, UX-MODES.md, CONFIG-COMPATIBILITY.md, TOUCH-SUPPORT.md. + **Если цикл не первый — объём разбора по дельте, а не заново** + (PROCESS.md §2.10, issue #214): найди вердикт и материал + предыдущего раунда (блок «Материал раунда» его документа), объяви + дельту `git diff <тот SHA>..HEAD` (для spec — дифф тела issue или + файла ТЗ), по каждой находке покажи, чем именно она закрыта — + строка кода или текста, а не заявление автора, — и заново проверяй + только AC, чьё доказательство дельта задевает. Разбор остаётся + ПОЛНЫМ, если дельта не локальна: ребейз на ушедший вперёд dev, + смена контракта, новая подсистема, объём сопоставим с задачей. + Сомневаешься — разбирай полностью и скажи почему. Сокращается + объём РАЗБОРА, а не строгость. + Для этапа spec: ТЗ живёт в теле issue (#517) — читай его, а не файл. Файлы docs/specs/-*.md — архив ТЗ до 2026-09-10: если такой файл есть у старой задачи, он и есть материал, новые не создаются. Проверь обязательные разделы §7.1, однозначность каждого AC и - указание способа доказательства. Отдельно проверь, что автор не - выдал догадку за решение: утверждение о поведении, которого нет ни - в одном документе и которое не помечено как предположение, — + указание способа доказательства. Утверждение о поведении, которого + нет ни в одном документе и которое не помечено как предположение, — замечание. Не бывает сложной задачи без единого открытого вопроса. - - Владельцу задаются только продуктовые вопросы: что человек видит или - делает и каков объём видимых изменений в этом issue. Технический - вопрос, вынесенный владельцу, — тоже замечание: ты его снимаешь и - решаешь по существу в своём вердикте. + Технический вопрос, вынесенный владельцу, — тоже замечание: ты его + снимаешь и решаешь по существу в своём вердикте. Для этапа code: материал — диапазон `git log --oneline origin/dev..HEAD` и `git diff origin/dev...HEAD`. **Материал ревью — ровно @@ -1044,95 +1032,60 @@ jobs: привязан к этому SHA (#312), и страж слияния сверяет вершину ветки с ним. Если автор в issue называет более новый коммит, которого в материале нет, — это находка «материал не был запушен до метки», а не - повод подтянуть его самому (#437 r3→r4 стоил лишнего раунда именно так, - #499). Ручного тестирования в цикле нет, + повод подтянуть его самому (#499). Ручного тестирования в цикле нет, поэтому именно ты отвечаешь на вопрос «оно вообще работает». По каждому AC: либо он доказан автотестом и ты убедился, что тест умеет падать, либо разобран по коду с явной записью «проверено - чтением, не исполнением». «Verified» без названной команды и её - результата доказательством не является. Зависимости уже установлены - workflow, Chromium тоже — `npm ci` выполнять не нужно. Проверь - трейлеры Issue и User-Visible, при User-Visible: yes — правки в оба - changelog в том же коммите. + чтением, не исполнением». Для защитного AC (валидация, гард, лимит, + отказ, инвариант) в документе обязательна строка таблицы + «AC · чем доказан · чем краснеет» с результатом прогона; пустой + третий столбец — находка Medium (§2.7, #435). «Verified» без + названной команды и её результата доказательством не является. + Проверь трейлеры Issue и User-Visible, при User-Visible: yes — правки + в оба changelog в том же коммите. Если дифф меняет величину, видимую + пользователю, назови прямо: какое число видно дважды и один ли у + него источник (§8). - **Объём гейтов соразмерен задаче.** Прогонять весь набор на каждой - правке — не тщательность, а потеря времени: полные наборы это - предрелизный гейт (PROCESS.md §8), а не гейт ревью. + **Объём гейтов соразмерен задаче** (PROCESS.md §8): полные наборы — + предрелизный гейт, а не гейт ревью. ${{ needs.prepare.outputs.validated_note }} Если зелёного прогона на этом SHA нет — прогоняешь сам, они дешёвые, - и в повторном раунде тоже: код изменился, а стоят они минуты: - `npx tsc --noEmit`, `npm test`, `npm run build` со сверкой трёх - копий бандла. Плюс `node scripts/check-docs.mjs`, если diff трогает - `src/**`: отпечаток скриншотов документации считается по всему - `src/**`, поэтому любая правка фронтенда делает его устаревшим — - выбирать тут нечего. Пропуск этого шага в #230 и #234 оставил `dev` - с красным job `docs` до следующей задачи (#237). - - Если diff трогает геометрию или ссылки на неё — рёбра комнат, - записи толщины, `layout`, `marker.space`, `open_spans` — обязательны - инварианты модели (#254): `npm test` уже гоняет их на всех моделях - проекта, а на конкретной конфигурации они проверяются командой - `npm run invariants -- --config <экспорт или ответ config/get>`. - Три вопроса, на которые они отвечают, и все три уже стоили - продукту дефектов: не исчезла ли запись толщины (#253), разрешима ли - каждая ссылка (#244, #252) и равен ли ключ записи толщины ключу - решёточного ребра (#258, #259). Последний сравнивает строки без - допусков: сдвиг ключа на один шаг решётки равен допуску первых двух, - поэтому они на нём промахиваются. Если задача меняет геометрию, а - инварианты в отчёте не названы — это непрогнанный гейт, а не мелочь. - - По необходимости, и «необходимость» определяется diff'ом и AC: - - браузерные смоки `demo/smoke_*.mjs` — названные в AC плюс те, - что печатает `node scripts/smoke-select.mjs --base --head `. - Сколько их всего — считает `ls demo/smoke_*.mjs | wc -l`; вшитое - в этот текст число трижды расходилось с деревом, поэтому его - здесь больше нет. Прогон всех уместен только когда задача - действительно задевает всё. Выбирать по теме недостаточно: регресс #234 поймал - `smoke_wall_junctions`, который по названию про стыки стен, а не - про толщину отрезка. Инструмент печатает три вида ответа, и они - разные: «прямое совпадение» — смок называет изменённый символ, - «зарегистрированная связь» — смок проверяет следствие контракта, - не называя его, «НЕОПРЕДЕЛЁННОСТЬ» — связь не доказана, и это не - разрешение ничего не прогонять. Вывод инструмента прикладывается - к комментарию ревью вместе с решением по каждой строке: прогнал - либо не прогнал и почему. Слабые связи (одно распространённое - имя) — повод посмотреть, а не обязанность прогонять; - - `npm run golden:verify` — если diff может изменить видимый - результат: рендер, геометрия, стили, слои; - - `python -m pytest tests_backend -q` — если тронут - `custom_components/**/*.py`; - - performance-профили — если названы в AC либо тронуты - чувствительные к перфу пути. - - **Одно число — один источник.** Если дифф добавляет или меняет - величину, видимую пользователю, назови в отчёте прямо: какое число - видно дважды (превью против записи, подпись против площади, - подсветка инструмента против сохранённого значения) и один ли у него - источник. Три дефекта подряд имели именно эту причину — #234, #233 и - способ, которым #234 обнаружили. Механическая часть закреплена - тестом `test/single-source-numbers.test.mjs`, смысловая — твоя. - - Дисциплина «тест должен уметь падать» не отменяется, но применяется к - тем тестам, которые ты прогонял. + и в повторном раунде тоже: `npx tsc --noEmit`, `npm test`, + `npm run build` со сверкой трёх копий бандла, плюс + `node scripts/check-docs.mjs`, если diff трогает `src/**`. + Зависимости уже установлены workflow, Chromium тоже — `npm ci` + выполнять не нужно. По диффу и AC: браузерные смоки — названные в AC + плюс вывод `node scripts/smoke-select.mjs --base --head `, + приложенный к комментарию с решением по каждой строке: прогнал либо + не прогнал и почему. Три вида ответа инструмента разные: «прямое + совпадение», «зарегистрированная связь», «НЕОПРЕДЕЛЁННОСТЬ» — связь + не доказана, и это не разрешение ничего не прогонять; слабые связи — + повод посмотреть, а не обязанность прогонять; + `npm run golden:verify` при видимом изменении; + `python -m pytest tests_backend -q` при правке + `custom_components/**/*.py`; инварианты модели + `npm run invariants -- --config <экспорт>` при правке геометрии или + ссылок на неё (#254) — задача меняет геометрию, а инварианты в + отчёте не названы, это непрогнанный гейт, а не мелочь; + performance-профили, если названы в AC. Дисциплина «тест должен + уметь падать» не отменяется, но применяется к тем тестам, которые + ты прогонял. **В комментарии обязателен перечень: какие гейты прогнал, какие нет и - почему.** Это условие честности такого сужения: непрогнанный гейт - становится видимым решением, а не молчаливым пропуском. Раздел «чего - не проверял» в документе ревью — не формальность, а главный его - раздел на коротких задачах. + почему.** Раздел «чего не проверял» в документе ревью — не + формальность, а главный его раздел на коротких задачах. Ты НЕ правишь ни ТЗ, ни продуктовый код. Только оцениваешь. Серьёзность: High блокирует; Medium В СКОУПЕ задачи чинится в ней же — без High это жёлтый вердикт и возврат автору, отдельный issue - НЕ заводится (решение владельца 2026-08-19, #202: заведение и - обслуживание issue дороже правки на месте); Low либо правится, - либо снимается с записью. Жёлтый вердикт допустим и при полностью - выполненных AC, если изменение не решает заявленный сценарий или - ухудшает смежный. Продуктовое рассуждение расширяет вопросы, но не - отменяет AC и не даёт права менять скоуп. + НЕ заводится (#202); Low либо правится, либо снимается с записью. + Жёлтый вердикт допустим и при полностью выполненных AC, если + изменение не решает заявленный сценарий или ухудшает смежный. + Продуктовое рассуждение расширяет вопросы, но не отменяет AC и не + даёт права менять скоуп. Только Medium-находку ВНЕ скоупа задачи (попутный дефект соседнего поведения, который в этой ветке чинить нельзя) заведи отдельным @@ -1142,13 +1095,11 @@ jobs: Напиши полный документ ревью в файл, путь которого лежит в переменной окружения REVIEW_DOC (абсолютный, ВНЕ репозитория). - - Почему не в docs/reviews: документ там был некоммитнутым файлом того - же дерева, которое ты мутируешь, проверяя «умеет ли тест падать». На - #220 три раунда подряд документ исчезал — восстановление дерева - (`git checkout -- .`, `git clean -fd`) сносит собственный артефакт - ревью, потому что он untracked. В репозиторий его положит шаг - публикации, взяв из REVIEW_DOC; тебе трогать docs/reviews не нужно. + Не в docs/reviews: восстановление дерева после проверки «умеет ли + тест падать» (`git checkout -- .`, `git clean -fd`) сносит + untracked-файл, и на #220 документ так исчезал три раунда подряд. + В репозиторий его положит шаг публикации, взяв из REVIEW_DOC; + тебе трогать docs/reviews не нужно. В самом репозитории не создавай файлов вообще: любые изменения в рабочей копии будут отброшены. Имя документа в docs/reviews шаг diff --git a/AGENTS.md b/AGENTS.md index 8b07e869..b0ed533b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -22,11 +22,22 @@ For work that changes visible behaviour, also read `docs/USER-GUIDE.ru.md` — interface wording comes from there and is not invented, or the UI starts speaking developer. -Then `PROCESS.md` (the full process), `docs/STATUS.md` (where the release line -is), and for non-trivial changes `docs/ARCHITECTURE.md` plus the canonical -document of the subsystem you touch: `SUN.md`, `LIGHT.md`, `CANVAS.md`, -`WALL-THICKNESS.md`, `UX-MODES.md`, `CONFIG-COMPATIBILITY.md`, -`TOUCH-SUPPORT.md`. +**Reading order by role** (#634). `node scripts/entry-cost.mjs` measures each +route and `test/entry-cost.test.mjs` keeps this list equal to its routes: + +- author (analysis, spec, implementation, infrastructure): `docs/SCOPE.md` → + `AGENTS.md` → `docs/process/AUTHOR.md` → `docs/STATUS.md`; +- reviewer (spec or code): `docs/SCOPE.md` → `AGENTS.md` → + `docs/process/REVIEWER.md`, then the issue body and its comments; +- changing the pipeline, the gates or the process itself: `docs/SCOPE.md` → + `AGENTS.md` → `PROCESS.md` → `docs/STATUS.md`. + +The two digests quote and link `PROCESS.md` section by section; it stays the +only complete canon and wins any disagreement, so open the linked section +whenever a digest line governs your current step. For non-trivial changes add +`docs/ARCHITECTURE.md` plus the canonical document of the subsystem you touch: +`SUN.md`, `LIGHT.md`, `CANVAS.md`, `WALL-THICKNESS.md`, `UX-MODES.md`, +`CONFIG-COMPATIBILITY.md`, `TOUCH-SUPPORT.md`. Standard commands live in `package.json` scripts, `CONTRIBUTING.md` and `docs/DEVELOPMENT.md`. diff --git a/PROCESS.md b/PROCESS.md index 892ba945..7c0b9066 100644 --- a/PROCESS.md +++ b/PROCESS.md @@ -12,6 +12,14 @@ > репозитории: до августа 2026 канон лежал только в папке владельца, и свежий клон > его не содержал вовсе. > +> **Ролевые конспекты** (#634): `docs/process/AUTHOR.md` и +> `docs/process/REVIEWER.md` — выжимки этого файла со ссылками на его разделы. +> Автор и ревьюер входят через них (порядок чтения по роли — `AGENTS.md`) и +> открывают раздел канона, когда пункт конспекта касается текущего шага. +> Правил конспекты не добавляют; при расхождении побеждает этот файл. Ссылки и +> ключевые формулировки конспектов сверяет `test/process-digests.test.mjs`, +> поэтому правка формулировки здесь правит и конспект тем же коммитом. +> > **Приоритет источников.** Канонический бэклог — GitHub Issues; статус живёт в > метках и больше нигде: Project v2 не используется. При расхождении > документации с GitHub побеждает GitHub. Этот файл — единственный полный канон @@ -987,8 +995,9 @@ S7-code-review → код-ревью → слияние в dev → S8-merged л Текущая техническая реализация независимого ревьюера — `anthropics/claude-code-action`; это деталь автоматизации, а не закрепление роли -или вида задач за Claude. Ревьюер читает `docs/SCOPE.md`, `AGENTS.md`, этот -документ и тело issue, публикует разбор комментарием, заводит issue на +или вида задач за Claude. Ревьюер читает `docs/SCOPE.md`, `AGENTS.md`, +конспект `docs/process/REVIEWER.md` с разделами этого документа по его ссылкам +(#634) и тело issue, публикует разбор комментарием, заводит issue на Medium-находки вне скоупа задачи (#202), кладёт документ в `docs/reviews/` ветки задачи и возвращает вердикт структурированным JSON. **Метку переставляет отдельная детерминированная стадия по вердикту, а не модель.** diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md index 8c9ff3fb..18c96738 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -714,5 +714,6 @@ mode before dispatching synthetic gestures, it must also wait for the observable mode-transition and viewport-refit state to settle, because the stage can finish its physical resize after the Lit update (#460). -When adding a checklist line marked `[auto: ...]` in docs/TESTING.md, add the -failing check in the same commit — that is what the marker now promises. +When adding a checklist line marked `[auto: ...]` in docs/TESTING.md or its +appendices in docs/testing-notes/, add the failing check in the same commit — +that is what the marker now promises. diff --git a/docs/STATUS-FEATURES.md b/docs/STATUS-FEATURES.md new file mode 100644 index 00000000..e4e45575 --- /dev/null +++ b/docs/STATUS-FEATURES.md @@ -0,0 +1,254 @@ +# Feature surface and milestones + +> Moved verbatim from `docs/STATUS.md` by #634 so that the session entry +> route stays small (`node scripts/entry-cost.mjs`). The documentation policy +> of `STATUS.md` applies here unchanged: a user-visible feature gets its +> bullet in the same commit as the behaviour. + +## Current feature surface (since the 2026-07-17 snapshot) + +- **Configurable summary overlay** (#437, merged into `dev`): a two-part + View control opens one adaptive form or toggles a browser-local preference. + Shared ordered blocks can show readable HA entity states and three system + values; local icon/text scaling applies to ordinary and kiosk View without + changing editor geometry. The overlay is a screen-space layer on the right + or bottom, respects native HA mobile mode, and temporarily hides in a small + card. Shared saves remain revision-checked; read-only and kiosk users receive + only the local controls. + #505 aligns its split control, compact overlay and wide settings dialog with + the designer reference. Both entry and exit animate; size controls are absent + from this form without changing saved scales, and mobile visibility is + disabled under draft local-off. The GitHub issue is the source of workflow + status; `docs/design/505-summary-panel/` holds the reference and visual evidence + instructions, not a separate backlog. +- **Presence radars (#485 Stage 1, merged into `dev`):** an optional marker-owned, + change-aware v1 configuration binds exact HA sources for recognized ESPHome + LD2450 or explicit Cartesian, polar, range, zone-state and presence-only + profiles. The backend owns freshness, pairing, projection, room clipping, + ACL-filtered bounded frames and teardown. The lazy device editor owns source + inspection and physical two-point setup; View receives only normalized live + frames. Raw observations, calibration samples and the short target trail are + session-only and excluded from config, exports and support data. + +- **Primary sidebar panel + optional cards** (#486): the integration registers + `/houseplan` for every signed-in user after backend initialization. Its own HA + app bar wraps the same `houseplan-card`, so spaces, permissions, actions and + editor lifecycle stay shared; only the duplicated product title and outer + dashboard-card chrome are removed. Failure to register the panel is isolated + and reported in System Health. The optional full dashboard card requests full + width in Sections by default; explicit HA sizing remains authoritative. +- **Three editors + View**: Plan / Devices / Background (decor layer v1.33) as + tabs with an X to close; View is the default; only the last space persists, + while reload/return from another HA route always starts in View (#93). + **Kiosk mode** (v1.41.0): `kiosk: true` — no + header/editors, swipe between spaces, double-tap zoom reset, `cycle: N` + carousel, per-screen size multipliers in localStorage. Discrete wheel, + button, fit/home and kiosk-reset camera commands use one short retargetable + transition; direct pan/pinch and reduced motion remain immediate (#82). +- **Independent Glow overlay** (#55/#19, refined locally by #61/#65–#67): dark-room + base is used only when the effective fill resolver returns `null`, including + dynamic modes without usable data; resolved LQI/light/temp/custom colors keep + their exact alpha while per-source pools remain visible. Pools use + additive screen blending only after a cached real-pixel browser probe and + otherwise fall back to normal composition; legacy `fill_mode: glow` remains + losslessly readable/migratable. Marker roles are Auto/Always/Never; colour + can remain live, be overridden with live brightness, or be fixed together + with brightness. One perceptual alpha resolver gives every source the alpha at + the centre of its pool; `GLOW_FALLOFF` then spends it over the whole radius. + Per-source radius, doorway sight lines and transitive open boundaries remain — + all three now fall out of the visibility region instead of separate layers. +- **Custom room fill** (#56, space UX refined locally by #64): the space dialog + uses persistent user color/opacity as its ordinary first/default fill instead + of a redundant None option; a room keeps None as an explicit inherited-fill + suppression. Effective inheritance is room → space → safe documented default; + room reset restores space inheritance, and full/static renderers share the + same projection. +- **Universal device toggle** (#94, v1.62.0-beta.3): one `Toggle state` option is visible + for every marker and resolves the exact binding, functional device role or + explicitly configured controls. Hint, click, confirmation and cover + presentation share one result; partial groups call only the shown available + subset, stale controls never fall back, secure targets are no-op, and legacy + `cover`/absent light defaults round-trip losslessly. **Lights toggle by + default** (v1.39); other devices still default to the House Plan card. +- **Contextual Zigbee topology** (#54, refined by #457 and #464 on current dev): an + opt-in admin-only mouse hover shows direct neighbours without scanning. A + deterministic per-provider uplink tree now adds arrows towards the + coordinator while keeping non-route neighbour lines; a short bubble names a + remote space or explains that the next device/coordinator is not on the plan. + Active topology is above room names and unrelated markers, exact local + endpoints remain above it, and unknown-LQI dashes have a dark contrast + casing. Touch, kiosk, editors and the static card remain unchanged. +- **Plan geometry**: polyline split (v1.32), island rooms w/ evenodd holes + (v1.34), smart guides + 45° angle badge (v1.40), opening hover preview. + Openings support doors, windows and compact wide gates; gates retain door + contact/lock/Glow semantics but use two leaves opening only 10° outwards. +- **Canonical wall model** (#282, #306, #478, current dev): Walls and Thickness + accept `0..100 cm` for contour and independent segments. Model v10 + migrates legacy virtual spans into stable `cm:0` atoms; a per-space + dashed/solid selector controls both line style and Glow/sun transmission. + It also converts persisted unfinished contours to ordinary partitions; new + wall-chain segments are partitions from the first accepted click and chain + state is session-only. A finished chain losslessly merges/reconciles its own + seed component and finished-chain Undo/Redo restores the same fixed point; + room Delete/Merge owns direct and vacuum room-reference cleanup (#477). + Zero walls have no body, area or opening host, and + there is no Boundary tool. +- **Rooms**: room cards with metrics (temp/hum/lqi/light "1 of 3") and + proportional resize (v1.31); link icon to the HA area (v1.40.1, room taps + removed). **New-device red dot** (v1.29), lock action button (v1.30). +- **Dialog UX**: binding radios + entities checkbox + search dropdown + (v1.38.0); tap actions simplified to Device card / more-info / Toggle, + right-click → more-info (v1.38.1); Esc closes every dialog (v1.30.4). + Since #607, rejecting the real HA close button after an unsaved-changes + prompt explicitly reconciles the nested modal before reopening it; the same + Device, Room, Space or General-settings draft remains interactive. #609 keeps + every shared settings form on the reviewed 560 px canvas and single HA-owned + scroller in the authentic Home Assistant branch; at 480 px and below the + form is edge-to-edge fullscreen, while generic dialogs retain their previous + sizing. +- **Room settings, tier 3** (v1.42.0): per-room fill/temp-source/label sizes; + the settings button sits at the room's VISUAL centre (inscribed circle + + centroid pull), icon-derived size, zooms with the plan (v1.51.0). +- **Files & plans** (v1.44–v1.50): signed content urls with sandbox CSP, + copy-on-write plan files, "already uploaded" picker + explicit delete + (v1.47.0), store quotas instead of any age-based deletion (v1.49.0), + nothing is ever deleted on an inference (docs/SCOPE.md rule). +- **Square canvas** (v1.48.0) with a crash-safe two-store migration + (geom_pending, v1.50.0) and an explicit geometry/repair command (v1.50.1); + content-fit default zoom with devices as content, zoom out to 0.4×, + measured stage height (v1.49–v1.50.2). +- **Explicit hide flags** (v1.51.0, docs/FILTERING.md): per-device + `marker.hidden` seeded once from the old filter and controlled by the + bottom-left "Hide" / "Show" action in the device dialog; local + "Show hidden" ghosts; hidden counts toward room LQI on both cards, casts + no light (v1.51.1). +- **True plan deletion** (v1.60.0-beta.1, 2026-08-07): confirmed Delete beside Hide/Show; + a minimal `marker.removed` tombstone prevents auto-rediscovery but exposes + the binding to Add. Deleted devices are absent from every plan aggregate and + linked marker presentation; layout/files/trails are cleaned and stale layout + writes are rejected. Exact contact/lock references owned by architectural + openings remain active without restoring the standalone marker; live text + and other marker controls retain the older re-add-to-reactivate contract. +- **Yellow = working right now** (v1.51.0): climate uses `hvac_action` when + available and falls back to a current advertised non-off HVAC mode only when + the integration omits the action; service switches can no longer become + primary, and glow pool and icon share one condition. Editor gestures on touch + (pinch/pan) landed the same release. +- **Unified device status/activity** (v1.59.0-beta.10; pulse pipeline #98 local): four display + modes (Icon + state / Icon + state and activity / Value + state / Always static icon), + one semantic resolver for yellow + actual work, orange open/unlocked, unavailable and always-red alarms; + activity projects to three finite waves for a short event or one continuous + pulse for presence, mechanical travel and running. Always-static deliberately suppresses every state-driven visual, + satellite badge and live vacuum overlay while leaving hover, actions, Glow and + controls intact. Legacy Ripple-only migrates to Icon + activity on the next save. +- **Passive media lifecycle** (v1.60.0-beta.1): every `media_player`, + regardless of model, stays neutral while powered or playing and fades to + the existing unavailable appearance on explicit `off`; transport playback + is no longer classified as yellow actual work and no new visual state is + introduced. +- **Unified Background editor** (v1.60.0-beta.1): typed decor model, + physical cm/in strokes and text size, independent contour/fill opacity, decor+room smart + magnet, common selection/resize/rotate frame for every decor kind, numeric + geometry properties, and shared 50-command Undo/Redo. The plan image now has + an exclusive tool, 0.5 editor de-emphasis elsewhere, independent axes, + rotation, numeric properties and rotated content bounds. Legacy `width` and + `plan_scale` remain read-compatible and migrate only through explicit plan + optimisation. Furniture properties also expose the symbol itself. See + `DECOR-EDITOR.md`. +- **v1.59.0-rc.1** (2026-08-06): whole-plan lossless optimization with an + atomic config+layout commit and safe undo; the yellow actual-work plate is + retained alongside source glow; all Background objects have Select-mode + double-click properties; View hover highlights every room and reports its + clean-floor area. Audit fixes add eager activity baselines/source resets, + lossless legacy live-text editing, exact wall-fragment endpoints/compaction, + editor-visible virtual walls and prerelease-discovery reporting in CI. +- **v1.59.0-rc.2** (2026-08-06): Plan actions have one meaning each; Room + outline names the closed-contour tool; one named 50-command Undo/Redo stack + covers committed plan geometry; positional placement is always grid-bound. + Glow and toast overlays no longer steal room/tool pointers, View hover covers + shared thick walls, device Hide/Show is explicit, the static no-op aspect + field is gone, and the new user guide/product audit replaces archived docs. +- **v1.59.0** (2026-08-06): The stable 1.59 line includes all beta/RC work. + Room hover follows clean-floor wall faces, including nested contours and + projected opening gaps. Thick-wall rendering unions each room's own wall + ring, so one room's floor cannot erase another room's wall or leave white + slivers at complex crossings. +- **v1.59.1** (2026-08-06): device markers resolve a semantic entity role + instead of trusting registry order; one light-source resolver now drives + Glow, Light fill, room statistics, marker feedback and controls. Glow uses + 0.7 opacity. Current dev keeps external `controls` non-spatial: they still + drive group state, but only a real lamp marker or explicit `is_light` marker + can place a Glow pool. Compacted real walls retain their correct inner face + and body at real/virtual T-junctions. +- **v1.59.2** (2026-08-07): every modal uses the shared `hp-dialog` shell, + backed by Home Assistant's `ha-dialog` with a native demo fallback. Titles, + modal semantics, initial focus, focus trapping, Escape and restore focus are + consistent across all editors, nested dialogs and dialog replacement flows. + +## Recent milestones (details in CHANGELOG.md) + +- **v1.10.0** — audit & refactor: asyncio.Lock around all store writes (race fix, atomic + `expected_rev`), point `layout/update` instead of full `layout/set` (anti last-writer-wins), + new `layout/delete`, `safeUrl()` XSS guard, `fetchWithAuth`, KEY_HASS, streaming upload cap, + card split into modules (`styles.ts` / `types.ts` / `devices.ts`), dynamic spaces in the GUI + editor, dead code removed. +- **v1.11.0** — full English translation + en/ru UI localization. +- **v1.11.1** — brand images inside the integration; CI fully green for the first time. +- **v1.11.2** — Description textarea fix in the device dialog. +- **v1.12.0** — Quality Scale conformance: runtime_data, test-before-setup, unloading, + single_config_entry, Store migrations hook, diagnostics, repairs, system health, + uninstall cleanup, HA-harness tests in CI, quality_scale.yaml. +- **v1.13.0** — universality: floors-import wizard, editable icon rules (+device_class + fallback), tap actions with a security model, i18n dictionaries in JSON, light-theme pass. +- **v1.13.1** — distribution: synthetic-home demo GIF in the README, issue templates, + CONTRIBUTING.md, Discussions. Forum/Reddit drafts are in the user folder + (`posts_drafts.md`) awaiting manual posting. +- **v1.13.2** — audit round 3: buildDevices unit-test suite, multi-placeholder t(), + conflict resync in _saveConfigNow, pointercancel long-press fix, repairs re-check + on config save (repairs.py). +- **v1.13.3** — privacy: legacy real-house assets/ removed; README screenshots synthetic. +- **v1.14.0** — per-space display settings (borders/names/color/opacity/fills), + draggable room labels, hand-drawn spaces (no image required), demo/ harness in-repo, + docs/TESTING.md manual checklist (update with every functional change!). +- **v1.15.0** — temperature room fill (blue/green/yellow) with editable comfort bounds. +- **v1.15.1** — display-settings UX: radio fill selector, inline compact bounds + (with the Number('')→0 bound-collapse bug fixed), avg room temp in the tooltip, + darken-on-hover, wider space dialog. +- **v1.15.2** — fix: average room temperature (fill + tooltip) counted non-thermometers + (fridges/TRVs/chip `*_device_temperature`/diagnostic); now thermometer/air-monitor only. +- **v1.15.3** — fix: device icon badge sat 1 px off its anchor (content-box + 1 px + border); `box-sizing: border-box` centres it exactly on the device point. +- **v1.15.4** — fix: real `ha-icon` (block + big line-height) put the glyph ~1.8 px low; + `.dev ha-icon` is now a zero-line-height flex box. Reverted v1.15.3 border-box (shrank the + badge). Verified live. Demo stub made faithful so the smoke guards it. +- **v1.15.5** — fix: room hover was always grey; legacy overlay/yard hover rules scoped + with `:not(.styled)` so filled rooms darken their fill, unfilled ones grey. +- **v1.15.6** — room hover also reveals the border (stroke colour kept, hidden via + opacity) even when borders are off. +- **v1.16.0** — NEW read-only `houseplan-space-card` (static single-space schematic, + pointer-events:none, deep-link button) + `#space=` deep-link in the full card; shared + space-geometry/space-render + module-level config cache (config-store). +- **v1.16.1** — space-card renders room fills as configured on the full card (snapshot), + no longer omitted; +shared areaLqi(). +- **v1.17.x** — entity markers get auto icon/temp (issue #1); correct resource URL documented + (issue #2); humidity badge (gated on device_class, not the icon). +- **v1.18.x** — live ruler while drawing rooms (metres / feet+inches) + per-space scale + `cell_cm`; visibility fix (the badge lived in the markup-hidden devlayer). +- **v1.19.0** — a line is never an entity of its own: walls derived from room outlines + (`roomEdges`), unfinished contours persist nothing, Erase tool removed, `space.segments` + stripped on save. +- **v1.20.0** — rooms may not overlap (strictly-inside clicks refused, overlapping contours + refused at close; shared walls stay legal). +- **v1.21.x** — merge & split rooms (boolean geometry via polyclip-ts; merge = union collapses + to one hole-free outline; split = wall-to-wall chord, bigger part keeps identity) + UX fixes. +- **v1.22.0** — presence ripples (badge/ripple/icon_ripple + colour/size, `isActiveState`), + per-device icon size/rotation (`--dev-size`), one-click HACS badge. NOTE: sources were lost + in a sandbox reset after deploy and restored from conversation patches — push immediately + after building, never wait for verification. +- **v1.23.0** — doors & windows: "Opening" markup tool (snap onto derived walls, absolute + coords), length in real cm, contact sensor + lock, animated leaf/arc, padlock badge, status + card; lock never toggled from the plan. +- **v1.23.1** — openings UX: hover outline + grab cursor, drag along walls (angle normalized + to [-90,90) so the hinge never flips), click=status / double-click=properties, thicker hit + strip. Release v1.23.1 published on GitHub (covers v1.22.0–v1.23.1). diff --git a/docs/STATUS.md b/docs/STATUS.md index b5c95062..bafe85bc 100644 --- a/docs/STATUS.md +++ b/docs/STATUS.md @@ -17,11 +17,28 @@ change must pass through a published beta/RC before stable. Stable release commits are promotion-only (versions, generated bundles and release/changelog metadata). Only an explicit owner-approved emergency hotfix may skip this gate. -## Snapshot (2026-09-23) +## Snapshot + +Everything computable from the tree and git; regenerate, never edit by hand +(#634). Workflow status is not here — it lives only in issue labels. + + +| Item | State | +|---|---| +| Generated | 2026-09-24 — rerun `node scripts/status-snapshot.mjs` for the current tree | +| Version | **1.78.0-beta.1** in all 7 version sources (`scripts/release-contract.mjs`) | +| Latest stable tag | `v1.77.0` | +| Latest prerelease tag | `v1.78.0-beta.1` | +| Tests | Node unit 2868 · pure backend 381 · HA-harness backend 292 · browser smokes 263 (`npm run inventory`) | + + +## Standing state and decisions + +Prose kept by hand: decisions and the state they explain. Update a row in the +same commit as the change it describes. | Item | State | |---|---| -| Version | **v1.78.0-beta.1** everywhere (manifest, const.py, package.json, package-lock in both places, CARD_VERSION in the card and the editor runtime) | | Current local cycle | **Beta v1.78.0-beta.1 candidate** — prepared from the exact integrated `dev` tree. It combines settings-dialog reliability and validation (#607, #608, #609, #610, #614), safer import/export and storage (#611, #625), read-only and touch fixes (#612, #613), the dishwasher palette icon (#640), and the associated validation/release infrastructure. `main` remains on stable v1.77.0. | | Hidden Alpha Stage | #89 Stage 1 ships in v1.63.0-beta.1, #122 Stage 2 in v1.64.0 and #160 Stage 3 in v1.73.0-beta.1; #570 adds the Stage 4 designer handoff and #583 refines it on `dev`: only the building ambient shadow remains, door/gate volumes pivot on the selected host face without strokes, and devices/lock badges receive deterministic mutual collision correction while room labels remain passive below them. The same hidden `iso` view uses a fixed 0°/20° camera and scale-aware wall height 84, low screen-facing device/room/lock overlays without tethers or ground dots, neutral full-height window frames with blue glass, and bounded theme materials. Since #448 the experiment is enabled only through the single indefinite browser-local `hp_alpha` gate; it is not expiring and has no per-stage key. Flat remains default; editors, `houseplan-space-card`, floor effects, stored coordinates and HA actions remain unchanged. Stage 4 stays internal and is absent from public changelog/user documentation. | | Workflow | Superseded 2026-08-12: the pre-1.62 rule of "local edits without tests or commits" is **dead** — since release 1.62 every product change follows `PROCESS.md` (issue in `S5-ready`+, branch `issue/-slug`, trailers on every commit, review pipeline; `AGENTS.md` is the summary). Release mechanics below remain current. A requested pre-release gets a production build plus the smallest targeted unit/smoke set covering the changed surfaces, one tested `dev` commit/tag and a GitHub Release with `prerelease=true`; `main` stays untouched. The complete local frontend/backend/smoke gate runs only before a stable release, after which `main` is fast-forwarded to the exact tested `dev` SHA and the stable release is produced by `release.yml` (`workflow_dispatch` on `main` with the tag) — the only publisher of installable assets since #540: gates on the exact SHA (Validate, Full Performance, E2E on the candidate commit), one build, `houseplan.zip` archived from the committed tree, `SHA256SUMS`, draft → publish → read-back verification; a release published by hand in the GitHub form is turned back into a draft and walked through the same path, and a re-dispatch on a public tag is a repair that adds only missing assets. Release bodies are short and bilingual (Russian first); every bullet links its GitHub issue (#NN) so the #328 rules stay machine-checkable. A STABLE body aggregates the changelog since the PREVIOUS STABLE release (never since the last beta): features/fixes described across the line's beta changelogs must appear, while bugs that were introduced and fixed strictly inside the beta line (never shipped in any stable) are excluded — draft with `npm run release:notes -- `, curate by hand, then `npm run release:notes -- --verify` must pass. `Мелкие исправления и улучшения` / `Small fixes and improvements` is allowed only when the range really contains user-visible work not itemised in the body; a single-issue hotfix ships without it (the verifier enforces this). Every body ends with separate links to the Russian and English changelogs. Open or partially delivered issues are never presented as shipped. Telegram announcements are sent only for stable releases; beta and RC publication is silent. `docs/RELEASE-NOTES.md` is the current canonical body instance; `npm run release:prerelease -- --issues=… --yes` is the primary local publication path and the manual `Publish prerelease` workflow is its GitHub-only equivalent once present on `main`. Nothing is copied to the home instance by hand | @@ -39,253 +56,10 @@ metadata). Only an explicit owner-approved emergency hotfix may skip this gate. | Community | **Telegram chat: https://t.me/ha_houseplan** (created 2026-07-27) — the primary user-facing support channel; GitHub issues stay for bugs/features. Link it from any new release notes and posts | | Product scope | `docs/SCOPE.md` is the feature guard rail; `docs/TOUCH-SUPPORT.md` is the input-support contract — check both before accepting interaction work | -## Current feature surface (since the 2026-07-17 snapshot) - -- **Configurable summary overlay** (#437, merged into `dev`): a two-part - View control opens one adaptive form or toggles a browser-local preference. - Shared ordered blocks can show readable HA entity states and three system - values; local icon/text scaling applies to ordinary and kiosk View without - changing editor geometry. The overlay is a screen-space layer on the right - or bottom, respects native HA mobile mode, and temporarily hides in a small - card. Shared saves remain revision-checked; read-only and kiosk users receive - only the local controls. - #505 aligns its split control, compact overlay and wide settings dialog with - the designer reference. Both entry and exit animate; size controls are absent - from this form without changing saved scales, and mobile visibility is - disabled under draft local-off. The GitHub issue is the source of workflow - status; `docs/design/505-summary-panel/` holds the reference and visual evidence - instructions, not a separate backlog. -- **Presence radars (#485 Stage 1, merged into `dev`):** an optional marker-owned, - change-aware v1 configuration binds exact HA sources for recognized ESPHome - LD2450 or explicit Cartesian, polar, range, zone-state and presence-only - profiles. The backend owns freshness, pairing, projection, room clipping, - ACL-filtered bounded frames and teardown. The lazy device editor owns source - inspection and physical two-point setup; View receives only normalized live - frames. Raw observations, calibration samples and the short target trail are - session-only and excluded from config, exports and support data. - -- **Primary sidebar panel + optional cards** (#486): the integration registers - `/houseplan` for every signed-in user after backend initialization. Its own HA - app bar wraps the same `houseplan-card`, so spaces, permissions, actions and - editor lifecycle stay shared; only the duplicated product title and outer - dashboard-card chrome are removed. Failure to register the panel is isolated - and reported in System Health. The optional full dashboard card requests full - width in Sections by default; explicit HA sizing remains authoritative. -- **Three editors + View**: Plan / Devices / Background (decor layer v1.33) as - tabs with an X to close; View is the default; only the last space persists, - while reload/return from another HA route always starts in View (#93). - **Kiosk mode** (v1.41.0): `kiosk: true` — no - header/editors, swipe between spaces, double-tap zoom reset, `cycle: N` - carousel, per-screen size multipliers in localStorage. Discrete wheel, - button, fit/home and kiosk-reset camera commands use one short retargetable - transition; direct pan/pinch and reduced motion remain immediate (#82). -- **Independent Glow overlay** (#55/#19, refined locally by #61/#65–#67): dark-room - base is used only when the effective fill resolver returns `null`, including - dynamic modes without usable data; resolved LQI/light/temp/custom colors keep - their exact alpha while per-source pools remain visible. Pools use - additive screen blending only after a cached real-pixel browser probe and - otherwise fall back to normal composition; legacy `fill_mode: glow` remains - losslessly readable/migratable. Marker roles are Auto/Always/Never; colour - can remain live, be overridden with live brightness, or be fixed together - with brightness. One perceptual alpha resolver gives every source the alpha at - the centre of its pool; `GLOW_FALLOFF` then spends it over the whole radius. - Per-source radius, doorway sight lines and transitive open boundaries remain — - all three now fall out of the visibility region instead of separate layers. -- **Custom room fill** (#56, space UX refined locally by #64): the space dialog - uses persistent user color/opacity as its ordinary first/default fill instead - of a redundant None option; a room keeps None as an explicit inherited-fill - suppression. Effective inheritance is room → space → safe documented default; - room reset restores space inheritance, and full/static renderers share the - same projection. -- **Universal device toggle** (#94, v1.62.0-beta.3): one `Toggle state` option is visible - for every marker and resolves the exact binding, functional device role or - explicitly configured controls. Hint, click, confirmation and cover - presentation share one result; partial groups call only the shown available - subset, stale controls never fall back, secure targets are no-op, and legacy - `cover`/absent light defaults round-trip losslessly. **Lights toggle by - default** (v1.39); other devices still default to the House Plan card. -- **Contextual Zigbee topology** (#54, refined by #457 and #464 on current dev): an - opt-in admin-only mouse hover shows direct neighbours without scanning. A - deterministic per-provider uplink tree now adds arrows towards the - coordinator while keeping non-route neighbour lines; a short bubble names a - remote space or explains that the next device/coordinator is not on the plan. - Active topology is above room names and unrelated markers, exact local - endpoints remain above it, and unknown-LQI dashes have a dark contrast - casing. Touch, kiosk, editors and the static card remain unchanged. -- **Plan geometry**: polyline split (v1.32), island rooms w/ evenodd holes - (v1.34), smart guides + 45° angle badge (v1.40), opening hover preview. - Openings support doors, windows and compact wide gates; gates retain door - contact/lock/Glow semantics but use two leaves opening only 10° outwards. -- **Canonical wall model** (#282, #306, #478, current dev): Walls and Thickness - accept `0..100 cm` for contour and independent segments. Model v10 - migrates legacy virtual spans into stable `cm:0` atoms; a per-space - dashed/solid selector controls both line style and Glow/sun transmission. - It also converts persisted unfinished contours to ordinary partitions; new - wall-chain segments are partitions from the first accepted click and chain - state is session-only. A finished chain losslessly merges/reconciles its own - seed component and finished-chain Undo/Redo restores the same fixed point; - room Delete/Merge owns direct and vacuum room-reference cleanup (#477). - Zero walls have no body, area or opening host, and - there is no Boundary tool. -- **Rooms**: room cards with metrics (temp/hum/lqi/light "1 of 3") and - proportional resize (v1.31); link icon to the HA area (v1.40.1, room taps - removed). **New-device red dot** (v1.29), lock action button (v1.30). -- **Dialog UX**: binding radios + entities checkbox + search dropdown - (v1.38.0); tap actions simplified to Device card / more-info / Toggle, - right-click → more-info (v1.38.1); Esc closes every dialog (v1.30.4). - Since #607, rejecting the real HA close button after an unsaved-changes - prompt explicitly reconciles the nested modal before reopening it; the same - Device, Room, Space or General-settings draft remains interactive. #609 keeps - every shared settings form on the reviewed 560 px canvas and single HA-owned - scroller in the authentic Home Assistant branch; at 480 px and below the - form is edge-to-edge fullscreen, while generic dialogs retain their previous - sizing. -- **Room settings, tier 3** (v1.42.0): per-room fill/temp-source/label sizes; - the settings button sits at the room's VISUAL centre (inscribed circle + - centroid pull), icon-derived size, zooms with the plan (v1.51.0). -- **Files & plans** (v1.44–v1.50): signed content urls with sandbox CSP, - copy-on-write plan files, "already uploaded" picker + explicit delete - (v1.47.0), store quotas instead of any age-based deletion (v1.49.0), - nothing is ever deleted on an inference (docs/SCOPE.md rule). -- **Square canvas** (v1.48.0) with a crash-safe two-store migration - (geom_pending, v1.50.0) and an explicit geometry/repair command (v1.50.1); - content-fit default zoom with devices as content, zoom out to 0.4×, - measured stage height (v1.49–v1.50.2). -- **Explicit hide flags** (v1.51.0, docs/FILTERING.md): per-device - `marker.hidden` seeded once from the old filter and controlled by the - bottom-left "Hide" / "Show" action in the device dialog; local - "Show hidden" ghosts; hidden counts toward room LQI on both cards, casts - no light (v1.51.1). -- **True plan deletion** (v1.60.0-beta.1, 2026-08-07): confirmed Delete beside Hide/Show; - a minimal `marker.removed` tombstone prevents auto-rediscovery but exposes - the binding to Add. Deleted devices are absent from every plan aggregate and - linked marker presentation; layout/files/trails are cleaned and stale layout - writes are rejected. Exact contact/lock references owned by architectural - openings remain active without restoring the standalone marker; live text - and other marker controls retain the older re-add-to-reactivate contract. -- **Yellow = working right now** (v1.51.0): climate uses `hvac_action` when - available and falls back to a current advertised non-off HVAC mode only when - the integration omits the action; service switches can no longer become - primary, and glow pool and icon share one condition. Editor gestures on touch - (pinch/pan) landed the same release. -- **Unified device status/activity** (v1.59.0-beta.10; pulse pipeline #98 local): four display - modes (Icon + state / Icon + state and activity / Value + state / Always static icon), - one semantic resolver for yellow - actual work, orange open/unlocked, unavailable and always-red alarms; - activity projects to three finite waves for a short event or one continuous - pulse for presence, mechanical travel and running. Always-static deliberately suppresses every state-driven visual, - satellite badge and live vacuum overlay while leaving hover, actions, Glow and - controls intact. Legacy Ripple-only migrates to Icon + activity on the next save. -- **Passive media lifecycle** (v1.60.0-beta.1): every `media_player`, - regardless of model, stays neutral while powered or playing and fades to - the existing unavailable appearance on explicit `off`; transport playback - is no longer classified as yellow actual work and no new visual state is - introduced. -- **Unified Background editor** (v1.60.0-beta.1): typed decor model, - physical cm/in strokes and text size, independent contour/fill opacity, decor+room smart - magnet, common selection/resize/rotate frame for every decor kind, numeric - geometry properties, and shared 50-command Undo/Redo. The plan image now has - an exclusive tool, 0.5 editor de-emphasis elsewhere, independent axes, - rotation, numeric properties and rotated content bounds. Legacy `width` and - `plan_scale` remain read-compatible and migrate only through explicit plan - optimisation. Furniture properties also expose the symbol itself. See - `DECOR-EDITOR.md`. -- **v1.59.0-rc.1** (2026-08-06): whole-plan lossless optimization with an - atomic config+layout commit and safe undo; the yellow actual-work plate is - retained alongside source glow; all Background objects have Select-mode - double-click properties; View hover highlights every room and reports its - clean-floor area. Audit fixes add eager activity baselines/source resets, - lossless legacy live-text editing, exact wall-fragment endpoints/compaction, - editor-visible virtual walls and prerelease-discovery reporting in CI. -- **v1.59.0-rc.2** (2026-08-06): Plan actions have one meaning each; Room - outline names the closed-contour tool; one named 50-command Undo/Redo stack - covers committed plan geometry; positional placement is always grid-bound. - Glow and toast overlays no longer steal room/tool pointers, View hover covers - shared thick walls, device Hide/Show is explicit, the static no-op aspect - field is gone, and the new user guide/product audit replaces archived docs. -- **v1.59.0** (2026-08-06): The stable 1.59 line includes all beta/RC work. - Room hover follows clean-floor wall faces, including nested contours and - projected opening gaps. Thick-wall rendering unions each room's own wall - ring, so one room's floor cannot erase another room's wall or leave white - slivers at complex crossings. -- **v1.59.1** (2026-08-06): device markers resolve a semantic entity role - instead of trusting registry order; one light-source resolver now drives - Glow, Light fill, room statistics, marker feedback and controls. Glow uses - 0.7 opacity. Current dev keeps external `controls` non-spatial: they still - drive group state, but only a real lamp marker or explicit `is_light` marker - can place a Glow pool. Compacted real walls retain their correct inner face - and body at real/virtual T-junctions. -- **v1.59.2** (2026-08-07): every modal uses the shared `hp-dialog` shell, - backed by Home Assistant's `ha-dialog` with a native demo fallback. Titles, - modal semantics, initial focus, focus trapping, Escape and restore focus are - consistent across all editors, nested dialogs and dialog replacement flows. - -## Recent milestones (details in CHANGELOG.md) - -- **v1.10.0** — audit & refactor: asyncio.Lock around all store writes (race fix, atomic - `expected_rev`), point `layout/update` instead of full `layout/set` (anti last-writer-wins), - new `layout/delete`, `safeUrl()` XSS guard, `fetchWithAuth`, KEY_HASS, streaming upload cap, - card split into modules (`styles.ts` / `types.ts` / `devices.ts`), dynamic spaces in the GUI - editor, dead code removed. -- **v1.11.0** — full English translation + en/ru UI localization. -- **v1.11.1** — brand images inside the integration; CI fully green for the first time. -- **v1.11.2** — Description textarea fix in the device dialog. -- **v1.12.0** — Quality Scale conformance: runtime_data, test-before-setup, unloading, - single_config_entry, Store migrations hook, diagnostics, repairs, system health, - uninstall cleanup, HA-harness tests in CI, quality_scale.yaml. -- **v1.13.0** — universality: floors-import wizard, editable icon rules (+device_class - fallback), tap actions with a security model, i18n dictionaries in JSON, light-theme pass. -- **v1.13.1** — distribution: synthetic-home demo GIF in the README, issue templates, - CONTRIBUTING.md, Discussions. Forum/Reddit drafts are in the user folder - (`posts_drafts.md`) awaiting manual posting. -- **v1.13.2** — audit round 3: buildDevices unit-test suite, multi-placeholder t(), - conflict resync in _saveConfigNow, pointercancel long-press fix, repairs re-check - on config save (repairs.py). -- **v1.13.3** — privacy: legacy real-house assets/ removed; README screenshots synthetic. -- **v1.14.0** — per-space display settings (borders/names/color/opacity/fills), - draggable room labels, hand-drawn spaces (no image required), demo/ harness in-repo, - docs/TESTING.md manual checklist (update with every functional change!). -- **v1.15.0** — temperature room fill (blue/green/yellow) with editable comfort bounds. -- **v1.15.1** — display-settings UX: radio fill selector, inline compact bounds - (with the Number('')→0 bound-collapse bug fixed), avg room temp in the tooltip, - darken-on-hover, wider space dialog. -- **v1.15.2** — fix: average room temperature (fill + tooltip) counted non-thermometers - (fridges/TRVs/chip `*_device_temperature`/diagnostic); now thermometer/air-monitor only. -- **v1.15.3** — fix: device icon badge sat 1 px off its anchor (content-box + 1 px - border); `box-sizing: border-box` centres it exactly on the device point. -- **v1.15.4** — fix: real `ha-icon` (block + big line-height) put the glyph ~1.8 px low; - `.dev ha-icon` is now a zero-line-height flex box. Reverted v1.15.3 border-box (shrank the - badge). Verified live. Demo stub made faithful so the smoke guards it. -- **v1.15.5** — fix: room hover was always grey; legacy overlay/yard hover rules scoped - with `:not(.styled)` so filled rooms darken their fill, unfilled ones grey. -- **v1.15.6** — room hover also reveals the border (stroke colour kept, hidden via - opacity) even when borders are off. -- **v1.16.0** — NEW read-only `houseplan-space-card` (static single-space schematic, - pointer-events:none, deep-link button) + `#space=` deep-link in the full card; shared - space-geometry/space-render + module-level config cache (config-store). -- **v1.16.1** — space-card renders room fills as configured on the full card (snapshot), - no longer omitted; +shared areaLqi(). -- **v1.17.x** — entity markers get auto icon/temp (issue #1); correct resource URL documented - (issue #2); humidity badge (gated on device_class, not the icon). -- **v1.18.x** — live ruler while drawing rooms (metres / feet+inches) + per-space scale - `cell_cm`; visibility fix (the badge lived in the markup-hidden devlayer). -- **v1.19.0** — a line is never an entity of its own: walls derived from room outlines - (`roomEdges`), unfinished contours persist nothing, Erase tool removed, `space.segments` - stripped on save. -- **v1.20.0** — rooms may not overlap (strictly-inside clicks refused, overlapping contours - refused at close; shared walls stay legal). -- **v1.21.x** — merge & split rooms (boolean geometry via polyclip-ts; merge = union collapses - to one hole-free outline; split = wall-to-wall chord, bigger part keeps identity) + UX fixes. -- **v1.22.0** — presence ripples (badge/ripple/icon_ripple + colour/size, `isActiveState`), - per-device icon size/rotation (`--dev-size`), one-click HACS badge. NOTE: sources were lost - in a sandbox reset after deploy and restored from conversation patches — push immediately - after building, never wait for verification. -- **v1.23.0** — doors & windows: "Opening" markup tool (snap onto derived walls, absolute - coords), length in real cm, contact sensor + lock, animated leaf/arc, padlock badge, status - card; lock never toggled from the plan. -- **v1.23.1** — openings UX: hover outline + grab cursor, drag along walls (angle normalized - to [-90,90) so the hinge never flips), click=status / double-click=properties, thicker hit - strip. Release v1.23.1 published on GitHub (covers v1.22.0–v1.23.1). +The feature surface since the 2026-07-17 snapshot and the early release +milestones moved to [`STATUS-FEATURES.md`](STATUS-FEATURES.md) (#634): they +are reference, not session entry. New feature-surface bullets go there, in the +same commit as the behaviour. ## Where things live diff --git a/docs/TESTING-DEMO.md b/docs/TESTING-DEMO.md index 0ea6659b..079c0223 100644 --- a/docs/TESTING-DEMO.md +++ b/docs/TESTING-DEMO.md @@ -9,8 +9,9 @@ > долгоживущие проверки («файл исчез через сутки», «пережило рестарт») на стенде > не живут. Рестарт HA изнутри заблокирован demo_guard. > -> Источник истины — [TESTING.md](TESTING.md): формулировки пунктов и их -> `[auto:…]`-маркеры смотреть там. Здесь каждый пункт сокращён до сути и дополнен +> Источник истины — [TESTING.md](TESTING.md) и его приложения +> [`testing-notes/`](testing-notes/README.md) (ручной чек-лист по поверхностям +> перенесён туда, #634): формулировки пунктов и их `[auto:…]`-маркеры смотреть там. Здесь каждый пункт сокращён до сути и дополнен > строкой «Стенд: …» — где и как ткнуть именно на демо-стенде. > > **Демо-дом v2** (анонимированная планировка реального загородного дома): diff --git a/docs/TESTING.md b/docs/TESTING.md index 741c42ab..a395c0f4 100644 --- a/docs/TESTING.md +++ b/docs/TESTING.md @@ -1,30 +1,12 @@ -# Manual testing checklist +# Testing -## Вопрос о несохранённых настройках (#610) - -- [ ] В русской локали четыре формы настроек показывают **Вернуться** и - **Не сохранять**: первая кнопка сохраняет черновик, вторая закрывает - форму без сохранения [auto: `demo/smoke_discard_copy.mjs`]. -- [ ] Заголовок и кнопка потери черновика используют - `mdi:content-save-off-outline`; обычный warning без override сохраняет - иконки открытого замка [auto: `demo/smoke_discard_copy.mjs`; mutation: - `discard-confirm-action-icon-falls-back-to-lock`]. -- [ ] RU/EN/DE/FR на 320–640 px, light/dark и DPR 1/2 сохраняют одну строку, - полный текст, безопасный autofocus и возврат к форме - [auto: `demo/smoke_dialog_polish_603.mjs`]. - -## Числовые поля со слайдером (#608) - -- [ ] В настройках комнаты поле размера имени принимает `120` посимвольно: - до выхода из поля черновик и слайдер остаются на прежнем значении, после - выхода оба показывают 120; очистка поля возвращает последнее - подтверждённое значение [auto: `demo/smoke_range_line_draft.mjs`]. -- [ ] В настройках пространства `123` становится 125 только после завершения - ввода; движение слайдера во время незавершённого ввода отбрасывает текст - и сразу синхронизирует поле [auto: `demo/smoke_range_line_draft.mjs`, - `demo/smoke_dialog_config_parity.mjs`]. -- [ ] Возврат раннего clamp на каждом `input` делает свидетель красным - [mutation: `range-line-clamps-every-keystroke`]. +Действующая инструкция: правила для новых тестов, гейты, локальный набор и +матрицы релизной проверки. Ручные чек-листы по поверхностям и приложения по +отдельным issue перенесены дословно в [`testing-notes/`](testing-notes/README.md) +(#634) — индекс там же, по заголовку и номеру issue. Новый чек-лист по задаче +ложится в приложение своей подсистемы; сюда — только то, что действует для +любой задачи. Маркер `[auto: …]` у пункта в любом из этих файлов обещает +падающую проверку в том же коммите (`docs/DEVELOPMENT.md`). ## Правила для новых тестов (issue #85) — обязательны @@ -133,988 +115,6 @@ job-минут и в основном отменялись следующим п что отменённый пуш или таймаут не пропадают даром. Полный прогон и `--id` журнал не читают; `--ledger` без `--changed` — ошибка. -## E2E на реальном Home Assistant (#514) - -Репозиторий `Matysh/houseplan-e2e`: настоящий HA в docker, House Plan из -релиза или из дерева коммита, 13 сценариев Playwright (боковая панель, -дашборды, роли, телефон, PDF, рестарт HA, первый запуск, обновление). Ночью — -по расписанию на последней бете; для **стабильного** релиза `release.yml` -запускает его на **SHA кандидата** (`scripts/e2e-gate.mjs --ref=`, #540: -ставится `custom_components/houseplan` из tarball коммита — то же дерево, из -которого `git archive` строит `houseplan.zip`) и ждёт зелёного: красный — -релиз остаётся черновиком, ассеты не публикуются. В цикле разработки и на -бетах не участвует. - -## Версия в кадрах и попиксельная приёмка (#512) - -Golden-кадры и скриншоты документации не должны меняться от bump версии. Для -golden отображаемая версия идёт через seam `displayVersion()` (`src/card-version.ts`, -глобальная `__HP_VERSION_OVERRIDE__` — только для харнесов; продукт её не задаёт), -и харнес фиксирует `0.0.0-golden`. Для docs-скриншотов есть локальная приёмка -`npm run docs:accept -- --identical`: кадры сравниваются по декодированным -пикселям, и при полном совпадении обновляется только отпечаток исходников. - -## Manifest входов: какие job запускать и что хешировать (#492) - -Один модуль, `scripts/check-inputs.mjs`, объявляет каждую проверку Validate -(`preflight`, `frontend`, `changed_mutants`, `integration`, `smoke`, `golden`, -`performance_smoke`, `backend`) через корни и точки входа, а остальное -вычисляет: от точек входа берётся замыкание — импорты транзитивно, строковые -пути как листья, каталог по строке — все текстовые файлы под ним. Из этого -manifest читают и `classify-changes.mjs` (job `changes`: job запускается, -если дифф задел хотя бы один её вход), и `gate-reuse.mjs` (ключ реюза = -хеш содержимого всех входов job). Два места не могут разойтись: до #492 у -бэкенда ключ не знал relay, converter и schema, а golden/perf — `serve.mjs`, -`demo.html` и compat-хелперов. - -Правила, которые стоит знать: - -- `validate.yml` — вход toolchain каждой job: правка workflow гоняет всё; -- `src/**` — вход только браузерных job, кроме точных cross-runtime входов - `src/plan-optimizer.ts` и `src/logic.ts`, которые backend pytest читает для - parity-контрактов; `demo/fixtures/large-house.mjs` и - `demo/fixtures/visual-matrix.mjs` так же явно входят в backend, потому что - pytest запускает их через Node dynamic import. Точные исключения не делают - весь UI или каталог fixtures входом backend (#542); backend от остального UI - не зависит, а - `custom_components/houseplan/manifest.json` (версия) держит правило «кандидат - релиза прогоняет всё» и для него; -- **неизвестный исполняемый вход** — файл под `scripts/`, `demo/`, `test/`, - `tests_backend/`, `.github/`, `custom_components/`, `src/`, которого нет в - manifest ни одной проверки, — расширяет прогон до полного набора и называется - в summary. Лист покрытия (`node scripts/check-inputs.mjs --coverage`, - `test/check-inputs.test.mjs`) требует, чтобы каждый такой файл был чьим-то - входом либо стоял в `NOT_AN_INPUT` с причиной: новый скрипт без записи — - красный юнит, не вечное расширение прогонов; -- **overlay принятых эталонов** (`demo/golden/baselines/**`, #573) — вход - только `golden`, у которой он стоит явным корнем. Раскрытие каталога по - строке его не выдаёт: корпус отпечатка называет `demo/golden` каталогом, - но берёт из него только `*.mjs`, а до #573 индекс эталонов через это - раскрытие становился входом smoke, perf и 181 из 183 браузерных - свидетелей — приёмка 13 кадров на beta.3 (`ad4000f9`) сменила их ключи и - отпечатки, и второй полный Validate повторил 22 минуты уже сделанной - работы. Явная ссылка на файл индекса (как в `test/check-inputs.test.mjs`) - входом остаётся; -- `--check=` печатает входы, `--why=<файл>` — цепочку, по которой файл - стал входом. - -Отрицательные пробы (`test/gate-reuse.test.mjs`, `test/classify-changes.test.mjs`, -`test/check-inputs.test.mjs`) держат представителей каждой категории входов и -обратную пробу для UI ↔ backend; мутанты `manifest-drops-workflow-input`, -`classify-unknown-input-is-unaffected`, `reuse-backend-hashes-ui`, -`backend-dynamic-inputs-dropped`, `baseline-overlay-leaks-into-every-key`, -`guard-inputs-ignore-wrapper-defaults`, `registry-diff-not-selected`, -`merge-pushes-unvalidated-candidate`, `merge-ignores-lease-rejection`, -`nightly-does-not-wait` держат сам протокол. - -Чистые Python-контракты канонизации, которым не нужен Home Assistant, находятся -в `tests_backend/test_coordinate_canonicalization_pure.py`. Они обязаны реально -исполняться в локальном `pytest tests_backend`, а не исчезать за module-level -`importorskip`; schema/store/virtual-light проверки остаются в HA-зависимом -`test_coordinate_canonicalization.py`. Поведение static-card capability -доказывается unit/smoke-счётчиками и состоянием runtime, а не regex по исходнику -(#440). - -Без установленного Home Assistant (нативная Windows, песочница) HA-харнесс -`tests_backend/test_ha_*.py` **не собирается вовсе** — `conftest.py` исключает -эти файлы через `collect_ignore_glob`, поэтому их нет ни в `passed`, ни в -`skipped`, и зелёная итоговая строка про них ничего не говорит. С #630 pytest в -таком прогоне печатает в шапке и в итоге строку `HA harness NOT collected: N -test_ha_*.py files (M tests)` со ссылкой на канон — Linux CI или WSL -(`bash scripts/wsl-setup.sh --verify`). N и M считаются при каждом запуске по -тому же glob (объявления `def test_*`/`async def test_*`, без размножения -параметризацией), в документах их не переписывают. Проверка — -`tests_backend/test_conftest_harness_notice.py`, мутанты -`ha-harness-notice-silent` и `ha-harness-notice-count-frozen`. - -## PDF export polish (#482) - -- [ ] Нормализация контура сначала схлопывает соседние и шовные дубли в - физическом допуске 1 мм и только затем удаляет коллинеарные точки; ложная - диагональная хорда из почти совпавших вершин не появляется - [unit: `test/pdf-dimensions.test.mjs`]. -- [ ] Размеры выводятся только для горизонтальных/вертикальных граней с - каноническим допуском 0,25°. Одна локальная пара противоположных граней - комнаты или связного наружного кольца получает одну подпись; одинаковые - длины в разных комнатах, осях и несвязных кольцах сохраняются - [unit: `test/pdf-dimensions.test.mjs`, `test/pdf-scene.test.mjs`]. -- [ ] Подписи центрированы на своей стене и сдвигаются от кладки целыми - размерными дорожками без касательного джиттера; вогнутые комнаты выбирают - внутреннюю нормаль по геометрии, а не по центроиду. Bbox текста и все - сегменты полосы проверяются точными пересечениями, включая отверстия, - вырожденные отрезки и разные направления ring - [unit: `test/pdf-collision.test.mjs`, `test/pdf-scene.test.mjs`, golden: PDF scene]. -- [ ] Стены, перегородки и колонны имеют точную заливку `#7f7f7f`, общую - привязанную к странице штриховку 45° с шагом 3 мм и чистые even-odd - вырезы всех проёмов; стены нулевой толщины не получают штриховку - [unit: `test/pdf-writer.test.mjs`, `test/pdf-scene.test.mjs`, golden]. -- [ ] Ориентация и первый подходящий стандартный масштаб выбираются по bbox - полной сцены вместе с размерами, выносками, декором и растром. Вся сцена - центрирована в доступном поле листа с допуском 0,5 мм; тяжёлая геометрия - пространства вычисляется один раз - [unit: `test/pdf-layout.test.mjs`, `test/pdf-scene.test.mjs`]. -- [ ] Векторный компас правильно поворачивается для 0/90/180/270°, а текст PDF - не содержит удалённой легенды `wall · door · window` - [unit: `test/pdf-compass.test.mjs`, `test/pdf-scene.test.mjs`]. -- [ ] Реальный диалог при ширине View 320 px не имеет горизонтального scroll, - действия остаются внутри окна, имеют высоту не менее 44 px и на узком - экране идут вертикально; сохранение PDF работает во всех четырёх локалях - [auto: `demo/smoke_pdf_export.mjs`]. -- [ ] Каждый поведенческий барьер выше защищён соответствующим мутантом, а - обновлённый PDF golden принят только после визуальной проверки Linux CI - [mutation: `scripts/mutation-gate.mjs`; golden: `demo/golden/`]. - -## Многоэтажный робот: карты и пространства (#162) - -- [ ] Чистый резолвер разбирает все шесть исходов на общей фикстуре и не - зависит от порядка списка маршрутов; усыновление легаси-прогона даёт - ровно три исхода, оба отрицательных — fail-closed - [unit: `test/vacuum-routes.test.mjs`, `tests_backend/test_vacuum_routes.py`, - фикстуры `test/fixtures/vacuum-routes/*.json`]. -- [ ] Живой оверлей рисуется в пространстве активного маршрута, док остаётся в - своём; прошлый прогон виден в пространстве СВОЕГО маршрута, а легаси-конфиг - сохраняет прежнее правило [unit: `test/vacuum-routes.test.mjs`]. -- [ ] Рекордер подписывается на источники всех маршрутов и пишет прогон под - маршрутом, который его породил; смена маршрута начинает новый прогон даже - при том же `map_id` - [backend: `tests_backend/test_trail_recorder.py`, `test_trails.py`]. -- [ ] Правка маршрутов проверяется семантически на `config/set`, optimize и обоих - импортах, нетронутые легаси/будущие данные не блокируют чужое сохранение - [backend: `tests_backend/test_vacuum_route_validation.py`]. -- [ ] Удаление пространства называет число чужих карт и уносит только их - маршруты; экспорт одного пространства отбрасывает кросс-пространственные - маршруты и считает их в `dropped_marker_links` - [unit: `test/space-deletion.test.mjs`, backend: `tests_backend/test_ha_import_export.py`]. -- [ ] Восемь мутантов краснеют: `vacuum-route-ambiguity-takes-the-first`, - `vacuum-route-missing-space-falls-back-to-dock`, - `vacuum-route-unmapped-draws-anyway`, - `vacuum-route-identity-duplicates-allowed`, - `vacuum-legacy-run-adopts-the-first-candidate`, - `vacuum-overlay-ignores-the-rendered-space`, - `vacuum-previous-run-follows-the-robot`, - `vacuum-route-warning-stays-silent`, плюс серверные - `vacuum-run-forgets-its-route`, `vacuum-retargeted-route-keeps-its-old-trails`, - `vacuum-route-validation-accepts-a-dead-space` и - `space-delete-keeps-foreign-vacuum-routes` - [mutation: `scripts/mutation-gate.mjs`]. -- [ ] Продакшен-бандл показывает робота на этаже активной карты, а на этаже дока - не показывает; предупреждение у дока появляется на движущемся роботе с - несопоставленной картой; донастройка предложения с высоким residual - открывается на этаже маршрута, а не дока - [auto: `demo/smoke_vacuum_multifloor.mjs`]. -- [ ] Отдельная camera на карту: источник без читаемого id карты маршрута не - создаёт и объясняет причину [ручная проверка блока «Карты и этажи»]. - -## Vacuum trail smoothing (#209) - -- [ ] Pure `smoothVacPath` tests prove exact endpoints, separate subpaths, - degenerate/reversal fallbacks, the 17.5 cm sampled deviation bound and - the 64-subpath/4000-point `≤2N` command budget - [unit: `test/vacuum.test.mjs`]. -- [ ] The production bundle renders current and previous runs through paired - case/core `` elements with quadratic commands, unchanged source - authority/modes, live-target trim and puck tip - [auto: `demo/smoke_vacuum.mjs`]. -- [ ] Flat and dormant-Iso projections retain the same curved live layer on - touch and across HA updates [auto: `demo/smoke_isometric_live_touch.mjs`]. -- [ ] `vacuum-trail-smoothing-dark` records the 17.5 cm default in fixture data, - contains two current subpaths plus a stored previous run, and its semantic - guard requires two `M` sections, quadratic - commands and byte-identical case/core geometry before PNG comparison. - The baseline is accepted only from reviewed Linux CI - [golden: `demo/golden/matrix.mjs`]. -- [ ] Replacing the quadratic corner with a straight vertex makes the focused - unit guard red [mutation: `vacuum-trail-smoothing-disabled`]. - -## Stable wall-segment identity (#282) - -- [ ] Wall junction limits (#329): drawing refuses an apex under 15°, a seventh - wall in one node, a wall shorter than 20 cm or than its own thickness, - nodes closer than 5 cm and a room with under 25 cm² of interior, each - through the surface's own channel — a toast naming the rule for drawing - and Thickness, a stopped wall for Resize. A T-joint stays legal, a short - filler atom compensating a thickness step stays legal (length is measured - along the collinear same-thickness wall run), and an inherited violation - never blocks an unrelated edit - [unit: junction-limits; auto: smoke_junction_limits, smoke_island_rooms; - mutants: junction-limit-angle-not-enforced, - junction-limit-write-gate-removed, degenerate-apex-bevelled-again]. -- [ ] A degenerate sharp apex renders as ONE point on both faces — no flat - chamfer, no bow-tie fold, no jags between the inner and outer vertex; the - room ring of the #329 fixture triangle has exactly three distinct - vertices [unit: junction-limits §4]. -- [ ] Shared fixture `test/fixtures/282-wall-identity-parity.json` produces the - same exact v8 catalog, room references, opening host and draft IDs in - TypeScript and Python. -- [ ] Initial v7 migration is deterministic and idempotent; new post-v8 atoms - use UUIDs. A split/promoted draft keeps one documented carrier ID, while - reserved/colliding deterministic IDs receive stable `-2`, `-3` suffixes. -- [ ] The three structural writer families — interactive commit, Undo/Redo - restore and Optimize — are enumerated by the source guard and each has an - independent bypass mutant in `scripts/mutation-gate.mjs`. A rejected - migration changes neither config, Undo history nor revision. -- [ ] Full/space imports cover v7→v7 (no upgrade), v7→v8 and v8→v8; copy/merge - remaps every ID and reference together. A byte-equivalent legacy-client - round-trip of v8 is accepted, while a structural legacy change is rejected. -- [ ] Resize, Split/Merge, opening edit and Optimize browser smokes retain wall - thickness and ownership across reload. Performance gate: - `npm run benchmark:wall-model` materialises 10,000 atoms with p95 below - 500 ms on the reference Windows machine. -- [ ] Local commands: `npm test`, `npm run typecheck`, - `npm run benchmark:wall-model`; backend parity/schema tests run through - `tests_backend/test_wall_segment_model.py` and - `tests_backend/test_validation.py`. HA import/export coverage runs in the - normal Linux/CI Home Assistant harness when unavailable natively. - -## Legacy draft migration and atomic wall-chain writes (#314, #478) - -- [ ] `demo/smoke_v8_draft_write.mjs` (compatibility filename) proves that a - model-v9 draft migrates once to ordinary partitions, preserves existing - edge IDs/thicknesses and leaves no `room_drafts` in model v10. -- [ ] The same fake-WS smoke proves each accepted current wall reaches storage, - a later edge preserves earlier identities, Undo removes only the terminal - wall, and a closed chain creates a durable room without carrier debris. -- [ ] A rejected in-flight physical write synchronously rolls back its whole - pending batch: active path, session partition IDs, pending map and command - history are empty, so no optimistic ghost wall survives. -- [ ] Frontend/backend model tests consume the same - `478-room-draft-migration-vectors.json`: missing/null IDs, colliding IDs, - numeric strings, `null`/boolean thickness and epsilon-length edges must - produce the same exact partitions or rejection reason in both runtimes. - Backend coverage also rejects a stale v9 `room_drafts` write over v10. -- [ ] `demo/smoke_unified_wall_tool.mjs` accepts a room over a partially - coincident older partition, preserves only its outside tail and an - unrelated partition, then proves an immediate Optimize reports zero - reconciliation and no config change. A forced missing post-mutation wall - model restores the whole room transaction. -- [ ] Local commands: `npm test`, `npm run bundle:sync`, - `node demo/smoke_v8_draft_write.mjs`, targeted backend pytest and - `node scripts/check-docs.mjs --external`. -- [ ] Mutation `current-rejected-physical-write-keeps-optimistic-wall` proves - the browser rollback scenario fails when rejection recovery is bypassed. -- [ ] Mutation `room-accept-leaves-coincident-partitions` proves the real editor - smoke fails if accepted room carriers stop being consumed atomically. - -## Current-writer fixed point (#477) - -- [ ] `test/writer-fixed-point.test.mjs` proves seed-bounded repeated collinear - merge, safe positive coincident reconciliation/opening rehost, identical - fail-closed cases, exact room-reference rewrite/restore and the executable - writer-owner manifest. -- [ ] `demo/smoke_writer_fixed_point.mjs` finishes a real straight Walls chain - through the production bundle, then proves the immediate, post-Undo and - post-Redo Optimize previews are no-ops. The same smoke deletes a room and - restores/reapplies direct and cross-space vacuum references without - overwriting unrelated marker fields. -- [ ] `demo/benchmark_wall_draw_click.mjs` keeps the #461 terminal-click budgets - and structural counters, then separately proves finish uses one bounded - physical/junction transaction, one write, no extra history and sublinear - scaling when unrelated rooms are added. -- [ ] `test/align-grid.test.mjs` proves Optimize leaves complete furniture/image - transforms byte-equivalent while an ordinary decor rectangle remains - grid-bound. -- [ ] The `writer-*` mutations in `scripts/mutation-gate.mjs` remove one finish - owner, pre-adoption safety, history normalization, direct/vacuum reference - rewrite, free-transform exclusion, terminal-click separation, seed merge - and seed reconciliation; each named witness must turn red. - -## Resize: реальный pointer pipeline (#293) - -- [ ] `demo/smoke_resize_pointer_real_plan.mjs` загружает tracked fixture - второго этажа обычным `houseplan/config/get`, включает Resize кнопкой и - двигает доступную общую стену только реальными `page.mouse` событиями. - Прямые вызовы приватных resize-методов в этом smoke запрещены source - guard-юнитом. -- [ ] На десятом шаге сетки обе комнаты имеют видимый preview, а server config - ещё байт-в-байт исходный. Pointerup создаёт одну history-команду и одну - запись; wall count и набор толщин сохраняются; Ctrl+Z возвращает исходную - геометрию. -- [ ] Pointer capture продолжает жест минимум в двух диаметрах и 120 px от - хэндла, чужой - pointer id игнорируется, а Escape и `lostpointercapture` возвращают DOM и - config без дополнительной записи. -- [ ] Невозможная физическая preview-геометрия останавливает стену на последней - безопасной позиции и показывает один локализованный toast за жест. Отдельно - проверяется отказ финального preflight без commit/history. -- [ ] Мутанты `resize-pointer-delta-zeroed`, - `resize-shared-seam-not-coalesced`, `resize-pointer-capture-removed`, - `resize-preview-reject-silent` и - `safe-resize-commit-preflight-bypassed` обязаны красить соответствующие - unit/production smoke guards. -- [ ] Fixed-topology wall records (#298): moving-wall breakpoints translate - rigidly, side-wall interior endpoints never scale proportionally, - the exact first-floor 49→52 gesture ends on 17/52/57/101, unrelated - records remain byte-equivalent, and a full-span carrier/lattice proof - rejects gaps before preview. Key-only legacy records move only by one - whole-edge identity; partial midpoint ambiguity produces no preview, - history or config write [unit: `wall-thickness.test.mjs`; auto: - `smoke_resize_pointer_real_plan`, `smoke_resize_wall_thickness`, six - `smoke_edit_walk` runs; mutation: - `safe-resize-wall-endpoints-affine-scaled`, - `safe-resize-legacy-midpoint-fail-open`]. - -## Decor composition order (#231) - -- [ ] All six decor kinds render in one `.decorlayer` after opaque room/data - fill, active room-hover fill, opening tunnels and Glow-base rooms/tunnels, - but before live Glow, sun, physical walls, opening symbols and the HTML - device/room-label layer [auto: `smoke_decor_layer_order.mjs`, - `smoke_glow.mjs`]. -- [ ] Pixel probes through an opaque room and a filled opening tunnel stay the - decor colour before/after hover and over Glow base. Restoring the old DOM - order makes those probes red [auto: `smoke_decor_layer_order.mjs`; - mutation: `decor-restored-below-room-fills`]. -- [ ] The complete #231 golden impact set is reviewed before baseline - acceptance. The two dedicated Light/opaque-hover and Dark/Glow-base - scenes contain all five decor types and semantic probes in both rooms and - the shared doorway. The three existing large-house scenes also change - because their dense decor grid now renders above Glow-base room fills. - Reviewed baselines are accepted only from the Linux release artifact - [golden: `decor-over-opaque-hover-light`, - `decor-over-glow-base-dark`, `isometric-large-warm-remount-dark`, - `large-house-zoom-250-dark`, `large-house-warm-remount-dark`]. -- [ ] `hide_decor`, the Background editor override and stored config remain - unchanged; no per-object under-plan compatibility flag is introduced. - -## Custom decor images (#51) - -- [ ] Background has one **Image** button. PNG/JPEG/WebP/safe SVG upload opens - the reusable palette; picking a file shows an exact one-shot preview and - one click creates a 100 cm wide aspect-preserving object (height capped at - 200 cm), then returns to Select [unit: `decor-assets.test.mjs`; auto: - `smoke_decor_images.mjs`]. -- [ ] Image move/continuous resize/four side handles/crossing mirrors/free - rotation/`Shift` 45° match furniture, while placement and movement have - no wall magnet. The complete rotated rectangle remains selectable through - transparent pixels [auto: `smoke_decor_images.mjs`]. -- [ ] Full View and `houseplan-space-card` paint the same signed raster/SVG - under live Glow/walls/devices and obey `hide_decor`. A missing or - hash-mismatched file paints nothing in View and a selectable crossed - repair placeholder only in Background [unit: `decor-assets.test.mjs`, - `test_decor_assets.py`; auto: `smoke_decor_images.mjs`]. -- [ ] Upload rejects wrong magic, corrupt decode, oversized dimensions/files, - forbidden namespaces/elements/attributes/URLs, DTD/entities, processing - instructions and local-reference cycles. Responses have exact MIME, - `nosniff` and SVG sandbox CSP [pure backend: `test_decor_assets.py`; HA - harness: `test_ha_import_export.py` and endpoint tests]. -- [ ] Identical canonical bytes reuse one SHA-256 id. Resolve is deduplicated - and batched at 200; deletion rechecks all spaces and refuses an in-use - file. Export v2 has hashes but no bytes/signed URLs; v1 remains readable, - and confirmed missing imports preserve image geometry [unit/pure/HA: - `decor-assets.test.mjs`, `test_decor_assets.py`, - `test_ha_import_export.py`]. - -## Opening symbol centreline (#242, #250) - -- [ ] Unit and browser checks prove that door/window/gate stay on the wall - centreline for both `flip_v` values, door/window flips change only their - direction, and gate `flip_v` reverses the first-leaf 10° turn on shared room walls, - independent partitions and hidden Iso without translating the gate - [unit: `opening-symbol.test.mjs`, `iso-openings.test.mjs`; auto: - `smoke_wall_thickness.mjs`, `smoke_isometric_contract.mjs`; mutations: - `opening-symbol-flip-restores-edge-offset`, - `opening-gate-flip-cancels-turn`]. -- [ ] Matrix v37 adds four dedicated semantic scenes. Before PNG comparison - they assert the saved flip value, wall centreline, visible-group offset, - full jamb depth, window glass membership and opposite gate turn signs: - `opening-symbol-room-wall-light`, - `opening-symbol-diagonal-partition-dark`, - `opening-symbol-flip-pairs-light`, - `isometric-opening-symbol-parity-dark`. -- [ ] #250 reuses those four scenes and requires `offset: center` for every - door/window/gate entry, including flipped pairs. The semantic guard must - fail before PNG comparison if any saved flip restores a wall-face offset. -- [ ] The exact existing golden impact set below contains **67** scenes. It was - measured by comparing `actualSha256` for HEAD and `origin/dev` under the - same Chromium build; baseline status alone is not used because `dev` - already has unrelated pending pre-release candidates. Every listed frame - uses a shared fixture containing an affected opening or retains that plan - behind an editor/dialog. No other existing frame changed: - - `isometric-geometry-view-dark`, `isometric-geometry-view-light`, - `isometric-live-layers-dark`, `isometric-no-borders-dark`, - `isometric-touch-kiosk-dark`, `isometric-large-warm-remount-dark`, - `geometry-view-dark-fit`, `geometry-view-light-fit`, - `room-label-parity-view-dark`, `room-label-parity-plan-dark`, - `room-label-parity-view-light`, `room-label-parity-plan-light`, - `day-cycle-dawn-dark`, `day-cycle-day-dark`, `day-cycle-dusk-dark`, - `day-cycle-night-dark`, `geometry-plan-editor-dark`, - `space-tab-drop-before-light`, `space-tab-drop-after-dark`, - `plan-snap-endpoint-light`, `plan-snap-line-gaps-dark`, - `junction-patch-resilience-plan-dark`, - `opening-placement-door-thick-wall-dark`, - `opening-placement-passage-thick-wall-dark`, - `opening-placement-passage-thick-wall-light`, - `geometry-devices-editor-dark`, `geometry-decor-editor-dark`, - `tray-wide-selection-en`, `tray-wide-tool-ru`, - `tray-medium-group-en`, `tray-medium-selection-ru`, - `tray-narrow-palette-en`, `tray-narrow-tool-ru`, - `geometry-diagonal-45-opening-dark`, `openings-thick-wall-dark`, - `lighting-glow-sun-dark`, `device-value-badge-positions-dark`, - `device-icon-state-table-light`, `device-icon-state-table-dark`, - `device-text-shell-long-light`, `device-text-shell-long-dark`, - `lighting-sun-window-state-only-dark`, - `lighting-fill-light-axis-split-dark`, - `lighting-fill-temp-axis-split-dark`, - `lighting-fill-lqi-axis-split-dark`, `lighting-temp-glow-dark`, - `lighting-temp-glow-light`, `lighting-custom-glow-dark`, - `lighting-opaque-glow-two-doorways-dark`, - `lighting-custom-glow-light`, `lighting-temp-glow-no-sources-dark`, - `lighting-temp-glow-room-override-dark`, - `lighting-manual-auto-spill-overlap-dark`, `hover-over-glow-dark`, - `hover-nested-room-dark`, `large-house-zoom-040-dark`, - `large-house-zoom-250-dark`, `large-house-warm-remount-dark`, - `device-dialog-desktop-en`, `device-help-popover-light-ru`, - `decor-color-popover-desktop-en`, `general-color-popover-desktop-en`, - `space-room-color-popover-desktop-ru`, - `backup-full-preview-desktop-en`, - `backup-plan-only-export-desktop-en`, - `optimize-preflight-dialog-dark-en`, - `optimize-preflight-dialog-light-ru`. -- [ ] Baselines for the 67 existing and four dedicated scenes are accepted - only from the reviewed full Linux pre-beta artifact. Local - `golden:accept` remains forbidden. - -## Device icon design package (#179) - -- [ ] Pure presentation tests cover lock/unlock, exact marker-only LQI bands - `0/40/41/179/180`, unchanged room gradient, package pulse defaults, - semantic colors and reduced motion - [unit: `device-presentation.test.mjs`, `device-pulse.test.mjs`]. -- [ ] Shared face tests cover shell/core DOM, four Double positions, a third - legacy section, deterministic font fitting, full text and safe CSS color - variables [unit: `device-face.test.mjs`]. -- [ ] The browser renders the exact shell ratio and shadow color, Light/Dark - cores without backdrop blur, state/LQI colors, 3.6 s presence pulse, - unavailable no-hover, full Text/Double values, 44×44 target, View and - Device-editor keyboard paths, and no Plan tab stop - [auto: `smoke_device_icon_design.mjs`]. -- [ ] Full plan, preview and static card preserve the same face after the DOM - redesign; static mode, state/value and disabled-device contracts stay - green [auto: `smoke_device_preview_parity.mjs`, `smoke_static_icon.mjs`, - `smoke_state_value.mjs`, `smoke_disabled_device.mjs`]. -- [ ] Restoring unavailable hover, shifting the LQI boundary, restoring value - ellipsis or bypassing `_clickDevice()` makes its guard red - [mutation: `device-unavailable-hover-restored`, - `device-marker-lqi-low-boundary-shifted`, - `device-long-value-ellipsis-restored`, - `device-keyboard-bypasses-click-path`]. -- [ ] Pre-beta golden reviews desktop/mobile Light/Dark states, combo states, - Text, four Double positions, long and legacy values, LQI, reduced motion, - sizes 32/56/96 and colored backgrounds. Full smoke/golden/performance - remains a Linux release gate. - -## Dense device-marker hit ownership (#564) - -- [ ] Pure geometry covers visible capsule priority, invisible 44 px-floor - overlap, nearest-core selection, stable exact ties, stadium corners and - spatial-index locality [unit: `device-hit-owner.test.mjs`]. -- [ ] Source contracts keep painted shells globally above transparent floors, - route hover/click/pointer lifecycle through one semantic owner and forbid - layout reads from the pointer-move path - [unit: `device-hit-owner-contract.test.mjs`]. -- [ ] Household J7 checks the five-marker dense column at tablet and phone - widths: every visible centre has the same native and semantic owner, its - click opens that marker, and a pointer sequence remains latched through - its terminal click [auto: `smoke_household_journeys.mjs`]. -- [ ] Real browser geometry covers Icon, Text, Double and untouched legacy - faces at all four cardinal sides/directions. In every case a painted - point overlaps only the neighbour's invisible 44 px floor and still owns - the native hit, semantic owner and click - [auto: `smoke_device_hit_capsules.mjs`]. -- [ ] Replacing nearest-screen-core selection with first-input/DOM order makes - both the pure geometry guard and the real rendered-face browser guard red - [mutations: `dense-device-hit-falls-back-to-input-order`, - `dense-device-hit-browser-skips-painted-priority`]. - -## Device marker polish and pointer modality (#212) - -- [ ] Shared icon geometry applies one 0.9 visual factor after card/per-marker - sizing, keeps the saved centre and 44×44 hit floor unchanged, and gives - wide Text cores a radius equal to half their height - [unit: `device-marker-polish-contract.test.mjs`; auto: - `smoke_device_icon_design.mjs`]. -- [ ] Only a genuinely dispatched toggle/run action produces one - `1 → .95 → 1` feedback cycle lasting 200 ms. Info, editor, confirmation - before acceptance, unavailable/secure/no-target and cancelled gestures do - not; reduced motion has no scale tween - [auto: `smoke_device_icon_design.mjs`]. -- [ ] Pointer authority is isolated per card. Touch/pen and compatibility mouse - input clear JS/CSS hover, while a later real mouse restores it only on - fine/hover hardware; mode/space/visibility/disconnect cleanup remains - bounded [unit: `pointer-modality.test.mjs`; auto: `smoke_feedback_v2.mjs`]. -- [ ] Pre-beta visual review covers Light/Dark desktop and touch matrices for - ordinary, Text, Double, unavailable and vacuum markers; full golden and - performance gates remain release work. - -## Empty-space lifecycle (#113) - -- [ ] Active selection keeps active-or-first compatibility, while an empty - model returns `undefined`; exact lookup of a stale saved id never falls - back to another space [unit: `space-model-selection.test.mjs`]. -- [ ] There are no unguarded `_spaceModel().…` dereferences, explicit-id calls - use `_spaceModelById()`, and marker/position persistence validates its target - before config/file/WS side effects - [unit: `optional-space-model-contract.test.mjs`]. -- [ ] Delete the last space while an editor gesture and debounced write are - active: the empty card renders, View is restored, pointer/draft/dialog - state is cleared, the pending write is cancelled and Add space still - opens Create. Recreate a plan, then receive an empty WS config and repeat - under a theme/resize/read-only tick [auto: `smoke_optional_space_model`]. -- [ ] Removing the authoritative empty-state cleanup makes that smoke red - [mutation: `empty-space-cleanup-disabled`]. - -## Fixed card space (#210) - -- [ ] `floor` resolves exact stable IDs and zero-based finite integer indexes; - quoted numeric strings stay IDs, and explicit empty, unknown, fractional, - negative or out-of-range values fail closed [unit: `initial-load.test.mjs`]. -- [ ] Three coexisting instances (fixed ID, fixed index and unpinned) keep - independent authority while sharing the legacy navigation key. Fixed - cards ignore hash, tabs, guarded internal transitions, warm remount, - kiosk swipe/cycle/dots and never read or write saved navigation. Invalid - config renders an accessible error without a spatial stage, while the - unpinned card still restores legacy navigation - [auto: `smoke_fixed_floor.mjs`, `smoke_nav_persist.mjs`, `smoke_kiosk.mjs`]. -- [ ] The GUI offers stable IDs only, preserves an existing numeric YAML value - during unrelated edits and deletes the `floor` property when cleared - [unit: `fixed-floor-contract.test.mjs`; auto: `smoke_fixed_floor.mjs`]. -- [ ] Bypassing the shared transition guard makes the focused browser scenario - red; before-fix evidence also records that `origin/dev` has no fixed - resolver or guarded transition - [mutation: `fixed-floor-transition-guard-bypassed`]. - -## Toggle confirmation state (#103) - -- [ ] Every executable `ToggleNextEffect` formats current and expected lines - without deriving direction from the state label; `toggle` names Home - Assistant as the authority and a no-operation intent produces no lines - [unit: `device-toggle.test.mjs`]. -- [ ] All-off/mixed/partial groups use only executable targets for their - active/total count and show skipped targets as a separate line - [unit: `device-toggle.test.mjs`]. -- [ ] EN/RU confirmation renders prompt → current → expected → skipped before - the buttons, wraps a long name at 390 px and has no horizontal scroll - [auto: `smoke_toggle_confirmation.mjs`]. -- [ ] A state race with the same target executes the newly resolved direction; - a changed target set makes zero service calls and shows the existing - retry toast [auto: `smoke_toggle_confirmation.mjs`]. -- [ ] Cover, virtual-light, HA-control and Run confirmations keep their - existing actuation/cancel contracts [auto: `smoke_cover_tap`, - `smoke_virtual_light_toggle`, `smoke_ha_controls`, `smoke_controls`, - `smoke_tap_run`]. - -## Unified color and opacity picker (#57) - -- [ ] RGB↔HSV round trips stay within one RGB channel; 3/6-digit HEX input is - normalized and invalid drafts never become persisted colors - [unit: `color-picker.test.mjs`]. -- [ ] Every existing `hp-color-opacity` consumer receives per-card localized - labels through the unchanged color/opacity event contract, and the shared - component contains no native `input[type=color]` - [unit: `color-picker.test.mjs`]. -- [ ] The localized full-width OK button is the final picker control, remains at - least 40 CSS px high in native and fallback surfaces, and closes without a - duplicate value event or click-through to the parent card - [unit: `color-picker.test.mjs`, auto: `smoke_color_picker.mjs`, - `smoke_help_affordance.mjs`]. -- [ ] Invalid HEX keeps the picker open and focused on its error even after a - repeated OK; only new valid HEX input unlocks confirmation, while live - color/opacity updates and the existing outside/trigger/Escape close paths - retain their values [auto: `smoke_color_picker.mjs`]. -- [ ] At 390 px the one surface exposes hue, saturation, brightness, HEX and - opacity without horizontal overflow; keyboard Shift+Arrow, touch pointer - cancellation, invalid HEX recovery, Escape focus return and disabled mode - remain safe [auto: `smoke_color_picker.mjs`]. -- [ ] The color-only Glow consumer keeps the same picker without an opacity row, - and native/fallback floating surfaces remain mutually exclusive with help - [auto: `smoke_help_affordance.mjs`]. -- [ ] The Hue range keeps `0…359`, step 1 and its existing input events while its - WebKit/Blink and Gecko tracks expose the same cyclic spectrum. A dual - theme-aware ring keeps the native thumb distinct from every hue, while - forced-colors falls back to system track/thumb rendering [unit: - `color-picker.test.mjs`, auto: `smoke_color_picker.mjs`]. -- [ ] The dark mobile and light desktop open-picker goldens are reviewed from the - complete Linux artifact before a beta; baseline acceptance is not part of - the implementation loop [golden: `decor-color-popover-mobile-ru`, - `decor-color-popover-desktop-en`]. - -## Unified picker coverage for every color field (#180) - -- [ ] A recursive source contract rejects every native `input[type=color]` in - product TypeScript and fixes the complete shared-component inventory at - 13 template instances [unit: `color-picker.test.mjs`]. -- [ ] The 11 general light/temperature/LQI/Glow/wall palettes and the space room - colour move their existing opacity into the unified picker; color and - alpha update one parent draft atomically [unit: `color-picker.test.mjs`, - auto: `smoke_color_picker_consumers.mjs`]. -- [ ] Global background, space background and activity ripple are color-only; - opening/closing does not materialize an inherited/default value, and - Default/Inherit restore `null` without adding alpha - [auto: `smoke_color_picker_consumers.mjs`]. -- [ ] General settings keep one exclusive picker open among 12 swatches; marker - activity colour and ripple size use separate, non-overlapping mobile rows, - ripple size remains independent, and cancelling the space dialog writes - no color draft [unit: `color-picker.test.mjs`, auto: - `smoke_color_picker_consumers.mjs`]. -- [ ] The three new dialog families are reviewed from the complete Linux - artifact before a beta; implementation does not accept their baselines - [golden: `general-color-popover-desktop-en`, - `device-ripple-color-popover-mobile-ru`, - `space-room-color-popover-desktop-ru`]. - - -## Open passage (#157) - -- [ ] Подменю и диалог показывают четвёртый тип в порядке Окно / Дверь / - Открытый проём / Ворота; новый проём имеет ширину 90 см. [auto: open-passage-contract, opening-placement] -- [ ] В Plan/View у passage отсутствуют створка, дуга, рамка и пунктир, но - сохраняются hitbox, wall cut и room-coloured tunnel. [auto: opening-symbol, smoke_open_passage] -- [ ] Static вырезает и заполняет тоннель только для passage, не меняя старые - door/window/gate. [auto: space-geometry, smoke_open_passage] -- [ ] Внутренний passage пропускает Glow, внешний и неизвестный будущий тип - остаются fail-dark. [auto: light-visibility, smoke_open_passage] -- [ ] Passage в скрытой изометрии имеет full-height cut и zero leaves. - [auto: iso-openings, golden] -- [ ] Смена типа предупреждает о датчике/замке; Save удаляет пять - неприменимых ключей, Cancel не меняет config. [auto: open-passage-contract, smoke_open_passage] -- [ ] Full/space import отвергает forged binding до preview, а старое битое - значение можно прочитать и очистить. [auto: test_validation, test_ha_import_export] -- [ ] Пять passage-мутантов из `scripts/mutation-gate.mjs` пойманы своими - guards до передачи в review. [auto: mutation-gate] - -## Independent-wall openings and structural axes (#132, #185) - -- [ ] Door/window/gate/passage placement on a finished independent wall stores - `host.kind/id/t`; a coincident room wall chooses that explicit host, while - crossing or duplicate-host ties are rejected. [auto: opening-placement, - partition-openings] -- [ ] Every hosted type cuts only its host full-depth in Plan/View/Static/Iso; - exact composite room masonry is also cut, nearby bodies remain intact, - and malformed hosts fail dark. [auto: physical-geometry, - smoke_partition_openings] -- [ ] Rigid host drag preserves `t` and updates projections atomically; delete - lists hosted openings, Cancel changes nothing, Confirm cascades in one - Undo/Redo command. [auto: partition-openings, smoke_partition_openings] -- [ ] Contact/lock actions keep existing security rules; passage stays inert; - windows and exterior passages stay opaque to Glow, and partition windows - produce no sun wedge. [auto: runtime contracts, smoke_glow] -- [ ] Door/window/gate/passage presentation gaps do not split the structural - axis used by the Walls face graph; real `open_spans` still do. [auto: - plan-snap-overlay, smoke_room_autoclose, smoke_partition_openings] -- [ ] Backend rejects missing host references, out-of-range `t`, non-fitting or - overlapping hosted openings and stale host stripping; exports round-trip - the host. [auto: test_validation, test_ha_import_export] -- [ ] The exact #276 Optimize candidate is shared by frontend and backend tests: - Python independently proves the removed partition, two-room solid wall, - envelope, opening identity and non-overlap; config/set and every partial - or mutated candidate remain rejected. Linux HA WS persists and reloads - the implicit opening, then Undo restores the partition and explicit host. - [auto: coincident-partitions, test_validation, test_ha_websocket] - -## Independent-wall opening jamb margin (#186) - -- [ ] Strict resolver and placement reserve half the host depth for - door/window/gate/passage at both endpoints, including exact-boundary, - diagonal, reversed, thickness and scale matrices; room-wall placement - keeps its zero-jamb rule. [auto: partition-openings, opening-placement] -- [ ] Direct drag, dialog length edits and rebind share the same formatted - RU/EN guidance; a rejected edit writes neither config nor history. - [auto: smoke_partition_openings] -- [ ] Backend config/set and optimize reject a new/direct invalid geometry with - `invalid_partition_opening_jamb_margin`, while unrelated writes, rigid - translation and full backup restore preserve a legacy near-end record. - [auto: test_validation, test_ha_websocket, test_ha_import_export] - - -## Device value badge (#90) - -- [ ] An untouched legacy thermometer/humidity marker remains pixel-identical; - saving another field does not materialize `value_badge`. [auto: device-presentation] -- [ ] Explicit on/off overrides the legacy temperature gate; zero, false and - off remain visible, while missing/unknown/unavailable render a stable `—`. [auto: device-presentation] -- [ ] State, every allowlisted attribute, derived LQI and `marker:` light - state resolve identically on the full plan, static space card and preview. [auto: smoke_device_preview_parity] -- [ ] Opening the editor explicitly selects the persisted source and position, - even when they are not the first dynamic options, without touching config. [auto: smoke_device_preview_parity] -- [ ] Right, bottom, left and top update live in the editor; bottom stacks above - system LQI and derived LQI suppresses the duplicate system row. [auto: device-presentation] -- [ ] Browser bounding boxes stay inside `.previewstage` with a safe gap for - all positions, a long value, scale ×3 and the maximum activity ring. [auto: smoke_device_preview_parity] -- [ ] Rebind resets the source, delete leaves a missing diagnostic reference, - and space import remaps internal refs or disables/counts external refs. [auto: test_ha_import_export] -- [ ] `static_icon` suppresses but preserves the setting; live-state and room - label toggles do not suppress an explicit badge. [auto: device-presentation] - -> **Policy:** this checklist is updated **in the same commit** as any functional -> change (like CHANGELOG.md). For a pre-release, build the production bundle and -> run the smallest unit/smoke subset that covers its changed surfaces. Run the -> complete local frontend, backend and smoke gates only before a stable release. -> The exact-SHA GitHub Validate remains mandatory for publication. Items marked -> `[manual]` are covered by unit tests or the headless smokes in `demo/` — they still -> deserve an occasional eyeball. File every failure as a GitHub issue before fixing. - -> **What `[manual]` means (since 2026-07-27).** A named check exists that FAILS -> when the behaviour breaks — in `npm test` or in the smoke suite, both of which -> run in CI on every push. Where the check lives is written next to the marker -> (`[auto: smoke_modes]`). Before this date the marker described an intention: -> the smoke suite printed values and always exited 0, so 96 markers guarded -> nothing (external audit T1/T3). If you add a checklist line marked `[manual]`, -> add the failing check in the same commit. - -> **⚠ Rule: a new scrollable list inside a dialog is tested by GEOMETRY, never -> by the DOM.** Any new scrolling box or `overflow` container added to a dialog -> MUST get a smoke that measures **the container's own height and the visible -> position of its first item** (`getBoundingClientRect`, and the item's rect -> against the box's rect) — counting rendered rows, or asserting that the nodes -> exist, proves nothing. The failure mode is always the same and always -> invisible to a DOM check: a scrolling box is a flex item whose automatic -> minimum size is zero (`min-height: auto` → 0 for an `overflow` child), and a -> dialog body is a flex column with a height cap, so the box is the one child -> that can be squeezed to a sliver while every row inside it renders happily. -> It has bitten us twice already: the **target search results** in the tap -> action dialog (v1.53.1 — 26 matching automations rendered into a 1 px -> stripe; the smoke counted rows and passed) and the **«Already uploaded»** -> plan picker (dev, unreleased — rows present, box 14 px tall, same story). -> Both smokes measure heights now; write the third one that way from the start. - -## HA-disabled binding gate - -The source-of-truth matrix is -`docs/superpowers/specs/2026-08-08-ha-disabled-devices-design.md` §17. -`test/ha-binding-status.test.mjs` covers full/limited registry decisions and -the active-only state projection. The standalone demo exposes complete -`disabled_by` rows through both registry list WS commands plus -`window.__setRegistryDisabled(kind, id, disabledBy)` and -`window.__setRegistryAccess(mode)` for browser scenarios. - -- [ ] A saved device/entity marker disappears from View, room data, Glow, - controls, live text, openings and vacuum overlays after its registry row - becomes disabled; config/layout remain byte-for-byte unchanged. -- [ ] Device editor → Hidden and disabled shows a labelled grey ghost. Show is - refused, metadata/Delete/Open in HA remain available, and the ghost is - not draggable. -- [ ] Reactivating the same ID restores its metadata/layout without a false - new-device event; an explicitly user-hidden marker stays hidden. -- [ ] A never-seen auto device disabled before discovery appears as new only - after its first activation. -- [ ] All disabled child entities make an otherwise active device disabled; - one disabled auxiliary entity never suppresses active functional rows. -- [ ] If full registry WS access is denied, positive live evidence stays - active, an unknown binding is `unverified`, and no false disabled/orphaned - ghost or service call is produced. -- [ ] Two full cards plus a static space card share one registry fetch and one - subscription pair per HA connection; registry events invalidate all of - them without a reload. -- [ ] `houseplanDiagnostics()` reports only redacted registry access/age/error - and binding-status counts; it contains no names, states or marker data. - -## HA Area marker relocation (#126) - -- [ ] `test/device-area-relocation.test.mjs` covers direct-binding authority, - same/cross-space transitions, conservative legacy backfill, explicit and - composite exclusions, rebind, malformed metadata and delete-first - provenance. -- [ ] `test/space-geometry.test.mjs` proves both marker-position paths can - suppress one stale saved point without changing the stored layout input. -- [ ] `demo/smoke_area_relocation.mjs` changes an authoritative registry Area - against the production bundle, checks one serialized layout delete plus - config/attention persistence, and proves the read-only hosted card moves - immediately without writes. -- [ ] Area-provenance cleanup ignores the filtered display roster, preserves - empty Device/Entity Registry namespaces, accepts exact live entity states - as existence evidence, requests only one confirmation refresh and removes - an orphan only after two distinct non-empty authoritative revisions. -- [ ] A rejected confirmed-cleanup config write remains retryable on the next - rebuild; the same revision, state ticks and repeated empty frames never - become extra confirmation or a registry reload loop. -- [ ] Backend validation and import/export tests cover the 20,000-entry bound, - exact entry schema, same-source preservation and cross-source removal. - -## Device display preview and face parity - -The behaviour matrix is defined in -`docs/superpowers/specs/2026-08-08-device-display-preview-design.md` §22. -Pure source/value/presentation rules live in `test/device-presentation.test.mjs`. -`demo/smoke_device_preview_parity.mjs` compares the same live fixture across -the interactive plan, `hp-device-preview` and `houseplan-space-card`, including -semantic classes, icon/value/badges, scale variables, provider text and the -public binding-status hook. - -- [ ] Every binding/display/icon/size/angle/control/temperature draft change - updates the preview before Save; Cancel writes neither config nor layout. -- [ ] Working, open, cover, presence, short event, transition, alarm, static, - unavailable, media-neutral, composite-Power and `live_states: false` - explanations match the actual face. -- [ ] The local short-activity demo lasts 3.3 seconds and the continuous demo - runs until stopped. Neither sends a service call; reduced motion uses a - compact dot, and both reset immediately on binding change, real activity - or alarm. -- [ ] Provider metadata is cached between dialog openings and refreshed after - registry/config-entry changes; source integrations remain separate from - the binding provider. -- [ ] Long provider/source/state text wraps without horizontal scroll; maximum - marker/ripple size fits the stage and reports its preview scale. -- [ ] Derived temperature/humidity values keep the compact plan form (`22.4°`, - `48%`), while a direct entity value continues to use HA localization and - units. - -## Device icon package parity (#211) - -The independent reference subset under -`demo/srv/reference/device-icons/` comes directly from designer package 1.1.1; -it is not generated from production CSS. The package archive hash and the -owner's #219 red/green Lock/Unlock paint override are recorded in that -directory's README. - -- [ ] `node demo/smoke_device_icon_design.mjs` reads the SVG colors and stroke - widths, then compares them with fresh computed styles. It also measures - circular core/shell geometry, the real `mdi:lightbulb-spot` painted path, - value-pill radius and 44×44 hit area at 32/56/96 px. -- [ ] `node demo/capture_device_icon_reference.mjs` writes a two-column - **Reference SVG / Runtime** matrix for both themes to - `artifacts/device-icon-reference/`. Code review must inspect this artifact - visually; a green historical golden is not proof of package parity. -- [ ] Preview/static parity and unavailable keyboard/tap behavior remain - covered by `smoke_device_preview_parity`, `smoke_static_icon` and - `smoke_disabled_device` after a fresh production build. - -## Device marker geometry and input polish (#213) - -- [ ] `node demo/smoke_device_icon_pixel_alignment.mjs` covers core bases - 24…112 CSS px in quarter-pixel steps at DPR 1/1.25/1.5/2. DOM centres, - isolated painted centroids/support and a deliberate 1 CSS px mutant must - distinguish browser raster parity from a persistent offset. -- [ ] `node demo/smoke_device_icon_design.mjs` keeps the effective 32/56/96 - geometry, uses the direct 0.55 MDI/core ratio and proves hover plus the - configured action from the far value-capsule end at right/bottom/left/top. -- [ ] Opening binding/registry-less/lock-action smokes preserve the secure - no-toggle-on-plan invariant while checking compact Light/Dark - locked/unlocked/unknown shell/core presentation. -- [ ] `node demo/smoke_opening_entity_search.mjs` checks the real opening - dialog: contact and lock search by friendly name/entity ID, preserved - contact priority, visible IDs, persistent **none** option and unchanged - `opening.contact`/`opening.lock` storage. -- [ ] Unit presentation coverage compares marker LQI colour with the shared - continuous `lqiColor()` across former 40/41 and 179/180 boundaries; bands - remain semantic metadata only. - -## Text marker shell shape (#217) - -- [ ] `node demo/smoke_device_icon_design.mjs` checks the external Text frame, - not only its core: a long value keeps a saturating capsule radius at - 24/32/56/96/112 px, while Icon-only remains circular and Double remains a - capsule. The runtime mutation to `border-radius: 50%` must be rejected. -- [ ] `device-text-shell-long-light` and `device-text-shell-long-dark` isolate a - large `498 ppm` Text marker. Golden review must visibly confirm straight - upper/lower middle sections rather than an ellipse. -- [ ] `node demo/capture_device_icon_reference.mjs` includes an additional - 96 px Text row beside the normative Light/Dark `Text Default.svg`. - -## Device lock and orange foreground palette (#219) - -- [ ] Closed/`locked` is green `#66D17A`; open/`unlocked` is red `#F0410C`. - The same core/stroke palette is used by ordinary lock markers and compact - door/gate lock badges; glyph shape remains closed/open/question. -- [ ] Every device glyph on an orange core (`on`/working and physical `open`) - is white in Light and `#252525` in Dark. `device-icon-state-table-light` - and `device-icon-state-table-dark` show `on` and `open` together, plus - both lock states [unit: device-marker-polish-contract; auto: - smoke_device_icon_design; golden: device-icon-state-table-*]. -- [ ] Alarm, hover, focus, selected, unavailable, virtual, press feedback, - pulse, hit-area and lock actions retain their existing priority and - behaviour [unit: device presentation/polish/pointer; visual source review]. - -## Touch support and release gates - -The product contract is defined in `docs/TOUCH-SUPPORT.md`: - -| Surface | Touch release status | -|---|---| -| View and View dialogs/actions | Required and release-blocking | -| Kiosk/wall tablet | Required and release-blocking | -| Editor entry/exit and no accidental mutation during multi-touch | Safety floor; release-blocking | -| Feature parity of Plan/Device/Background editors | Best effort; not a general release gate | - -All editors remain fully tested on desktop with mouse/keyboard. A touch-editor -failure may be accepted only as a deliberate scope change with updated user -documentation and test classification in the same change. “Best effort” cannot -be used to waive data corruption, unsafe service calls, permission failures, -missing destructive confirmation or an editor exception that breaks View. - -- [ ] Smoke harness itself (v1.43.2, audit T1/T2): every smoke asserts named - facts via `check`/`checkAll` and exits non-zero on any mismatch or - uncaught in-card exception; the suite runs in CI against a FRESHLY built - bundle. Sanity ritual: break one invariant on purpose (e.g. remove the - kiosk editor guard) and confirm the matching smoke goes red [auto: CI job "smoke"] - -- [ ] Room gear discoverability (v1.43.3, user feedback): in the Plan editor - every room card carries a pill button "⚙ Room" of a FIXED readable size - (independent of the card font) — including rooms without a name; it opens - Room settings [auto: smoke_feedback_v2] -- [ ] Metrics readability (v1.43.3): the metrics line is 0.75 of the room name - (was 0.62 — unreadable on tablets); per-room sliders still apply on top [auto: smoke_feedback_v2] -- [ ] Touch tooltips: touch/pen immediately clears hover even if the browser - claims hover support; compatibility mouse is ignored, while a later real - paired-mouse event restores desktop hover without reload - [auto: smoke_feedback_v2] - -- [ ] Light-source flag (v1.44.0, user feedback): a smart SWITCH driving dumb - fixtures creates a Glow pool only once "This device is a - light source" is ticked. External targets under "Controls" still feed - group state/statistics but never create a pool at the switch coordinates; - unticked devices without a light entity never glow [auto: smoke_glow] -- [ ] Device card controls (v1.44.0): the device card opens with its - controllable entities FIRST — toggles right there (≥30 px tap targets), - cover/lock/climate open HA more-info; model, links and manuals moved - below; config/diagnostic entities are not listed; locks never toggle from - the card [auto: smoke_card_controls] - -- [ ] Lock invariant, all paths (v1.44.2, review CR-1): icon tap, controls[], - device card and _cardToggle refuse locks/alarm panels entirely; the door - card's Unlock asks for confirmation, Lock does not [auto: smoke_lock_invariant] -- [ ] Attachment migration is transactional (v1.44.2, review CR-2/CR-3): - rebinding COPIES files, saves the config, and only then deletes the old - folder; a rejected save leaves the old files and urls intact; a name - collision in the destination gets a unique name (the pre-existing file is - never silently linked); urls are rewritten only for confirmed copies - [auto: unit logic.test + tests_backend] - -- [ ] Plans and PDFs load in a real browser (v1.44.3, B1 regression): open a - dashboard with an uploaded plan — the background renders and a manual link - opens; DevTools shows /api/houseplan/content/... returning 200 via a - signed url, while the same url without authSig returns 401 - [auto: tests_backend + manual] -- [ ] Auth policy is single-sourced (v1.44.4, B2): the HTTP upload and every WS - write use the same `may_write`, which denies non-admins when the config - entry is unavailable [auto: tests_backend] -- [ ] Coordinates and caps (v1.44.4, B5): NaN/Infinity are refused on room - rects, polygon vertices, view_box and openings — not only in layout; the - openings list honours MAX_OPENINGS [auto: tests_backend] -- [ ] Drag hardening (v1.44.4, L4 sub-item): every drag pipeline captures the - pointer through the tolerant helper; decor follows the infinite canvas - and stops only at the shared normalized ±5000 garbage bound - [auto: smoke_decor, smoke_drag_bounds] - -- [ ] Room climate counts hidden sensors (v1.44.5): a thermometer that is NOT - placed on the plan (hidden by filtering or by the user) still feeds the - room card, the tooltip and the temperature fill; fridges/TRVs still do - not; an explicit per-room source still wins [auto: unit devices.test] -- [ ] Room climate follows explicit House Plan placement (#317): a real - temperature/humidity sensor moved away from registry Area A votes exactly - once in its marker target, including an area-less `space + room_id` room; - hidden markers still vote, while removed/HA-disabled ones do not. Exact - entity placement wins over its parent device only for that entity, and - explicit room sources remain authoritative [auto: - smoke_room_climate_placement; units: test/devices.test.mjs; mutation: - room-climate-ignores-marker-placement] -- [ ] Room hover + tooltip: in View, hovering any room visibly highlights it - (filled, transparent and area-less alike) and shows its name plus clean- - floor area; temperature/signal follow when available. Thick walls reduce - the area to the inner contour. The wash/halo use plain SVG without CSS - filters, and hovering never replaces or flashes the Glow pool/gradient - DOM. Editors do neither [auto: smoke_ux_fixes + smoke_glow; manual visual] - ## Новый код не добавляет any (#342) ```bash @@ -1232,6 +232,132 @@ tsc, юниты, смоки и мутанты по диффу. Решение х Обойти, как и процессный гейт, можно через `git push --no-verify` — и тогда то же самое найдёт Validate, уже после того как код окажется в `dev`. +## Manifest входов: какие job запускать и что хешировать (#492) + +Один модуль, `scripts/check-inputs.mjs`, объявляет каждую проверку Validate +(`preflight`, `frontend`, `changed_mutants`, `integration`, `smoke`, `golden`, +`performance_smoke`, `backend`) через корни и точки входа, а остальное +вычисляет: от точек входа берётся замыкание — импорты транзитивно, строковые +пути как листья, каталог по строке — все текстовые файлы под ним. Из этого +manifest читают и `classify-changes.mjs` (job `changes`: job запускается, +если дифф задел хотя бы один её вход), и `gate-reuse.mjs` (ключ реюза = +хеш содержимого всех входов job). Два места не могут разойтись: до #492 у +бэкенда ключ не знал relay, converter и schema, а golden/perf — `serve.mjs`, +`demo.html` и compat-хелперов. + +Правила, которые стоит знать: + +- `validate.yml` — вход toolchain каждой job: правка workflow гоняет всё; +- `src/**` — вход только браузерных job, кроме точных cross-runtime входов + `src/plan-optimizer.ts` и `src/logic.ts`, которые backend pytest читает для + parity-контрактов; `demo/fixtures/large-house.mjs` и + `demo/fixtures/visual-matrix.mjs` так же явно входят в backend, потому что + pytest запускает их через Node dynamic import. Точные исключения не делают + весь UI или каталог fixtures входом backend (#542); backend от остального UI + не зависит, а + `custom_components/houseplan/manifest.json` (версия) держит правило «кандидат + релиза прогоняет всё» и для него; +- **неизвестный исполняемый вход** — файл под `scripts/`, `demo/`, `test/`, + `tests_backend/`, `.github/`, `custom_components/`, `src/`, которого нет в + manifest ни одной проверки, — расширяет прогон до полного набора и называется + в summary. Лист покрытия (`node scripts/check-inputs.mjs --coverage`, + `test/check-inputs.test.mjs`) требует, чтобы каждый такой файл был чьим-то + входом либо стоял в `NOT_AN_INPUT` с причиной: новый скрипт без записи — + красный юнит, не вечное расширение прогонов; +- **overlay принятых эталонов** (`demo/golden/baselines/**`, #573) — вход + только `golden`, у которой он стоит явным корнем. Раскрытие каталога по + строке его не выдаёт: корпус отпечатка называет `demo/golden` каталогом, + но берёт из него только `*.mjs`, а до #573 индекс эталонов через это + раскрытие становился входом smoke, perf и 181 из 183 браузерных + свидетелей — приёмка 13 кадров на beta.3 (`ad4000f9`) сменила их ключи и + отпечатки, и второй полный Validate повторил 22 минуты уже сделанной + работы. Явная ссылка на файл индекса (как в `test/check-inputs.test.mjs`) + входом остаётся; +- `--check=` печатает входы, `--why=<файл>` — цепочку, по которой файл + стал входом. + +Отрицательные пробы (`test/gate-reuse.test.mjs`, `test/classify-changes.test.mjs`, +`test/check-inputs.test.mjs`) держат представителей каждой категории входов и +обратную пробу для UI ↔ backend; мутанты `manifest-drops-workflow-input`, +`classify-unknown-input-is-unaffected`, `reuse-backend-hashes-ui`, +`backend-dynamic-inputs-dropped`, `baseline-overlay-leaks-into-every-key`, +`guard-inputs-ignore-wrapper-defaults`, `registry-diff-not-selected`, +`merge-pushes-unvalidated-candidate`, `merge-ignores-lease-rejection`, +`nightly-does-not-wait` держат сам протокол. + +Чистые Python-контракты канонизации, которым не нужен Home Assistant, находятся +в `tests_backend/test_coordinate_canonicalization_pure.py`. Они обязаны реально +исполняться в локальном `pytest tests_backend`, а не исчезать за module-level +`importorskip`; schema/store/virtual-light проверки остаются в HA-зависимом +`test_coordinate_canonicalization.py`. Поведение static-card capability +доказывается unit/smoke-счётчиками и состоянием runtime, а не regex по исходнику +(#440). + +Без установленного Home Assistant (нативная Windows, песочница) HA-харнесс +`tests_backend/test_ha_*.py` **не собирается вовсе** — `conftest.py` исключает +эти файлы через `collect_ignore_glob`, поэтому их нет ни в `passed`, ни в +`skipped`, и зелёная итоговая строка про них ничего не говорит. С #630 pytest в +таком прогоне печатает в шапке и в итоге строку `HA harness NOT collected: N +test_ha_*.py files (M tests)` со ссылкой на канон — Linux CI или WSL +(`bash scripts/wsl-setup.sh --verify`). N и M считаются при каждом запуске по +тому же glob (объявления `def test_*`/`async def test_*`, без размножения +параметризацией), в документах их не переписывают. Проверка — +`tests_backend/test_conftest_harness_notice.py`, мутанты +`ha-harness-notice-silent` и `ha-harness-notice-count-frozen`. + +## Версия в кадрах и попиксельная приёмка (#512) + +Golden-кадры и скриншоты документации не должны меняться от bump версии. Для +golden отображаемая версия идёт через seam `displayVersion()` (`src/card-version.ts`, +глобальная `__HP_VERSION_OVERRIDE__` — только для харнесов; продукт её не задаёт), +и харнес фиксирует `0.0.0-golden`. Для docs-скриншотов есть локальная приёмка +`npm run docs:accept -- --identical`: кадры сравниваются по декодированным +пикселям, и при полном совпадении обновляется только отпечаток исходников. + +## Ядра фронтенда растут только осознанно (#425, заменяет #34) + +- [ ] `node --test test/core-file-budget.test.mjs` — потолки на + `src/houseplan-card.ts` и `src/houseplan-editor-runtime.ts`. + +Гейт-храповик: наверх не пускает, вниз требует зафиксировать выигрыш. Если он +покраснел, вариантов два, и оба нормальные — вынести из ядра столько же строк, +сколько добавили, либо поднять потолок отдельным решением, объяснив его в +ревью. Молча поднять не выйдет: число живёт в тесте и попадает в дифф. + +Вторая половина правила (уменьшение ниже потолка на 250 строк тоже краснеет) +нужна затем, что без неё вынос двух тысяч строк ничего не меняет: потолок +остаётся прежним, и ядро дорастает до него обратно «в рамках бюджета». + +## Локальный CI-совместимый toolchain (#557) + +- [ ] `test/toolchain-pins.test.mjs` проверяет, что явно выбранный Python + используется и для version probe, и для `pip show`, без fallback к + `python`/`python3`/`py` из PATH; строки результата содержат пути Node, + Python, Playwright package и Chromium executable. +- [ ] Тот же unit запрещает Windows setup менять persistent PATH или удалять + существующий venv, требует SHA-256 проверки portable Node и подтверждает, + что WSL verify запускает настоящий HA subset и одну Linux golden-съёмку. +- [ ] На Windows два последовательных + `pwsh -File scripts/windows-toolchain.ps1 setup` проходят: первый ставит + изолированные runtimes, второй переиспользует их; `check` после каждого + зелёный и печатает фактические версии/пути. +- [ ] Из свежего ext4 checkout WSL команда + `bash scripts/wsl-setup.sh --verify` проходит `test_ha_setup.py` без skip, + создаёт непустой `panel-wide-view-light-en.png` и печатает длительность. + Это ранняя обратная связь; независимый exact-SHA Validate остаётся каноном. + +## E2E на реальном Home Assistant (#514) + +Репозиторий `Matysh/houseplan-e2e`: настоящий HA в docker, House Plan из +релиза или из дерева коммита, 13 сценариев Playwright (боковая панель, +дашборды, роли, телефон, PDF, рестарт HA, первый запуск, обновление). Ночью — +по расписанию на последней бете; для **стабильного** релиза `release.yml` +запускает его на **SHA кандидата** (`scripts/e2e-gate.mjs --ref=`, #540: +ставится `custom_components/houseplan` из tarball коммита — то же дерево, из +которого `git archive` строит `houseplan.zip`) и ждёт зелёного: красный — +релиз остаётся черновиком, ассеты не публикуются. В цикле разработки и на +бетах не участвует. + ## Environments matrix Run View/kiosk core flows in every applicable touch environment. Run editor core @@ -1259,1219 +385,95 @@ separately promised workflows: and 390 px without horizontal overflow or clipped actions [golden: German desktop/mobile scenarios; auto: dialog footer width at desktop and 320 px] -## Installation / upgrade / removal +## Touch support and release gates -- [ ] A successful entry setup registers `/houseplan` as `houseplan-panel` only - after store migrations/repairs complete. The panel is visible to admin and - read-only users; actual editor/write access still follows `can_write` and - `admin_only` [auto backend: panel registration/permission tests]. -- [ ] Missing `houseplan-panel.js`, static registration failure, foreign path - collision or panel API exception leaves the integration, WS API and - dashboard card usable. Unload removes only the exact panel object owned by - this setup generation; reload/reconnect cannot duplicate or delete a - replacement [auto backend: panel lifecycle + mutation tests]. -- [ ] Wide View/editor and 320 px read-only/empty `/houseplan` have no horizontal - overflow or duplicate product title. Menu activation emits bubbling and - composed `hass-toggle-menu`; `hass`, `narrow`, `route` and `panel` updates - preserve one child card; leaving the route keeps only the space and returns - in View [auto: `smoke_houseplan_panel`; golden: panel matrix]. -- [ ] The dashboard full card advertises `{columns:"full"}` to Sections while - retaining `getCardSize`; the compact space card has no grid default. Both - stable JS entries and their exact graphs/hashes are verified, the card graph - excludes the panel root, and panel-only gzip stays within 8 KiB [unit: - `houseplan-panel`, bundle manifest/budget/tree/freshness]. -- [ ] README and User Guide in EN/RU each separate Storage mode, HA 2026.2+ - `resource_mode: yaml` and legacy HA 2024.6–2026.1 full-YAML dashboard - setup; both hard-reload shortcuts are present and a flat top-level - `resources:` block is rejected [auto: `check-docs`]. -- [ ] Fresh Storage-mode install from the HACS default catalog (plain search) - → integration appears; the exact `?v=` URL is created or adopted in the - Lovelace resource registry without an `extra_module_url` fallback or a - duplicate. An upgrade updates the one canonical entry [auto backend: - `tests_backend/test_ha_frontend_registration.py`]. -- [ ] `single_config_entry`: adding a second entry is impossible [manual] -- [ ] A pending or transient registry installs the fallback immediately, then - retries exactly once after HA started plus the fixed one-second delay. - Success removes only this setup's exact fallback when supported; unload - before the listener, during the delay or during the task prevents all late - side effects, and reloads do not accumulate listeners [auto backend: - `tests_backend/test_ha_frontend_registration.py`; mutation gate]. -- [ ] HA 2026.2+ YAML resources and a legacy 2024.6–2026.1 full-YAML dashboard - use the truthful `extra_module_url` fallback without a registry write or - polling loop. A Storage dashboard is never instructed to switch to legacy - `mode: yaml` just for House Plan [auto backend: - `tests_backend/test_ha_frontend_registration.py`; auto: `check-docs`]. -- [ ] A missing frontend bundle or failed static-path registration does not fail - integration setup, does not report a successful loader and does not consume - the one-time notification; System Health reports file/static/loader/outcome - honestly [auto backend: `tests_backend/test_ha_frontend_registration.py`, - `tests_backend/test_ha_setup.py::test_missing_frontend_bundle_does_not_skip_backend_setup`]. -- [ ] The first available frontend registration creates one localized persistent - hard-reload notification. Repeated setup, retry completion and a new House - Plan version create no duplicate; uninstall dismisses it best effort [auto - backend: `tests_backend/test_ha_frontend_registration.py`; mutation gate]. -- [ ] System Health preserves existing plan statistics and reports card file, - static path, typed resource outcome, final loader, exact versioned URL, - retry state, safe error and first-notice state without traceback, tokens or - external paths [auto backend: - `tests_backend/test_ha_frontend_registration.py`]. -- [ ] In a full card, every successful `config/get` authoritatively replaces the - backend version. Equal versions and unknown/malformed values show nothing; - a known symmetric mismatch shows one direction-neutral manual-reload banner - in ordinary View/editor/dialog states and never reloads without a trusted - click [unit: `version-recovery`; auto: `smoke_version_recovery`]. -- [ ] In kiosk, an unsafe mismatch preserves the current frame. The first fully - safe idle state stores the backend target before exactly one reload; the same - target after reload/remount or in a second full card gets no second attempt, - a new target gets one, and unavailable `sessionStorage` is manual-only - [unit: `version-recovery`; auto: `smoke_version_recovery`; mutation gate]. -- [ ] `custom:houseplan-space-card` loads the shared bundle/config but never owns - the version banner, safety timer or automatic reload [unit: - `version-recovery`; auto: `smoke_version_recovery`]. -- [ ] Removal deletes every House Plan Lovelace resource entry with the canonical - base URL and dismisses the notification; `.storage/houseplan.*` survives and - reinstall picks the old config up [auto backend: - `tests_backend/test_ha_frontend_registration.py`]. -- [ ] Diagnostics download works; personal fields (name/link/description/pdfs) are `**REDACTED**` [manual] +The product contract is defined in `docs/TOUCH-SUPPORT.md`: -## Modes (v1.25.0) ★ +| Surface | Touch release status | +|---|---| +| View and View dialogs/actions | Required and release-blocking | +| Kiosk/wall tablet | Required and release-blocking | +| Editor entry/exit and no accidental mutation during multi-touch | Safety floor; release-blocking | +| Feature parity of Plan/Device/Background editors | Best effort; not a general release gate | -- [ ] The card always loads in **View**; edit modes are never restored [manual] -- [ ] View: pan/zoom/space-switch/tap/long-press/tooltips only — dragging an icon, - label or opening does nothing; panning may start on top of an icon [manual] -- [ ] View header: space tabs + count + zoom + editor tabs, the general-settings - cog and the per-space gears (visible in EVERY mode since v1.30.1/v1.30.3 - for users who may edit); no editor toolbars [auto: smoke_modes] -- [ ] Space-tab reorder (#243): in an editor with at least three spaces, use a - real mouse drag while browser pointer capture remains on the held tab; - moving left resolves the tab under the cursor and paints its left divider, - moving right paints the right divider, and each valid drop saves exactly - once [auto: smoke_space_tab_reorder; golden: space-tab-drop-before-light, - space-tab-drop-after-dark] -- [ ] Move a held space from a valid target out over the plan: the divider - clears immediately and release does not save. `pointercancel` and removing - the card mid-drag also end the gesture without changing order - [auto: smoke_space_tab_reorder] -- [ ] A sub-threshold mouse gesture remains a tab click; the next click after an - outside release still works. Touch, View and a card fixed to one `floor` - never expose reordering [auto: smoke_space_tab_reorder] -- [ ] Plan: markup toolbar, space gears, +space, ⚙ palette; device icons hidden, - labels/openings draggable; orange stage frame [manual] -- [ ] Devices: icon drag works, click opens the marker editor directly; +/👁/↺/⬡ - buttons; accent stage frame [manual] -- [ ] Mode tabs hidden for non-admin users; segmented control highlights the active mode -- [ ] Openings in View (v1.28.1+): the door/window/gate itself is a pure drawing — no - cursor change, no hover outline, no hit target, no click, regardless of - bindings [manual] -- [ ] The LOCK BADGE is the one exception: when a lock is bound it is shown and - clickable in View (pointer cursor, click → door/lock info card); inert in - Plan so it does not fight editing [manual] -- [ ] Device icons in View show a pointer cursor (no grab); grab only in the - Devices mode [manual] -- [ ] In Plan an opening is interactive: grab cursor, hover outline, drag along - walls, click (any tool) opens its properties — with a 3 px drag threshold - since v1.43.1, so a tap is never swallowed [auto: smoke_inert_openings] -- [ ] Opening drag rulers (2026-08-03): while an opening is dragged, a measure - badge on EACH shoulder shows the along-the-wall distance from the wall end - to the nearest opening edge, live; the wall is ONE room's edge — the edge - the opening is snapped to — so a neighbouring room's collinear edge is - never merged in; at that edge's center (±half a grid step) a - perpendicular dashed tick appears and the center magnet-snaps; there is - no modifier that disables the magnet; badges and tick vanish on release - [auto: smoke_opening_measure + unit openingShoulders] -- [ ] Physical rulers while PLACING a new opening (#238): one room shows two - lines/four endpoint ticks from the preview jambs to the physical inner - face endpoints. A shared wall shows four independently resolved lines/ - eight ticks, two on each room face. A finished independent wall stops per - direction at the nearest physical face of a connected wall/partition and - falls back to its own endpoint where none exists. Lines, ticks and labels - update in the same frame as the preview and are pointer/ARIA inert; saved - opening drag retains the legacy two badges and no new lines - [unit: opening-dimensions; auto: smoke_opening_inner_distances + - smoke_opening_measure] -- [ ] While PLACING a new opening: pressing **Opening** only - opens the shared secondary tray; choosing Window / Door / Gate arms the - 120 / 90 / 300 cm session preset. Moving over a physical wall shows the - complete architectural symbol at 50% opacity, above the masonry, together - with the physical dimension badges and the existing centre tick/magnet. The - preview accepts pointer hits anywhere inside a thick wall body, is absent - on virtual spans and existing openings, and never carries an interactive - or persistent identity. A direct click without prior hover resolves the - same candidate authoritatively and opens its dialog. Save and Cancel keep - the selected preset for repeated placement; tool/mode/space exit and Esc - clear it [unit: opening-placement; auto: smoke_opening_preview + - smoke_opening_measure; golden: opening-placement-door-thick-wall-dark] +All editors remain fully tested on desktop with mouse/keyboard. A touch-editor +failure may be accepted only as a deliberate scope change with updated user +documentation and test classification in the same change. “Best effort” cannot +be used to waive data corruption, unsafe service calls, permission failures, +missing destructive confirmation or an editor exception that breaks View. -## Onboarding ★ +- [ ] Smoke harness itself (v1.43.2, audit T1/T2): every smoke asserts named + facts via `check`/`checkAll` and exits non-zero on any mismatch or + uncaught in-card exception; the suite runs in CI against a FRESHLY built + bundle. Sanity ritual: break one invariant on purpose (e.g. remove the + kiosk editor guard) and confirm the matching smoke goes red [auto: CI job "smoke"] -- [ ] Empty config, HA has floors → floors-import wizard offers them sorted by level [manual] -- [ ] Wizard: uncheck all → "Create" disabled; "Start from scratch" → classic dialog -- [ ] Wizard: N floors → space dialog per floor with prefilled name and progress "i of N"; Skip skips one; Cancel aborts the whole queue [manual] -- [ ] After the last wizard space (or first manual space) → markup mode auto-opens with a toast -- [ ] Empty config, no floors → classic "New space" dialog auto-opens once per session -- [ ] All floors skipped, nothing created → empty state with "Add space" button remains usable - [auto: smoke_optional_space_model] +- [ ] Room gear discoverability (v1.43.3, user feedback): in the Plan editor + every room card carries a pill button "⚙ Room" of a FIXED readable size + (independent of the card font) — including rooms without a name; it opens + Room settings [auto: smoke_feedback_v2] +- [ ] Metrics readability (v1.43.3): the metrics line is 0.75 of the room name + (was 0.62 — unreadable on tablets); per-room sliders still apply on top [auto: smoke_feedback_v2] +- [ ] Touch tooltips: touch/pen immediately clears hover even if the browser + claims hover support; compatibility mouse is ignored, while a later real + paired-mouse event restores desktop hover without reload + [auto: smoke_feedback_v2] -## Spaces ★ +- [ ] Light-source flag (v1.44.0, user feedback): a smart SWITCH driving dumb + fixtures creates a Glow pool only once "This device is a + light source" is ticked. External targets under "Controls" still feed + group state/statistics but never create a pool at the switch coordinates; + unticked devices without a light entity never glow [auto: smoke_glow] +- [ ] Device card controls (v1.44.0): the device card opens with its + controllable entities FIRST — toggles right there (≥30 px tap targets), + cover/lock/climate open HA more-info; model, links and manuals moved + below; config/diagnostic entities are not listed; locks never toggle from + the card [auto: smoke_card_controls] -- [ ] Create with an image (SVG, PNG, JPG, WebP) → correct aspect, crisp at zoom (SVG) -- [ ] Oversized plan (>8 MB) → readable error toast, dialog stays open -- [ ] Create with "No image — I'll outline rooms by hand": orientation landscape/portrait/square respected [manual]; borders+names default ON [manual] -- [ ] Draw-space (no background) renders a WHITE canvas (paper-like), markup works on it; room borders/names stay legible on white [manual] -- [ ] Edit: rename; replace image; **switch image→draw detaches the plan** [manual] -- [ ] Delete space with rooms/devices → tab disappears, layout of other spaces untouched -- [ ] Delete the last space → empty state without console errors; active editor - gestures and drafts are aborted, and creating the first space remains available - [auto: smoke_optional_space_model] -- [ ] Display settings: borders toggle, names toggle, color picker + opacity slider live-preview after save, fill selector [manual] -- [ ] Fill "zigbee": rooms tint red→green by average LQI; rooms without zigbee stay unfilled [manual] -- [ ] Fill "lights": yellow when any light on, grey when all off, unfilled when the room has no lights [manual]; toggling a light from the plan recolors the room -- [ ] Fill "temperature": blue below the comfort range, green inside, yellow above [manual]; comfort bounds editable inline on the radio row (swapped bounds tolerated [manual], clearing a field cannot zero a bound [manual]); rooms without a temperature reading stay unfilled [manual] -- [ ] Fill mode is a radio group (no dropdown); labels carry no color legend -- [ ] Room hover adds a subtle accent wash and double contour without changing - the underlying room fill or Glow brightness -- [ ] Room tooltip shows average room temperature and humidity after the area line and before LQI; missing values are omitted [auto: smoke_ux_fixes] -- [ ] General settings can hide only the room tooltip: default/Cancel/save/reopen - semantics, skipped area work, persistent room highlight, unaffected - device tooltip and restoration on the next mouse move - [auto: smoke_room_tooltip_toggle] -- [ ] Average room temperature counts ONLY thermometer/air-monitor devices — fridges, TRV heads, - smart-plug chip temperatures (`*_device_temperature`) and diagnostic-category temps are excluded [manual] -- [ ] Space dialog is 500 px wide; the comfort-bounds inputs are compact (56 px) -- [ ] The scale input is compact (72 px), not full-width; it shows cm in metric - HA and inches in imperial HA [manual; auto: smoke_space_scale_defaults] -- [ ] A new manual space and every floor-import draft start at 1 cm in metric HA - or exactly 1 inch/2.54 canonical cm in imperial HA. Opening and saving an - existing 5 cm, fractional or missing legacy value without editing the - field is lossless; changing language does not rewrite the canonical draft - [auto: smoke_space_scale_defaults] -- [ ] Physically equivalent rich fixtures at 1 cm and 5 cm have equal View, - Plan-with-grid-masked and static-card pixels/critical bounds. The grid has - five times the intervals only; openings retain their edge hit target, and - physical/screen-fixed layers are not double-scaled - [auto: smoke_grid_scale_invariance; unit: grid-scale.test.mjs, - opening-symbol.test.mjs, canvas.test.mjs] -- [ ] General settings (⚙ in the header): fill colors grouped by mode (lights on/off/none, - temp cold/comfy/hot, LQI weak/strong), each with its own opacity slider [manual]; - Reset restores defaults; saving defaults stores nothing [manual] -- [ ] Custom fill colors apply to the full card AND the static space-card -- [ ] **Room colour follows the room's own mode (#581):** a room colour counts - only with the room's own «Свой цвет»; «Как у пространства» clears the - colour draft at once, the saved room keeps neither `fill_mode` nor - `custom_fill`, and the plan paints the space colour. A colour stored - without the mode (an orphan from an older editor) paints the space colour, - opens as «Как у пространства» without a colour row and is dropped by a - plain save; `name_scale`/`label_scale` survive [unit: `logic.test.mjs` - `#581 AC1`; auto: `smoke_room_settings` step 7, `smoke_space_settings` - `customRoomOrphanInheritsSpace`; mutation: `room-orphan-colour-wins-again`] -- [ ] LQI gradient interpolates between the configured weak/strong colors [manual] -- [ ] Per-space "Show zigbee signal (LQI)" toggle hides/shows the badges next to - devices and the signal line in room tooltips for that space only [manual] -- [ ] Device icon badge is centred exactly on its point (no 1 px down-right drift) [manual] -- [ ] Device glyph is centred within its badge (no vertical drift — real ha-icon is block+line-height) [manual] -- [ ] Room hover highlight still works when custom borders/fills are on -- [ ] Settings persist across reload and other browsers (server-side) +- [ ] Lock invariant, all paths (v1.44.2, review CR-1): icon tap, controls[], + device card and _cardToggle refuse locks/alarm panels entirely; the door + card's Unlock asks for confirmation, Lock does not [auto: smoke_lock_invariant] +- [ ] Attachment migration is transactional (v1.44.2, review CR-2/CR-3): + rebinding COPIES files, saves the config, and only then deletes the old + folder; a rejected save leaves the old files and urls intact; a name + collision in the destination gets a unique name (the pre-existing file is + never silently linked); urls are rewritten only for confirmed copies + [auto: unit logic.test + tests_backend] -## Room markup editor ★ +- [ ] Plans and PDFs load in a real browser (v1.44.3, B1 regression): open a + dashboard with an uploaded plan — the background renders and a manual link + opens; DevTools shows /api/houseplan/content/... returning 200 via a + signed url, while the same url without authSig returns 401 + [auto: tests_backend + manual] +- [ ] Auth policy is single-sourced (v1.44.4, B2): the HTTP upload and every WS + write use the same `may_write`, which denies non-admins when the config + entry is unavailable [auto: tests_backend] +- [ ] Coordinates and caps (v1.44.4, B5): NaN/Infinity are refused on room + rects, polygon vertices, view_box and openings — not only in layout; the + openings list honours MAX_OPENINGS [auto: tests_backend] +- [ ] Drag hardening (v1.44.4, L4 sub-item): every drag pipeline captures the + pointer through the tolerant helper; decor follows the infinite canvas + and stops only at the shared normalized ±5000 garbage bound + [auto: smoke_decor, smoke_drag_bounds] -- [ ] The toolbar has one **Walls** tool and no separate Room outline or - Partition drawing button; Split remains available [auto: - smoke_unified_wall_tool + unified-wall-tool-source.test] -- [ ] Grid appears; dots snap; the wall chain draws pair-by-pair; shared walls reused -- [ ] Ruler: while drawing, the length of the current segment follows the cursor - (metres, or feet+inches on an imperial HA); scale = canonical per-space - `cell_cm` (new-space default 1 cm or 1 inch; missing legacy fallback 5 cm) -- [ ] Every completed segment is saved immediately as one ordinary partition. - Changing tool, editor or floor clears only the session-local chain state; - accepted walls stay unchanged and are not resumed after reload/remount - [auto: smoke_unified_wall_tool + smoke_free_walls + - smoke_plan_snap_overlay] -- [ ] A legacy warm-viewport token `partition` still opens the unified Walls - tool, but `partition` is not a runtime tool-state, dispatch branch or - golden-matrix option [auto: wall-face-graph.test + - unified-wall-tool-source.test + golden-matrix.test] -- [ ] Re-selecting Walls, Reset, pan, pinch, a second pointer, `pointercancel` - and a suppressed synthetic click never finish a chain or save an extra - segment [auto: smoke_unified_wall_tool] -- [ ] The active segment keeps a visible thin axis and endpoint above a thick - preview. Distinct nodes inside the ambiguity radius fail closed with a - zoom prompt. Shift constrains the actual endpoint to the nearest exact - 45° ray, including exact ray/wall intersections; the angle colour follows - the stored vector, not pointer intent [unit: plan-snap-overlay.test.mjs; - auto: smoke_plan_snap_overlay + smoke_plan_drawing_repairs]. -- [ ] With no active chain, a click strictly inside the smallest unoccupied - exact wall face offers a room; boundary/snap hits and Shift bypass it. - Keep/Cancel is a true no-op. If one endpoint→endpoint or - endpoint→solid-line repair closes the face within 2 physical cm, the red - diagnostic is offered and moves geometry only together with Create. - A larger gap, hosted-opening mover or multiple possible repairs fails - closed [unit: wall-face-graph.test.mjs + wall-face-repair.test.mjs; auto: - smoke_plan_drawing_repairs]. -- [ ] Deleting a room uses an accessible Keep walls / Delete walls / Cancel - dialog. Keep materialises only exclusive positive solid intervals as - partitions and rehosts their openings; Delete cascades only openings on - those exclusive walls. Shared walls/openings, explicit partitions and - partition-hosted openings survive. Either accepted choice is one - Undo/Redo/save transaction [unit: room-deletion.test.mjs; auto: - smoke_plan_drawing_repairs]. -- [ ] There is no "Erase" tool in the markup toolbar (removed in v1.19.0) -- [ ] Rooms never overlap (v1.20.0): a click strictly inside an existing room is refused with a - toast; a click ON a shared wall (including mid-span of a longer neighbour wall) still works -- [ ] Closing an outline drawn AROUND an existing room is refused; the outline stays open -- [ ] Merge (v1.21.0): two rooms sharing a wall merge into one; the dialog picks the surviving - name/area; rooms touching only at a corner or apart are refused with a toast -- [ ] Split (v1.21.0): click a room, then two points on its walls — the bigger part keeps the - name/area/devices, the smaller opens the new-room dialog; Cancel leaves the room whole -- [ ] Split: a cut with an end off the wall, or along a wall, is refused with a toast -- [ ] Split: the click snaps to the nearest wall, so it works on non-grid-aligned rooms - (imported/legacy polygons), not only on rooms drawn on the current grid [manual] -- [ ] Split: a click far from any wall (middle of the room) is a miss with a toast — - the wall-snap pull is capped, accidental clicks do not pick a wall [manual] -- [ ] Esc / Ctrl+Z removes the last dot (and its line); Reset clears the active path -- [ ] A latest segment that creates bounded endpoint, T or X faces opens the - room queue in area/key order. Existing exact/partial rooms and physical - gaps, including opening cuts, are excluded; nested rooms remain eligible - [unit: wall-face-graph; auto: smoke_unified_wall_tool + - smoke_room_autoclose] -- [ ] Create and Keep as walls answers are buffered. The last answer applies all - rooms and unconsumed wall atoms as one Undo/Redo transaction; Cancel/Esc - restores the terminal draft without partial rooms [auto: - smoke_unified_wall_tool] -- [ ] A clean divider across one existing room offers only the smaller child; - the larger child keeps the original room id, name, area binding, settings - and device placement [auto: smoke_unified_wall_tool] -- [ ] Room dialog: area list shows only unassigned areas; picking an area prefills the name -- [ ] Room dialog uses the medium width and its body has no horizontal overflow; - long options stay inside it at desktop and narrow widths [auto: - smoke_editor_tabs; manual: narrow viewport] -- [ ] "No area" room (decorative) requires a name; saves with `area: null` -- [ ] Cancel in a Walls face queue restores the persisted terminal draft -- [ ] Saving a room with an area: area devices appear with icons; positions are fixed into the layout [manual] -- [ ] Delete-room consequences are chosen explicitly as described above; there - is no Erase tool -- [ ] Device icons hidden during markup; visible again on exit - -## Devices on the plan ★ - -- [ ] Auto devices appear only in rooms bound to their area [manual] -- [ ] **Entity/parent ownership (#226):** placing `entity:X` removes X from its - automatic parent. A visible unclaimed sibling keeps one residual parent; - an empty or HA-hidden-only residual removes it. State, primary/action, - `allEntities`, light/Glow and LQI cannot see X twice - [auto: unit `devices.test.mjs`; browser `smoke_entity_parent_dedup.mjs`]. -- [ ] Two explicit markers `entity:X` + `device:D` coexist and the device stays - complete. A user-hidden live entity marker still owns X, while an entity - tombstone returns X to the parent; disabled entity ownership follows the - known full-registry relation [auto: unit `devices.test.mjs`]. -- [ ] The #94 curtain boundary is deliberate: untouched hidden `cover.*` stays - cover-first, but after placing the only visible auxiliary switch the - hidden-only automatic remainder disappears. An explicit `device:D` - restores the complete curtain beside that entity marker - [auto: unit `devices.test.mjs`]. -- [ ] Renderer and seeder cannot drift back to exact-binding-only ownership - [mutation: `entity-marker-kept-in-parent-device`, - `entity-marker-parent-seeded`]. -- [ ] Filtering hides bridges/groups/scenes/excluded integrations; 👁 "show all" reveals [manual] -- [ ] Duplicate "name|area" numbered ("Lamp", "Lamp 2") [manual] -- [ ] Light groups fold their single lamps; `group_lights=false` unfolds [manual] -- [ ] Drag anywhere (no edit mode), snaps to grid, persists after reload, per space -- [ ] ↺ reset restores auto layout after confirm -- [ ] Temperature badge on thermometers; LQI value under zigbee icons with red→green color -- [ ] Unified live states (dev, owner 2026-08-05; lock palette #219): actual - work is yellow; open door/window and open valve are orange; unlocked lock - is red and locked lock is green; covers stay neutral and morph their icon; - unavailable is faded. The plate and the activity effect come from the same - semantic resolver -- [ ] State icons (v1.26.0): auto icons morph with state — door/window/garage open↔closed, - lock locked↔unlocked, bulb on; custom icons and unavailable states never morph [manual] -- [ ] display "Value instead of an icon": the marker shows the measurement (°/%/unit) - as its body, small badges hidden; non-numeric fallback keeps the icon [manual] -- [ ] RGB lights (v1.27.0, contract changed in v1.52.0): a lamp's colour lives - in its glow spot and the activity-effect fallback ONLY — the icon/badge/border get - no RGB tint; explicit activity color still wins; off lights unchanged - [auto: smoke_light_badges + smoke_rgb_alarm] -- [ ] Alarm pulse (v1.27.0, unified dev): leak/smoke/gas/CO/siren in `on` - and an alarm control panel in `triggered` - get a red plate and red pulse over every dynamic display mode and even - with ordinary live-state dressing off; `static_icon` is the deliberate - neutral exception after an editor warning; clears on 'off'; unavailable - never alarms [manual]; reduced-motion is static -- [ ] Render cost (v1.43.1, audit L1): geometry (space model, open pairs) is - computed once per config change, not per HA state push — smoke asserts - zero recomputations across 10 state pushes and recomputation after an - edit; the plan still renders dashes/islands correctly [auto: smoke_render_perf] -- [ ] Opening tap vs drag (v1.43.1, audit L4): a tap on a door in the Plan - editor opens its properties (3 px threshold like the other pipelines) and - writes nothing; a real drag that ends where it started also writes nothing [auto: smoke_render_perf] -- [ ] Concave containment (v1.43.1, audit G2): an island room inside a U- or - L-shaped parent is accepted and punches the evenodd hole; a traced - duplicate outline is still NOT containment [auto: smoke_inert_openings] -- [ ] Backend hardening (v1.43.1, audit B2-B5): the admin check fails closed - when the entry is unavailable; layout/set and config/set without - expected_rev may bootstrap revision zero, but over a non-empty store each - returns `conflict` without changing its document/rev/backups or firing an - event (including a no-op body); explicit stale revisions are rejected; a - production config-writer inventory requires expected_rev; - NaN/Infinity coordinates and oversized collections are rejected - [auto: tests_backend/test_ha_websocket.py + coordinate-write-barrier-guard.test] -- [ ] Save race (v1.43.0, audit L2): make a markup edit, then press Save in any - dialog within 500 ms (or let another client save) — the markup edit must - survive and reach the server; a failed reload now shows a toast [auto: unit: tests_backend] -- [ ] Niche split (v1.43.0, audit G1): a cut that starts AND ends on the same - wall carves a niche; the two parts' areas must sum to the original (the - invariant is enforced in code and asserted for every split test) [auto: smoke_save_race] -- [ ] Authenticated content (v1.43.0, audit B1): plan images and marker files - are only reachable through /api/houseplan/content/… with a session; the - old /houseplan_files/plans|files paths return 404 after a restart; old - stored URLs keep working (rewritten on read) [auto+manual] -- [ ] Every editor option is storable (v1.45.3, issue #3): set a sensor to - "value instead of an icon" and save — no validation error, the value shows - on the plan after a reload. Same for each tap action and each fill mode - [auto: backend test_every_display_mode_the_editor_offers_is_accepted and - neighbours, test_a_marker_showing_its_value_can_be_saved] -- [ ] Room settings button (dev): detached from the (movable) name label — - always at the room's geometric centre, one button-height below it; sized - at 70% of a device icon and zooming WITH the plan; the small metric rows - under the room name now show in the plan editor too - [auto: smoke_room_cards gearDetached/plainInPlan] -- [ ] Working plate remains universal: in a source-glow space a lit lamp or - other actually working device stays yellow in View and in every editor; - the glow pool is an additional spatial indicator, not a replacement - [auto: smoke_light_badges] -- [ ] Size/angle parity (v1.52.1, HP-1513-01): a marker with size 3 / angle 37 - scales x3 and rotates on BOTH cards [auto: smoke_size_angle_parity] -- [ ] Tap runs an automation (dev, owner's spec 2026-07-29): the tap-action - list has "Run automation/script/scene" with a searchable picker; saving - without a target is refused; the confirm checkbox guards toggle AND run - (our dialog, Esc/cancel = no call); automation.trigger / script.turn_on / - scene.turn_on per domain; a deleted target toasts and calls nothing - [auto: smoke_tap_run + unit resolveToggleIntent/runServiceFor + backend - test_run_target_is_bounded_to_runnable_domains] -- [ ] Universal Toggle state (#94): the option is visible for device, entity - and virtual markers; the separate Open/close option is absent. The hint - names the exact target(s), current state, next effect and skipped refs. - Exact entity never retargets to a sibling; explicit controls never fall - back to the controller; partial groups call exactly the shown available - subset; a no-target tap is a quiet no-op. Locks, alarm panels and - garage/door/gate covers are explained secure no-ops. Cover/valve use - closed→open, open→close, moving→stop when supported, otherwise their - domain toggle. Confirmation re-resolves current state but cancels if the - target set changed. Opening and saving an untouched legacy `cover` or an - absent light default preserves the original token/absence; an intentional - selector edit writes `toggle`. Feature-gated climate/water-heater/siren/ - camera/media-player/legacy-vacuum entities require their exact HA bits; - an empty service catalog is unsupported. If #73 retains an older visual - device while live controls change, click calls only the current controls - [auto: test/device-toggle.test.mjs + smoke_cover_tap + - smoke_cover_not_primary + smoke_controls + backend action-schema parity] - The synthetic HA fixture publishes its service catalog explicitly, so - browser checks exercise the same fail-closed resolver as production -- [ ] A cover is NEVER painted (dev, owner 2026-08-04): «у штор не должно быть - жёлтой подложки никогда, индикация открыто/закрыто за счёт морфинга - иконки». Walk one curtain through closed / open / ajar / opening / - closing: the plate is the plain neutral badge every time — never the - yellow «включено» one, never the orange «открыто» frame it used to wear - while open — the icon is the only open/closed signal, and the breathing - `.activity-transition` ring appears in the two travelling states and - nowhere else when «Icon + activity» is selected. - The morph is exhaustive: every device class gives two DIFFERENT glyphs - (awning included), a cover with no device_class morphs within its own - auto-icon family (mdi:roller-shade, mdi:garage-variant), a hand-picked - icon morphs only inside the pair it was picked from, and an - unknown/unavailable state morphs nothing. NOT touched: an open door / - window binary sensor and an open valve still wear the orange «открыто» - frame (a valve has no icon pair, so the frame is all it has); lock keeps - its separate red-unlocked/green-locked palette [auto: - smoke_cover_no_plate + unit stateIcon «every class, both ways»] -- [ ] Cover target and indication parity: on a device where a hidden functional - `cover.*` competes with an auxiliary option switch, the shared resolver - selects the cover, calls only it and supplies the same entity to icon - morph/activity. A mixed lamp+cover remains a lamp unless the universal - toggle result actually selects the cover. Cover presentation remains - neutral in every state and cannot be painted yellow by its controls; - secure cover classes call nothing and never fall back to info - [auto: smoke_cover_not_primary + smoke_cover_plate_precedence + - test/device-toggle.test.mjs] -- [ ] Light-source badges (current contract): in glow fill a lit lamp's badge - stays yellow just like a lit socket; the pool keeps the source's RGB while - the marker keeps semantic yellow. Other fills behave identically; - morphing and the activity-colour fallback survive [auto: smoke_light_badges + - smoke_rgb_alarm] -- [ ] Icon size multiplier scales the glyph (dev): set a marker's size to 3 — - the icon inside grows with the badge instead of staying default - [auto: smoke_icon_scale] -- [ ] Auto-grid parity (v1.51.2, HP-1511-01): with an empty layout, a visible - device among hidden ones sits at the same spot on both cards - [auto: smoke_hidden_flag autoGridParity] -- [ ] Activity ghost (v1.51.2, unified dev): a hidden icon+activity marker - shows its base icon and no effect [auto: smoke_hidden_flag rippleGhost*] -- [ ] Hidden LQI parity (v1.51.1, HP-1510-01): a room whose only Zigbee - devices are hidden paints the same lqi fill on the full and the static - card [auto: smoke_hidden_flag lqiParity] -- [ ] Ghost shows no numbers (v1.51.1, HP-1510-02): a hidden value-display - device renders as a plain ghost — no value/temp/hum/LQI, no icon morph - [auto: smoke_hidden_flag ghostHidesValue] -- [ ] Hide-from-plan flag (dev, docs/FILTERING.md): every existing device - dialog has a bottom-left "Hide" / "Show" action, incl. virtual; the - change is applied by Save; hidden devices vanish from every mode and - the count, still count toward room LQI, cast no glow/light fill; the - device editor's "Show hidden" (local, per tab) shows them as BLUE - dashed ghosts — distinct from a grey unavailable icon — with NO live - state paint (no yellow, no alarm, no activity); - showing and saving keeps a hidden:false marker (re-seed protection); an old - config materialises on first load by an editing client and legacy - clients keep the old behaviour until then - [auto: smoke_hidden_flag + unit seedHiddenBindings/seeded/legacy] -- [ ] Yellow means working (dev): a TRV whose hvac_action is heating glows - yellow; one that is merely enabled (idle) or has a service switch on - (anti-scaling, child lock) stays dark; a lit light turns its state on by - the same condition that lights the glow pool and shows the yellow badge - even where that pool is drawn - [auto: smoke_yellow_principle + smoke_light_badges] -- [ ] Activity baseline (beta.10 audit): rebuilding the device registry seeds - the current snapshot immediately, so the very first later motion/event - transition is detected. Rebinding a marker's effective source clears the - old source's finite flash in the same update [auto: smoke_motion_sense] -- [ ] Touch gesture ownership (dev): in the plan editor on a phone, pinch zooms - and a moving finger pans; releasing after a gesture does not draw a point - and a clean tap still does. In View, both marker-first and stage-first - pinches change zoom; the latter holds its second contact on the marker for - more than 600 ms. Neither order opens the local card, HA more-info, - confirmation/service or a compatibility click/contextmenu action. Reverse - release, a third contact, pointer cancel and lost capture stay blocked, - while the next deliberate single-touch tap and long press work once - without waiting; real mouse and keyboard context menus remain available - [unit: touch-gesture-click-guard; auto: smoke_editor_gestures; mutations: - touch-pinch-click-block-cleared-on-terminal, - touch-pinch-marker-hold-rearmed, - touch-pinch-contextmenu-guard-removed] -- [ ] Legacy geometry parity (v1.50.4, HP-1503-01): a store with a zero - viewport and a negative rect renders identically sane in BOTH cards — - full canvas fallback, normalised rectangle [auto: smoke_legacy_geometry] -- [ ] Sizes are positive (v1.50.3, HP-1502-01): view_box or room w/h of zero - or below is refused; a store that already holds one opens on the full - canvas, not a blank screen [auto: test_sizes_are_not_coordinates + unit - safeViewBox fallback] -- [ ] Room card layout (v1.50.3): the settings button is the bottom row of the - card and the room name sits in the same spot in view and plan modes - [manual; verified by vb-coordinate measurement] -- [ ] Geometry bounds (v1.50.2, HP-1501-01): a config with a 1e100 room - vertex is refused by the server; one already stored still renders with a - sane frame [auto: test_geometry_magnitudes_are_bounded + unit - contentBounds legacy case] -- [ ] No-op repair (v1.50.2, HP-1501-02): geometry/repair with a typo'd space - id errors, moves no revision and keeps the previous backup undoable - [auto: test_a_noop_repair_does_not_eat_the_backup] -- [ ] Card below other dashboard content (v1.50.1, HP-1500-02): place the card - after a tall card in a normal dashboard — the plan still gets most of the - viewport instead of a zero-height stage [auto: smoke_zoom_out] -- [ ] Frame never degenerate (v1.50.1, HP-1500-03): a space with one lone - marker opens with canvas around it, not an empty scene; an absurd stored - coordinate neither hides the plan nor is accepted by the server - [auto: unit contentBounds + backend test_layout_coordinates_are_bounded] -- [ ] Stranded migration repair (v1.50.1, HP-1500-01): geometry/repair with - dry_run previews, applies with a backup, undo restores; wrong space is - recoverable [auto: test_geometry_repair_is_explicit_previewable_and_undoable] -- [ ] Editors see the whole canvas (v1.50.0, HP-1490-03): a hand-drawn space - with one small room opens content-fit in View; switching to the plan - editor shows the full square with room to draw a second room far away; - back to View restores the content fit [auto: smoke_audit_1490] -- [ ] Save waits for a picked plan's proportions (v1.50.0, HP-1490-04): pick a - saved plan and hit Save before the thumbnail loads — the stored aspect is - the real one, never the previous file's [auto: smoke_audit_1490] -- [ ] Zoom goes below the fit (v1.50.0): minus past 100% floats the plan - centred, floor at 0.4x; entering an editor keeps the stage inside the - viewport [auto: smoke_zoom_out] -- [ ] Migration crash recovery (v1.50.0, HP-1490-01): kill HA between the two - store writes of the square migration — the next start finishes the layout - half from the saved intent - [auto: test_square_migration_finishes_after_a_crash_between_the_writes] -- [ ] Parallel upload quota (v1.50.0, HP-1490-02): two simultaneous uploads - with one slot left — exactly one succeeds - [auto: test_parallel_uploads_cannot_slip_past_the_quota_together] -- [ ] Zoom opens on the content (v1.49.0): a space with no background and one - small room opens with that room filling the screen, with a small margin. - With a background it still fits the whole image - [auto: unit: contentBounds] -- [ ] Deleting a picked plan is refused (v1.49.0, HP-1470-02): pick a saved - plan, reopen the list — its delete button is disabled. Ask the server to - store a plan url whose file is gone: `missing_plan`, and the revision does - not move [auto: smoke_saved_plans + backend - test_config_set_refuses_a_plan_that_no_longer_exists] -- [ ] Portable import rechecks local content under its paired-write lock: a - plan or marker PDF deleted after preview fails with `missing_plan` or - `missing_content`, and neither store advances - [auto: backend test_apply_rechecks_plan_file_under_the_write_lock + - test_apply_rechecks_attachment_under_the_write_lock] -- [ ] Uploads are bounded (v1.49.0, HP-1470-01): past the store quota an upload - is refused with a clear error and the disk does not grow; the plan list - returns the newest 60 with a total - [auto: unit: test_check_quota_counts_the_whole_store_not_one_request, - backend test_uploads_are_bounded_by_a_store_quota] -- [ ] An attachment already written to staging is not reserved twice: with the - real 512 MiB disk reserve intact its same-filesystem promotion succeeds, - while one byte below the reserve still fails. Uploads checked before any - bytes are written continue to reserve their full incoming size - [auto: backend test_issue_554_low_disk_reserve_distinguishes_staged_and_unwritten_bytes - + test_issue_554_upload_uses_actual_free_space_after_staging; mutation: - quota-reserves-staged-bytes-twice-on-disk] -- [ ] Square canvas migration (v1.48.0): after the upgrade every existing plan - looks exactly as before, just with margins where the canvas was extended. - Measure a wall in the plan editor — the length in cm is unchanged. Marker - positions, doors, decor and the saved zoom are all where they were - [auto: unit: test_a_wide_plan_gains_margins_above_and_below and neighbours, - test_migration_preserves_real_lengths_and_shapes] -- [ ] A plan image is centred (v1.48.0): a wide image sits in the middle with - empty bands above and below, a tall one with bands at the sides, and it is - never stretched [auto: unit: fitInSquare + smoke_space_settings] -- [ ] Re-attaching a detached plan (v1.47.0): detach a plan, save, RELOAD THE - PAGE, open space settings → "Already uploaded" → the image is listed with - its size and no "in use" note → attach it → it renders. The one a space - uses shows that space and cannot be deleted; a free one can, with a - confirm, and disappears from the list - [auto: smoke_saved_plans + backend test_stored_plans_can_be_listed_and_deleted_on_request] -- [ ] Detaching a plan keeps the file (v1.46.6): switch a space to "draw" and - SAVE — the image is still in `config/houseplan/plans/` right afterwards, - and after a restart, and can be re-attached. Deleting the space keeps it - too. Replacing a plan still removes the one it replaced, immediately. - Check straight after the save: the earlier bug deleted the file at that - moment, while every scheduled-pass test passed - [auto: unit: test_plan_collection_matrix, test_attachment_collection_matrix, - backend test_detaching_a_plan_keeps_the_file] -- [ ] Rebinding a device does not eat its manuals (v1.46.5): attach two files to - a device, rebind it to another HA device — both are readable afterwards. - If a copy failed, the file it failed on is still there rather than deleted - with the folder [auto: backend test_files_cleanup_keeps_referenced_files] -- [ ] Nothing accumulates on an idle instance (v1.46.2/v1.46.3, HP-1461-01, - HP-1462-01): attach a file, cancel the dialog, and do not save anything - else — the file is gone after a restart AND after the daily pass, while - every file the configuration still references is untouched. Seed the - strays AFTER the last save, or `config/set` collects them and the check - proves nothing - [auto: backend test_startup_sweep_collects_what_no_commit_will, - test_daily_sweep_callback_collects_too, test_sweep_and_a_config_write_do_not_race] -- [ ] A drag wins over a concurrent remote move (v1.46.2, HP-1461-02): drag an - icon and, while the save is still in flight, have another window move a - different icon — your icon stays where you put it and the other one - updates [auto: smoke_layout_sync] -- [ ] Concurrent uploads of one name (v1.46.1, HP-1460-01): attach the same - file from two browser tabs at once — two attachments, two sets of bytes, - neither lost. A file whose name is at the length limit still downloads - [auto: unit: test_reserve_filename_is_safe_under_concurrency and neighbours] -- [ ] No temporary files survive (v1.46.1, HP-1460-02): abort a large upload - mid-transfer, send two files in one request, make promotion fail — in each - case the files folder holds no `.upload-*`. An old one is swept at startup - [auto: backend test_upload_leaves_no_temporary_behind + unit: sweep_upload_temps] -- [ ] Two full cards agree on positions (v1.46.1, HP-1460-03): open the plan in - two windows, drag an icon in one — it moves in the other without a reload; - a drag in progress in the second window is not thrown away - [auto: smoke_layout_sync] -- [ ] Uploaded SVG is inert as a document (v1.46.0, HP-1454-01): open a plan's - signed url directly in a tab — a `