docs: specify device presentation decisions

Issue: #267
User-Visible: no
This commit is contained in:
Matysh
2026-08-27 21:17:18 +03:00
parent 2294d46f3f
commit c5e145269b
2 changed files with 549 additions and 0 deletions
@@ -0,0 +1,548 @@
# Issue #267 — таблица решений и явная политика «лица» маркера
- **Issue:** https://github.com/Matysh/houseplan-card/issues/267
- **Тип / приоритет:** tech-debt + docs / P2
- **Оценка:** пользовательская ценность 4/10; ценность для разработки 9/10;
сложность 7/10; риск 6/10
- **Класс изменения:** A + B + C, полный процесс
- **User-Visible:** no — вид, тексты, действия, конфигурация и порядок
приоритетов остаются прежними
- **Связано:** #34, #98, #251, #274; `docs/ARCHITECTURE.md`,
`docs/USER-GUIDE.ru.md` §12
## 1. Сценарий и персона
Домашний администратор или член семьи открывает View/киоск и видит маркеры
устройств; администратор также открывает тот же маркер в редакторе устройств и
смотрит preview. Для человека этот слайс обязан быть незаметен: один и тот же
маркер при тех же данных HA выглядит, подписывается и реагирует точно как до
рефакторинга, а план и preview продолжают давать один ответ.
Разработчик позднее добавляет новый источник, статус или вариант отображения и
может до изменения кода прочитать одну ограниченную таблицу: какой источник
становится «лицом», какой статус побеждает, что рисуется и почему. Изменение
политики требует нового именованного ряда и теста, а не ещё одного условия в
центральном резолвере.
### Что человек увидит
Ничего нового: прежние форма, подложка, иконка/значение, бейджи, LQI,
пульсация, доступность и действие сохраняются во всех текущих поверхностях.
## 2. Проблема и измеренная база
На `origin/dev@2294d46`:
- `src/device-presentation.ts` — 781 строка и 78 условных ветвлений;
- `resolvePresentationSources()` — 152 строки;
- `resolveDevicePresentation()` — 182 строки;
- поведение уже разделено между `device-visual.ts`, `device-pulse.ts`,
`device-value-badge.ts`, `device-face.ts` и поверхностными renderers, но
приоритеты lifecycle / source / availability / status / display выражены
последовательностью условий без одного проверяемого контракта;
- #251 и #274 доказали реальный класс ошибки: состояние управляемой цели,
доступность физического контроллера и состав draft-roster являются разными
осями, но по текущему коду их приоритет трудно предсказать.
Проблема не в количестве строк как таковом. Нет единственной именованной
политики, связывающей входные факты с итоговым marker face; существующие тесты
покрывают важные истории, но не образуют обозримую закрытую матрицу.
## 3. Зафиксированные продуктовые решения
Задача не принимает новых продуктовых решений. Источники истины:
1. `docs/USER-GUIDE.ru.md` §12 — подложка, активность, четыре display-режима,
value fallback, LQI, static icon и таблица типов устройств.
2. #251 — контроллер показывает работу цели, но его полупрозрачность зависит
только от собственных активных сущностей; виртуальный контроллер доступен.
3. #274 — сохранённый план и preview получают одинаковый полный marker-roster;
tombstone цели не превращает живой wireless controller в unavailable.
4. #98 — alarm является критическим эффектом, обычная пульсация доступна только
в `icon_ripple`, статичных колец нет.
5. `static_icon` подавляет всю live presentation, но не hover/focus, действия,
controls, Glow и room-light aggregation.
6. `removed` не является отображаемым lifecycle-состоянием: tombstone
исключается до вызова presentation resolver. HA-disabled и user-hidden
сохраняются в модели, но их видимость зависит от поверхности.
7. Surface interactivity не переносится в semantic resolver. Таблица фиксирует,
существует ли видимый marker/action hit-area; точный click/toggle/security
routing остаётся в действующих surface/controller модулях.
Если реализация обнаружит расхождение между этими решениями и текущим
поведением, она **не исправляет его попутно**. Расхождение записывается в #267 с
минимальным воспроизведением и становится отдельным продуктовым issue либо
явным решением владельца. До решения refactor сохраняет текущий выпуск.
Открытых продуктовых вопросов нет.
## 4. Термины и оси
- **Binding lifecycle** — `active`, `ha_disabled`, `orphaned`, user-hidden или
removed/tombstone до построения `DevItem`.
- **Face source** — `cover`, `controls`, собственный `light`, функциональная
`device_role`, `primary` fallback либо `none`.
- **Controller availability** — доступность физического контроллера по его
собственным активным сущностям; external controls сюда не входят.
- **Source visual** — объединённые availability/status/activity источников
отображения и critical siblings.
- **Face policy** — чистое упорядоченное решение из уже вычисленных фактов:
visibility, effective visual, форма icon/value, live-icon/color/metrics,
pulse eligibility, vacuum live и explanation.
- **Surface policy** — View/киоск, Device editor, preview или static card;
владеет DOM hit-area и действиями, но не выбирает новое визуальное состояние.
- **Decision row ID** — стабильный идентификатор сценария вида `L*`, `S*`,
`F*`, `A*`; используется в документе, fixture, тесте и mutation evidence.
## 5. Канонический артефакт после реализации
Создаётся `docs/DEVICE-PRESENTATION.md`. Это developer-facing канон, а не новый
пользовательский backlog. Он ссылается на §12 user guide и содержит четыре
непересекающиеся таблицы:
1. lifecycle/visibility;
2. выбор face source и controller/target availability;
3. финальная форма/подложка/иконка/значения/диагностика;
4. activity/pulse.
Каждый ряд содержит:
| Поле | Содержание |
|---|---|
| ID | стабильный `L01`, `S01`, `F01`, `A01` |
| Вход | только значимые оси; неуказанные оси обозначены «не влияет» |
| Победившее решение | именованный production decision ID |
| Видимый результат | форма, цвет/подложка, icon/value, подписи и pulse |
| Интерактивность | отсутствует / surface-owned normal / preview/static inert |
| Доказательство | fixture row + unit/smoke + mutant ID |
Документ не строит декартово произведение. Ряды выбираются по публичному
контракту, уже исправленным полевым дефектам и точкам приоритета. Редкая
комбинация, не меняющая победителя и результат, ссылается на существующий ряд,
а не дублирует его.
## 6. Минимальный набор рядов
Точные формулировки уточняются по коду при создании канонического документа,
но следующий набор нельзя сокращать без записи в ревью ТЗ.
### 6.1 Lifecycle / visibility
| ID | Сценарий | Обязательный результат |
|---|---|---|
| L01 | `removed:true` tombstone | не передаётся presentation resolver, marker и hit-area отсутствуют |
| L02 | HA-disabled в View/киоске/static card | marker и hit-area отсутствуют; данные не участвуют в presentation |
| L03 | HA-disabled в служебном Device editor | диагностический marker/preview с причиной, без live state/action |
| L04 | user-hidden в View/киоске/static card | marker и hit-area отсутствуют |
| L05 | user-hidden в design preview | реальный сохранённый дизайн видим, notice `hidden_design_preview`, preview inert |
| L06 | orphaned/unverified binding | сохраняется действующая диагностическая проекция и причина; pulse/service не оживают |
### 6.2 Source / availability
| ID | Сценарий | Обязательный результат |
|---|---|---|
| S01 | cover является функциональной ролью/primary | `sourceKind=cover`, icon morph берётся с exact cover |
| S02 | cover лишь побочная capability рядом с light/device role | cover не перехватывает face |
| S03 | active external controls | face показывает aggregate target status, availability — собственный controller |
| S04 | target controls unavailable, controller имеет live battery/LQI/update | доступный нейтральный controller, не `unavail` |
| S05 | target работает, все собственные controller entities недоступны | faded controller; target work не перекрашивает его в yellow и не даёт pulse |
| S06 | virtual controller с controls | доступен по определению, status следует target |
| S07 | tombstone фильтрует все saved controls | controller-role сохраняется; live diagnostics определяют availability (#274) |
| S08 | manual virtual light #107 с исходящими controls | собственный manual source владеет face |
| S09 | owned real/forced light | собственный light source владеет face |
| S10 | нет cover/light/controls, есть functional role | `device_role` и его aggregate visual |
| S11 | registry role временно отсутствует, есть live primary | deterministic `primary` fallback без смены сохранённого binding |
| S12 | нет пригодного source | neutral icon fallback; не выбирается случайная sibling entity |
| S13 | critical alarm sibling вне обычного face source | alarm добавляется к semantic aggregate и имеет публичный приоритет |
### 6.3 Face / content / diagnostics
| ID | Сценарий | Обязательный результат |
|---|---|---|
| F01 | dynamic + alarm | red alarm state; alarm precedence над working/open |
| F02 | dynamic + unavailable | faded neutral plate; hover styling/pulse отсутствуют, обычный surface action сохраняется |
| F03 | lock locked/unlocked | green/red lock classes и согласованный a11y state |
| F04 | working available | yellow plate |
| F05 | open available (не cover) | orange plate |
| F06 | neutral available | theme-neutral plate |
| F07 | `live_states:false`, не alarm | neutral plate/base icon; ordinary activity disabled |
| F08 | `static_icon` | base icon, neutral plate, без state/icon morph/RGB/value/metrics/pulse/vacuum live |
| F09 | `value` + ровно один scalar source | Text face с HA-formatted full value |
| F10 | `value` + missing/unavailable/non-scalar source | icon fallback с точной explanation reason |
| F11 | `value` + несколько равноправных sources | icon fallback `value_ambiguous_sources` |
| F12 | `value` + virtual marker | icon fallback `value_virtual` |
| F13 | dynamic icon + известный state morph | state icon; manual override сохраняет действующее исключение cover |
| F14 | explicit value badge | позиция/availability/tone из одного resolver; bottom смещает LQI |
| F15 | legacy automatic metric | действующая temperature/humidity эвристика без материализации config |
| F16 | LQI 0/40, 41/179, 180+ | low/mid/high band и continuous canonical colour; static/hidden suppresses LQI |
| F17 | live vacuum marker / static marker | live overlay только в dynamic visible face, dock/action contract не меняется |
### 6.4 Activity / pulse
| ID | Сценарий | Обязательный результат |
|---|---|---|
| A01 | alarm в любом dynamic display, даже `live_states:false` | red alarm pulse; static/hidden/disabled всё равно подавляет его |
| A02 | ordinary activity не в `icon_ripple` | pulse отсутствует, notice `activity_display_disabled` при применимости |
| A03 | witnessed event/terminal transition в окне 3.3 s | short pulse с generation/deadline; expired window не оживает |
| A04 | presence | continuous green pulse пока активно |
| A05 | transition | continuous blue pulse пока активно |
| A06 | running/working | continuous amber/live-RGB/configured pulse пока работает |
| A07 | unavailable/hidden/disabled/static | никакой pulse, даже при retained runtime window |
| A08 | reduced motion | ordinary pulse заменён dot; alarm остаётся семантически красным без ordinary dot |
## 7. Архитектурная граница
### 7.1 Новый pure policy-модуль
Создаётся `src/device-presentation-policy.ts`. Он получает уже разрешённые
semantic facts, не читает HA registry/state и не строит light graph. Модуль
владеет только **приоритетом** финальной проекции:
- lifecycle/effective visibility;
- выбор controller availability против target aggregate;
- alarm / live-state / unavailable / working / open / neutral precedence;
- static/dynamic/value face gates;
- разрешение dynamic icon, metrics, RGB, pulse и vacuum-live;
- canonical explanation reason и стабильные production decision IDs.
Ожидаемая форма результата (имена можно уточнить без изменения границы):
```ts
interface DevicePresentationPolicyInput {
bindingLifecycle: 'active' | 'ha_disabled' | 'orphaned' | 'unverified';
userHidden: boolean;
designPreview: boolean;
display: DeviceDisplayMode;
liveStates: boolean;
sourceKind: PresentationSourceKind;
sourceVisual: DeviceVisualState;
controllerFace: boolean;
controllerAvailability: DeviceAvailability;
valueAvailable: boolean;
valueFallback: ValueFallbackReason | null;
}
interface DevicePresentationPolicyResult {
effectiveHidden: boolean;
visual: DeviceVisualState;
face: 'icon' | 'value';
dynamicIcon: boolean;
metrics: boolean;
liveColor: boolean;
pulseEligible: boolean;
vacuumLive: boolean;
reason: PresentationReason;
decisionIds: readonly string[];
}
```
Production decision IDs описывают **правила**, а не test fixture. Они не
выводятся пользователю и не сохраняются в config. Их можно хранить в
`ResolvedDevicePresentation` как readonly diagnostics либо возвращать только
из pure policy helper; renderer не ветвится по ним.
### 7.2 Что остаётся в существующих модулях
- `device-visual.ts` классифицирует entity и агрегирует semantic visual;
- `device-pulse.ts` остаётся единственным resolver эффекта pulse;
- `device-value-badge.ts` остаётся единственным resolver satellite badge;
- `device-face.ts` остаётся одним renderer shell/core DOM;
- `device-toggle.ts` и surface handlers владеют action/security/interactivity;
- `resolvePresentationSources()` продолжает владеть HA/light graph, но
возвращает дополнительно явный source-decision ID либо эквивалентный trace,
чтобы S01…S13 не зависели от чтения порядка `if`;
- `houseplan-card.ts`, `space-card.ts`, `space-render.ts` не получают права
переопределять policy result.
Запрещены второй light resolver, второй entity-role resolver, перенос HA
данных в policy-модуль и branching в renderer по raw HA state.
### 7.3 Упорядоченная политика
Вместо взаимозависимых inline-условий policy задаётся небольшими именованными
ступенями либо ordered rule arrays. Обязательный порядок:
1. lifecycle/visibility;
2. static face;
3. critical alarm;
4. live-state gate;
5. controller-vs-source availability;
6. stable status;
7. icon/value fallback;
8. diagnostics/live colour/vacuum;
9. pulse resolver.
Одна ступень имеет один владеющий helper и один decision ID. Последующая
ступень не пересчитывает решение предыдущей из raw inputs.
## 8. Исполняемая матрица и защита от дрейфа
Создаётся `test/fixtures/device-presentation-decisions.mjs` (или typed fixture
с тем же назначением). Каждая строка §6 имеет ровно один fixture с тем же ID и:
- минимальным `hass`/registry/marker input;
- ожидаемыми `sourceKind`, source decision ID и policy decision IDs;
- ожидаемыми visibility, classes, icon/value, metrics/LQI, pulse и reason;
- ожидаемой surface interactivity там, где строка её задаёт.
Отдельный contract test читает `docs/DEVICE-PRESENTATION.md` и fixture:
1. множества row IDs совпадают точно;
2. ID уникальны и отсортированы;
3. у каждого ряда есть test assertion и mutation evidence;
4. ни один production decision ID не остаётся без документационного ряда;
5. неизвестный decision ID ломает тест, а не становится молчаливым fallback.
Тест не сравнивает только `decisionIds`: он проверяет наблюдаемый projection.
Иначе trace мог бы быть правильным при неправильной подложке.
## 9. Mutation contract
Каждый ряд документа обязан уметь покраснеть. Это доказывается не изменением
expected fixture, а мутантом production-ветки.
- Ряд с уникальным правилом получает собственный mutant ID.
- Несколько рядов одного правила могут ссылаться на один mutant только если
focused guard перечисляет/проверяет каждый их row ID.
- Уже существующие mutants #251/#274/LQI переиспользуются и получают ссылки в
таблице; дублировать их нельзя.
- Для новых policy priorities в `scripts/mutation-gate.mjs` добавляются
mutants как минимум для lifecycle, static-before-live, alarm, unavailable,
value fallback, metrics gate и ordinary-pulse display gate.
- `test/mutation-gate.test.mjs` и decision-table contract проверяют, что каждый
указанный mutant существует и его guard включает focused matrix test.
До S7 запускаются `--check` и новые/изменённые mutants по ID. Полный mutation
набор остаётся stable-release gate.
## 10. Surface parity
Один `ResolvedDevicePresentation` остаётся входом для:
- интерактивного full plan;
- `houseplan-space-card` / static render;
- device-dialog preview.
Для одинакового marker roster, HA snapshot и presentation options итоговые
semantic поля совпадают. Разрешённые различия перечислены явно:
- `designPreview` показывает user-hidden design и notice;
- static surface не создаёт action hit-area;
- preview может запросить `sourceDetails:true` для объяснения static face;
- preview demo-pulse оборачивает уже resolved pulse и не меняет HA/config.
Production-path browser smoke строит минимум одну сложную fixture: physical
wireless controller, live diagnostic siblings, unavailable/filtered target,
value badge и activity setting. План и preview сравниваются по face DOM,
classes и explanation; smoke должен поймать возврат #274.
## 11. Скоуп
- новый pure presentation-policy boundary и явные decision IDs;
- приведение `resolveDevicePresentation()` к этой политике без изменения
публичного результата;
- source-decision trace без изменения source semantics;
- `docs/DEVICE-PRESENTATION.md` с матрицами §6;
- полный fixture/test row set, parity test и targeted smoke;
- mutation evidence каждой строки;
- обновление `docs/ARCHITECTURE.md`, `docs/specs/README.md`, тестовой
документации и source-fingerprint публичных screenshots штатным процессом.
## 12. Не-скоуп
- новые статусы, цвета, иконки, display modes, badges или действия;
- изменение правил устройства любого домена;
- изменение light membership, Glow, room fill/statistics или toggle targets;
- новый пользовательский diagnostics UI;
- изменение config/schema, миграция или compatibility fields;
- перенос `resolvePresentationSources()` целиком в новый framework;
- исправление найденного расхождения без отдельного решения;
- изменение CSS/DOM/pixels либо принятие новых golden-baselines.
## 13. Данные, compatibility, i18n, touch и безопасность
- **Данные/миграция:** отсутствуют; serialized marker/config/layout byte-for-byte
не меняются.
- **Compatibility:** `normalizeDeviceDisplay()` остаётся обязательным read gate;
legacy `ripple` и все unknown marker fields сохраняют текущий round-trip.
- **i18n:** новых пользовательских строк нет; EN/RU dictionaries не меняются.
- **Touch:** View/киоск имеют тот же hit-area и действие; editor touch остаётся
desktop-first best effort. Новых gestures/listeners нет.
- **Безопасность:** lock/alarm/toggle policy не переносится и не ослабляется;
presentation decision IDs не содержат entity IDs, имён или состояний.
- **Privacy:** fixtures синтетические; docs и test output не включают real HA
registry/config.
## 14. Производительность
Policy — O(1) по уже разрешённым фактам и не читает полный registry/light graph.
Source resolution сохраняет текущий local fast path и один pre-resolved
whole-plan light graph. Запрещено:
- искать docs/fixture row at runtime;
- сериализовать весь input ради decision ID;
- добавлять per-marker registry traversal сверх существующего;
- добавлять DOM measurement/observer или повторный presentation resolve.
Performance smoke остаётся prerelease gate. На code review достаточно
статического подтверждения O(1) policy, unit benchmark/счётчика отсутствия
дополнительного `resolvedLightSources()` на marker и штатного build-size diff.
## 15. Acceptance criteria
### AC1 — каноническая таблица
`docs/DEVICE-PRESENTATION.md` содержит все ряды §6, их visible result,
interactivity и evidence. Терминология совпадает с RU user guide; документ
связан с issue, architecture и spec.
**Доказательство:** docs contract unit + code review.
### AC2 — один pure policy owner
Lifecycle/display/status/content/diagnostics gates из
`resolveDevicePresentation()` перенесены в один pure typed policy boundary;
renderer и surfaces не принимают альтернативных решений по raw HA state.
**Доказательство:** unit + architecture/source-boundary test + code review.
### AC3 — source decisions названы
Каждая ветка `cover|controls|light|device_role|primary|none` и critical sibling
возвращает стабильный decision ID/trace; S01…S13 проверяют наблюдаемый output.
**Доказательство:** parameterized unit.
### AC4 — каждый ряд исполняем
Множества row IDs документа и fixture совпадают. Каждый fixture вызывает
production resolver (либо documented pre-resolver lifecycle owner для
removed/disabled visibility) и проверяет полный значимый projection.
**Доказательство:** matrix contract + parameterized unit.
### AC5 — каждый ряд умеет падать
У каждого ряда есть существующий mutation ID; удаление/перестановка
соответствующей production policy красит focused guard. Новые/изменённые
mutants проходят `--check` и целевые mutation runs.
**Доказательство:** mutation registry contract + targeted mutation-gate runs.
### AC6 — controller/target regressions защищены
S03…S07 включают точные сценарии #251 и #274: controller availability, target
working/unavailable, event `unknown`, live battery/LQI/update, filtered target
tombstone и preview roster parity.
**Доказательство:** unit + production browser smoke; существующие #251/#274
mutants продолжают красить тест.
### AC7 — surface parity
Full plan, static card и preview получают одну semantic projection; различия
ограничены §10. Сложная fixture даёт одинаковые face DOM/classes на плане и в
preview, кроме явно разрешённых diagnostics/demo differences.
**Доказательство:** unit/source contract + targeted browser smoke.
### AC8 — refactor-only pixel/config contract
До/после не меняются marker DOM/CSS, пользовательские строки и serialized
config. Полный golden verify зелёный без изменения baseline PNG; публичные docs
screenshots после штатного CI capture имеют те же пиксели, меняется только
source fingerprint при необходимости.
**Доказательство:** git diff/code review, `golden:verify`, docs screenshot gate,
config round-trip unit.
### AC9 — fast path и bounds
Policy O(1); обычный marker не начинает разрешать whole-plan light graph;
pre-resolved graph по-прежнему строится один раз на render pass.
**Доказательство:** unit call counter + code review; prerelease performance gate.
### AC10 — штатные гейты
`typecheck`, `npm test`, production build и три bundle snapshots,
`check-docs`, `smoke-select` и выбранные smokes зелёные. Geometry/backend gates
неприменимы, если diff не выйдет за заявленный scope.
**Доказательство:** команды и результаты в handoff и code review.
## 16. План тестов
1. **Pure policy unit:** priority rows lifecycle/static/alarm/live/availability/
status/value/diagnostics.
2. **Parameterized decision matrix:** один production call на каждый ID §6.
3. **Document-fixture-mutant contract:** exact row-set parity, decision ID
coverage и существование mutation evidence.
4. **Existing regression suites:** `device-visual`, `device-pulse`,
`device-presentation`, `device-face`, `device-value-badge`, `devices`.
5. **Surface contract:** full/static/preview не строят своё состояние.
6. **Browser:** существующий `smoke_wireless_controller_parity` расширяется
либо добавляется узкий matrix smoke, без дублирования большого golden UI.
7. **Golden:** полный verify без baseline acceptance.
Перед S7 обязательны:
```text
npx tsc --noEmit
npm test
npm run build
npm run bundle:sync
node scripts/check-docs.mjs
node scripts/smoke-select.mjs --base origin/dev --head HEAD
node demo/smoke_wireless_controller_parity.mjs
npm run golden:verify
node scripts/mutation-gate.mjs --check
node scripts/mutation-gate.mjs --id=<каждый новый/изменённый presentation mutant>
```
## 17. Release-артефакты
- `User-Visible: no`; `docs/CHANGELOG.md` и `docs/CHANGELOG.ru.md` не меняются.
- Обновляются `docs/DEVICE-PRESENTATION.md`, `docs/ARCHITECTURE.md`,
`docs/specs/README.md` и при необходимости testing/development docs.
- Новых RU/EN user-guide формулировок нет: §12 остаётся продуктовым каноном.
- Golden baselines не меняются и не принимаются.
- Source fingerprint публичных docs screenshots обновляется только штатным
reviewed CI artifact; локально снятые PNG не принимаются.
- Release note отсутствует; задача может быть включена в общий refactor/small
fixes пункт только если release manager сочтёт это нужным.
## 18. Риски и меры
| Риск | Мера |
|---|---|
| Таблица просто перепишет текущие `if` | product rules из §3 первичны; расхождение не чинится скрыто |
| Trace правильный, пиксели неправильные | каждый ряд проверяет observable projection, не только ID |
| Матрица превратится в декартово произведение | четыре узких таблицы; новый ряд только при новом победителе/результате |
| Новый policy станет вторым resolver | он получает готовые facts, raw HA/light graph запрещены |
| Preview снова разойдётся с plan | exact roster fixture + production browser parity + #274 mutant |
| Mutation gate станет чрезмерно дорогим | mutants переиспользуются по правилу; targeted runs до S7, полный набор только stable |
| Рефактор случайно изменит pixels | DOM/CSS вне scope, full golden verify без acceptance |
| Decision IDs попадут в публичные данные | bounded constants only, no serialization/UI |
## 19. Откат
Обычный revert коммита возвращает inline policy. Миграции и новых полей нет,
поэтому rollback не требует переписывать config/layout или восстанавливать
данные. Документ/fixture/mutants откатываются вместе с policy, чтобы не оставить
ложный канон.
## 20. Технические допущения — можно менять на ревью
1. Policy живёт в отдельном файле, а не как группа pure helpers в текущем;
важен один owner и отсутствие raw HA, не имя файла.
2. Decision IDs могут возвращаться внутренним trace либо отдельным debug helper;
они не обязаны увеличивать renderer-facing interface.
3. L01/L02/L04 доказываются соответствующим lifecycle owner до resolver, потому
что «не вызвать resolver» нельзя честно доказать его собственным output.
4. Один mutant может доказывать несколько rows только при явном перечислении их
ID focused guard-ом; уникальный mutant на каждую почти одинаковую fixture не
является самоцелью.
5. Existing browser smoke расширяется только если это остаётся узким сценарием;
иначе создаётся новый `smoke_device_presentation_matrix.mjs`.
+1
View File
@@ -146,6 +146,7 @@ GitHub Issues и GitHub Projects (v2) остаются единственным
| [#258](https://github.com/Matysh/houseplan-card/issues/258) Канонический wall key после Optimize и storage round-trip | [258-wall-key-storage-roundtrip.md](258-wall-key-storage-roundtrip.md) |
| [#262](https://github.com/Matysh/houseplan-card/issues/262) Повторное добавление entity после удаления родительского устройства | [262-readd-child-entity-after-device-delete.md](262-readd-child-entity-after-device-delete.md) |
| [#265](https://github.com/Matysh/houseplan-card/issues/265) Единый контракт ссылочного шва импорта | [265-import-reference-seam.md](265-import-reference-seam.md) |
| [#267](https://github.com/Matysh/houseplan-card/issues/267) Таблица решений для «лица» маркера | [267-device-presentation-decision-table.md](267-device-presentation-decision-table.md) |
| [#274](https://github.com/Matysh/houseplan-card/issues/274) Беспроводной контроллер одинаково выглядит на плане и в preview | [274-wireless-controller-presentation-parity.md](274-wireless-controller-presentation-parity.md) |
| [#294](https://github.com/Matysh/houseplan-card/issues/294) Esc завершает текущую цепочку стен без удаления геометрии | [294-wall-esc-detach.md](294-wall-esc-detach.md) |