Files
houseplan-card/legacy/docs/QUALITY-560.md
T
Claudeandclaude[bot] c081f59a15 chore(hygiene): убрать мёртвые скрипты и старый архив, разовые документы — в legacy/ (#678)
Волна 1 эпика #674 — мёртвое без риска, каждое имя проверено git grep по
текущему dev.

Удалено: шесть demo/shot_*.mjs без читателей (shot_furniture ещё и не
работает с #159), demo/capture_wall_strip_backup.mjs,
demo/benchmark_coordinate_write_barrier.mjs (нигде не запускался, но входил
в манифест smoke через demo/benchmark_*.mjs — теперь лист покрытия чист),
scripts/dev/styles-split.mjs (падает на текущем src/styles.ts),
docs/README.ru.md (индекс четырёх документов из 38 — роль у README.ru.md),
demo/README.md (одна строка в карте пакета AGENTS.md вместо него) и из
legacy/ — снимок аудита v1.58.0, черновик 089, завершённые планы,
продуктовые снимки и две разовые диагностики; история git хранит.
legacy/docs/SUN-CONTRAST.md остаётся: на него ссылаются src/sun.ts и SUN.md.

Перенесено в legacy/docs/: superpowers/specs (11 дизайн-документов до
процесса), QUALITY-560.md, ROADMAP.md, STATUS-FEATURES.md. Ссылки
поправлены там, где они были: FILTERING.md, ТЗ 006/007/058, testing-notes,
комментарии src/open-spans.ts и validation.py (только путь документа),
SCOPE.md, STATUS.md, smoke_household_journeys.mjs, опись legacy/README.md;
ложный маркер benchmark_coordinate_write_barrier снят с чек-листа.

Issue: #678
User-Visible: no
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
2026-09-27 16:23:39 +00:00

16 KiB
Raw Blame History

Полевая проверка 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) — активная вкладка этажа не имеет программного состояния

<button class="tab active"> не несёт ни aria-current, ни aria-pressed, ни aria-selected (проверено чтением атрибутов в прогоне: все три null), а полоса вкладок — обычный div, не tablist. Какой этаж открыт, сообщается только классом, то есть видом. WCAG 4.1.2.

F4 (Low) — состояние в доступном имени повторяется дважды

Sink leak sensor, Alarm, Alarm, LQI 117, medium signal — слово состояния попадает в имя из двух источников. Скринридер произносит его дважды.

F5 (Low) — подсказка с названием и метриками не показывается при фокусе

Один и тот же узел подсказки: при pointerover — display: block и текст «Ceiling light / Smart Bulb E27», при клавиатурном фокусе — display: none. Данные не теряются для скринридера (они есть в aria-label), но зрячий пользователь клавиатуры их не получает.

F6 (Low) — записи layout на удалённые пространства переживают Optimize

Проверено на c6-stale-layout: Optimize видит их (positionsUnresolved: 2), но оставляет, и npm run invariants на экспорте пользователя сообщает о них как о нарушениях. В поле таких записей четыре на два удалённых пространства. Вреда пользователю нет; цена — ложный сигнал при будущем разборе.

6. Гипотезы UX (не находки, проверять с людьми)

  • U1. Проём, чей contact удалён из HA, рисуется ОТКРЫТЫМ: openingAmount при отсутствующем состоянии возвращает 1 для дверей и калиток (для окон 0). Это отказ в безопасную сторону, но признака «состояние неизвестно» нет. В поле ровно один такой случай: калитка ссылается на binary_sensor, которого в HA больше не существует (404) — то есть на плане владельца калитка нарисована открытой постоянно.
  • U2. До первого маркера на плане 15 остановок Tab: сначала весь хром.
  • U3. Состояние маркера зрительно передаётся заливкой (#F0A00C против светлой/тёмной) при одинаковой иконке и размере. Светимости различаются сильно, поэтому нарушения 1.4.1 здесь скорее нет, но без проверки с симуляцией цветовой слепоты это гипотеза.

7. Что заведено отдельно

Продуктовые правки в этой задаче не делались — она research/tests-only. F1, F2–F5 и F6 заведены отдельными issue со ссылкой на #560; свидетель к каждой приедет вместе с правкой, а не поселится здесь зелёной проверкой, узаконивающей дефект.