# Полевая проверка View, аудит доступности и геометрический корпус (#560) Дерево проверки: `dev` на 13.09.2026 (после #556). Всё, помеченное «проверено исполнением», снято прогоном на этом дереве; всё остальное помечено как чтение или арифметика. ## 0. Что было и чего не было Было: чтение реальной установки владельца (только чтение, по его прямому разрешению), синтетический стенд `demo/` в двух ширинах, узловые тесты модели. **Не было, и это не оговорка задним числом:** добровольцев кроме владельца, физического Companion на телефоне/планшете, записи в реальную установку, проверки со скринридером и с симуляцией цветовой слепоты. Пункты, которые без этого не проверяются, названы ниже гипотезами, а не находками. ## 1. Профиль реального плана (чтение, 13.09.2026) Модель v10, `rev` 2740, интеграция 1.76.0-beta.1. | что | сколько | |---|---| | пространств / комнат | 5 / 37 | | записей толщины / сегментов стен | 121 / — | | проёмов / перегородок / колонн | 35 / 7 / 2 | | маркеров | 156 | | шаг сетки по пространствам | 5, 1, 30, 1, 1 см | | толщины стен в одном пространстве | 20/22/28/29/33 · 20/30/40 · 4/15/17/33 · 10/15 | | минимальное ребро комнаты | 5 см (шаг 1 см) и 10 см (шаг 5 см) | | T-узлов по пространствам | 7 / 6 / 3 / 2 / 0 | | X-узлов (степень ≥ 4) | **ни одного** | | координат всего / с шумом ближе 1e-4 шага | 1010 / 23 | | почти осевых стен (`classifyNearAxisSegment`) | 0 | | проёмов, чей край лежит ровно в вершине комнаты | 1 | | записей `layout` на УДАЛЁННЫЕ пространства | 4 (на 2 пространства) | | ссылок на сущности HA в маркерах и проёмах | 42 | | из проверенных 25 ссылок не существует в HA | 1 | Инварианты модели на этом конфиге чистые: `node scripts/model-invariants.mjs` сообщает «ссылки разрешимы, записи толщины находятся». **Геометрия владельца в репозиторий не попала.** Фикстуры корпуса синтетические и воспроизводят перечисленные условия числами. Это и есть обезличивание: в репозитории нет ни плана дома, ни имён комнат, ни идентификаторов сущностей. ## 2. Household-сценарии Стенд: `demo/smoke_household_journeys.mjs`, штатная ширина карточки 780 px (настенный планшет) и 390 px (узкая колонка телефона). У каждого пути назван независимый оракул — не «класс появился», а вызов сервиса, доступное имя или содержимое модели. | путь | вход | оракул | результат | |---|---|---|---| | J1 «экран проснулся» | вкладка, не грузившая редакторский чанк | `_booting === false`, `.stage` видима, комнат 4, устройств 10 | пройден | | J2 «где протечка» | датчик протечки уходит в `on` | доступное имя называет состояние (`Sink leak sensor, Alarm, …`), `data-state="alarm"` | пройден, см. F4 | | J3 «сколько в спальне» | температурный маркер со значением | значение попадает в `aria-label` (`Temperature: 22.4°`), 7 из 10 маркеров несут число в имени | пройден | | J4 «выключить свет с клавиатуры» | Tab до маркера, затем Enter | вызван `light.turn_off`; кольцо фокуса `#0C82F0` против `transparent` у остальных | пройден; до первого маркера 15 остановок Tab | | J5 «переключить этаж» | клик по вкладке пространства | `_space` сменился, набор комнат перерисован, активная вкладка соответствует пространству | пройден, см. F3 | | J6 «вернулся после обновления» | конфиг приходит заново | выбранное пространство сохранено, план и устройства нарисованы | пройден | | J7 «нажать нужный светильник» | пять маркеров с шагом 0,0333 — самая плотная расстановка из поля | в центре ВИДИМОГО маркера нажатие достаётся ему же | 780 px — пройден; **390 px — не пройден, F1** | ## 3. Аудит доступности текущего View (проверено исполнением) Что уже сделано правильно и должно остаться: - маркер устройства с действием — `role="button"`, `tabindex="0"`, осмысленное `aria-label` с названием, состоянием и значением; - Enter и Space на маркере действительно вызывают действие (`_keyDevice` → `_clickDevice` → `light.turn_off` в прогоне); - фокус видим: `:focus-visible` красит кольцо `#0C82F0` (у остальных `transparent`), скриншот вокруг маркера меняется; - зона нажатия увеличена сознательно: `.dev::before` — круг `max(44px, --device-shell-size)`, то есть 44 px при любом размере маркера. Это выше минимума WCAG 2.5.8 (24 px) — и именно это порождает F1 при плотной расстановке. Измеренные проблемы — F1…F5 ниже. ## 4. Геометрический корпус `test/fixtures/560-corpus/` — шесть планов; `test/geometry-corpus.test.mjs` — 26 проверок; `demo/smoke_geometry_corpus.mjs` — браузерная половина. | фикстура | условие | откуда взято | |---|---|---| | `c1-short-edges` | уступ в два шага (10 см при шаге 5 см) | минимальное ребро поля | | `c2-t-junction-thicknesses` | T-узел, где сходятся три разные толщины | 7 T-узлов и 5 толщин в поле | | `c3-x-junction` | четыре комнаты в одной точке | **синтетика: в поле X-узлов нет** | | `c4-opening-at-vertex` | край проёма ровно в вершине комнаты | один такой проём в поле | | `c5-node-noise-near-axis` | шум у узла плюс честно наклонная стена | 23 шумных координаты в поле | | `c6-stale-layout` | `layout` на удалённое пространство | 4 таких записи в поле | Цепочка на каждом плане: импорт → инварианты → Optimize → инварианты → Optimize → Resize → Optimize → сохранение и перечитывание. Оракулы численные: - нарушений инвариантов ровно столько, сколько заявлено (для c1–c5 — ноль); - Optimize не меняет вход (immutability, как в #281); - приведённый план Optimize не трогает (`changed === false`), а второй проход не двигает ничего никогда — фикс-пойнт #477; - записи толщины целы (`checkWallRecordsPreserved`); - ключи записей канонические (`wallKey` пересчитывается независимо); - **наклонная стена не выпрямляется**: `classifyNearAxisSegment` не относит наклон 1 шаг на 60 к почти осевым, и Optimize оставляет её как есть. Это прямая проверка приёмки «корректная планировка не исправляется ради удобства рендера»; шум у узла, наоборот, снимается — и это правильно. Рендер эталоном не является: браузерная половина проверяет присутствие и число узлов (комнаты, проём, заливка стен), попадание геометрии в `viewBox` и сохранность набора толщин после ШТАТНОГО ресайза — то, что чистыми функциями не проверяется, потому что перенос записей толщины делает хост карточки (`project()` в `ResizeController.move`), а не `resize.ts`. **Граница узловой половины названа прямо:** вход её шага Resize — «рёбра сдвинуты, записи толщины на прежних координатах», состояние, которого штатный writer не создаёт. Поэтому там проверяется сходимость Optimize за ≤ 2 прохода, а не «за один». ## 5. Находки ### F1 (High) — в узкой колонке нажатие достаётся соседнему маркеру Проверено исполнением на осевшей геометрии (замер повторяется до двух одинаковых подряд — правило #533). При ширине карточки 390 px маркеры имеют размер 10,84 px, а расстояние между центрами при самой плотной расстановке поля (шаг 0,0333) — 11,8 px. Зона нажатия каждого маркера — круг 44 px (`.dev::before`, `pointer-events: auto`, `z-index: 3`), то есть ±22 px от центра. Круг соседа накрывает центр маркера целиком, и выигрывает тот, кто выше по порядку. Что измерено: центр `dense1` (203; 461,08) принадлежит `dense2` (`elementFromPoint` и реальный `page.mouse.click` — обоими). Для пяти маркеров владельцы центров: `dense2, dense3, dense4, dense5, dense5`. То есть **ни один из первых четырёх маркеров нельзя нажать в его собственном центре**. Порог: расхождение начинается, когда расстояние между центрами меньше 22 px (половина фиксированного круга). На 780 px тот же план даёт 23,6 px между центрами — проверка проходит, но запас 1,6 px. Почему это важно: J3 «безопасно действовать» — на телефоне пользователь включает не тот свет, и никакого признака ошибки нет. ### F2 (Medium) — подписи комнат: контраст 2,01:1 при норме 4,5:1 Тёмная тема по умолчанию: цвет подписи `rgb(85,96,108)` на фоне стейджа `rgb(28,37,48)`, `opacity: 0.8`, размер 12,05 px. Контраст по чистому цвету 2,41:1, с учётом смешивания полупрозрачности — **2,01:1**. WCAG 1.4.3 (AA) для такого размера требует 4,5:1. Механизм: цвет подписи — это цвет комнаты (`disp.color`), а прозрачность — `min(1, room_opacity + 0.25)`. Отдельного токена для текста плана нет, поэтому подпись нельзя сделать читаемой, не перекрасив сами комнаты. ### F3 (Medium) — активная вкладка этажа не имеет программного состояния `