diff --git a/docs/specs/369-audit-lows.md b/docs/specs/369-audit-lows.md new file mode 100644 index 00000000..b7f60026 --- /dev/null +++ b/docs/specs/369-audit-lows.md @@ -0,0 +1,117 @@ +# ТЗ #369 — Пачка Low аудита 29.08 (а–ж) + +- Issue: https://github.com/Matysh/houseplan-card/issues/369 +- Приоритет: P3, polish; полный трек (семь несвязанных поверхностей — критерий + «одна поверхность» §5 не выполняется, при том что каждая правка тривиальна) +- Ревизия: 1 (2026-08-29) + +## Сценарий + +Семь мелких шероховатостей, найденных аудитом beta.2/beta.3 — каждая по +отдельности не тянет на issue, вместе дают заметный полиш. Пользователь видит: +честное предупреждение в логе вместо молча пропавшего следа пылесоса; климат +в старых конфигурациях без HA Area; отсутствие «живых» маркеров у сознательно +отключённых устройств; превью мебели, реагирующее на Shift мгновенно; никаких +случайных установок мебели правой кнопкой. **До**: семь тихих странностей. +**После**: каждая либо починена, либо честно задокументирована. + +## Контракты по пунктам + +**(а) Undelete пылесоса теряет след — документируется.** Поведение осознанное +(#335: purge tombstone-трейлов на `config/set`), правится документацией: +абзац в `docs/VACUUM.md` («удаление маркера пылесоса стирает его серверный +след немедленно; восстановление маркера начинает след заново») + зеркальная +строка в USER-GUIDE (en/ru) в теме пылесосов. Кода нет. + +**(б) NaN-калибровка молча прячет след.** `smoothVacPath` (`src/vacuum.ts`) +отбрасывает сегмент с нефинитной точкой без следа в консоли. Добавить +`console.warn('[houseplan] vacuum trail segment dropped: non-finite point', …)` +**однократно на вызов** (не на сегмент — файл может содержать сотни), с +идентификацией количества отброшенных сегментов. Функция остаётся чистой по +возврату; warn — единственный побочный эффект, отключаемый параметром +`warn = console.warn` (юнит подменяет). + +**(в) Легаси-маркер с `area === undefined` не получает климат #317.** +`markerClimateTarget` (`src/devices.ts:1406`) проверяет `marker.area === null`; +экспортные/легаси конфиги несут отсутствие ключа (undefined). Заменить на +`marker.area == null` (ловит оба) — существующий кейс «маркер в Area» не +задет (`marker.area` строка). Юнит: маркер `{space, room_id}` БЕЗ ключа area +получает `roomClimateKey`, как и с явным `null`. + +**(г) «Живой» маркер при полностью отключённых сущностях.** #318 +(`controllerAvailability`, `src/device-presentation.ts:213`) считает +entityless-active устройство доступным. Уточнение: если у устройства В +РЕЕСТРЕ есть собственные сущности, но все они отключены пользователем +(`d.entities` пуст, а `d.bindingStatus.kind === 'ha_disabled'` ЛИБО +`allEntityIds` непуст при пустом enabled-ростере) — это не «нет свидетельств +offline», а «пользователь сознательно выключил»: возвращать текущее поведение +ДО #318 (unavailable/приглушён). Истинно безростерные контроллеры (#318, +реестр пуст) остаются available. Юнит рядом с тестами #318 на оба случая. + +**(д) Превью мебели не реагирует на Shift без движения мыши.** Пока палитра +мебели активна и превью видимо, runtime держит keydown/keyup-слушатели +(`Shift`) на `window` (снятие — вместе с деактивацией инструмента/палитры и в +`disconnected`-очистке жестов), обновляющие `free`-флаг превью и +`requestUpdate`. Паритет с кликом сохраняется — клик уже читает +`ev.shiftKey`. + +**(е) Правый клик ставит мебель/декор.** В точке принятия placement-жеста +(`_stagePointerDown` → decor/furniture ветки) добавить фильтр +`ev.pointerType === 'mouse' && ev.button !== 0 → return` — правая/средняя +кнопка не ставит объект. Touch/pen (`button` 0/-1) не задеты; контекстное +меню браузера над стейджем ведёт себя как раньше. + +**(ж) Имя правообладателя furniture-pack.** Решение владельца 2026-08-29: +автор — **Sergey Matyunin / Сергей Матюнин**. Исправить +`assets/furniture/houseplan-0.3.0/LICENSE.md`, `README.md` и `pack.json` +(поле author) с «Sergey Matyushin» на «Sergey Matyunin». Root `LICENSE` +(JB/justbusiness) не трогается. + +## AC + +1. (а) VACUUM.md + оба USER-GUIDE содержат абзац; check-docs зелёный. +2. (б) юнит: путь с NaN-точкой — warn вызван ровно один раз с числом + отброшенных сегментов; чистый путь — warn не вызван; возврат функции + байт-в-байт как сейчас (существующие юниты без правок). +3. (в) юнит: маркер без ключа `area` + `space`+`room_id` → room-ключ климата; + с `area: 'kitchen'` → 'kitchen' (регресс-защита). +4. (г) юниты: (i) устройство с пустым реестром + active → available (как + #318); (ii) устройство с непустым `allEntityIds`/ha_disabled при пустом + enabled-ростере → НЕ available. Существующие тесты #318 зелёные без правки. +5. (д) смок: палитра мебели активна, превью видимо — нажатие Shift без + движения мыши меняет `free`-флаг превью (и обратно на keyup); слушатели + сняты после закрытия палитры (счётчик addEventListener/removeEventListener). +6. (е) смок: right-button pointerdown по стейджу с armed-мебелью — объект НЕ + поставлен; левый клик — поставлен (регресс). +7. (ж) grep «Matyushin» по репо пуст; pack.json/LICENSE.md/README.md несут + «Sergey Matyunin»; SHA-256 архива-источника в README не пересчитывается + (архив не менялся — меняются только метаданные пака; расхождение имени с + внутренностями архива оговаривается строкой в README). +8. Мутанты: (б) warn выброшен → юнит красный; (в) `== null` → `=== null` → + юнит красный; (г) уточнение снято → юнит красный; (е) фильтр кнопки снят → + смок красный. + +## Риски + +- (г) меняет видимое поведение недавнего #318 — сужение честное (реестр + непуст ⇒ свидетельство есть), но требует внимательного ревью формулировки + условия; откат точечный. +- (д) слушатели на window — риск утечки; лечится симметричным снятием и + проверкой счётчиком в смоке. +- Остальные пункты — точечные, риск минимальный. + +## Release-артефакты + +- CHANGELOG en+ru: одна запись пачкой (User-Visible: yes — пункты в, г, д, е). +- Документация: VACUUM.md, USER-GUIDE en/ru (пункт а). +- i18n: не требуется (новых строк UI нет). +- golden/performance: не задеты (проверить прогоном обычного гейта). +- security: не затрагивается; (ж) — правка метаданных лицензии. + +## Вне скоупа + +Изменение семантики purge (а) кодом; любые новые пороги/поведение trails. + +## Откат + +Один revert; конфиг/схема/контракты не меняются.