diff --git a/docs/specs/267-device-presentation-decision-table.md b/docs/specs/267-device-presentation-decision-table.md new file mode 100644 index 00000000..9b411618 --- /dev/null +++ b/docs/specs/267-device-presentation-decision-table.md @@ -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`. + diff --git a/docs/specs/README.md b/docs/specs/README.md index cb745b30..029f41bb 100644 --- a/docs/specs/README.md +++ b/docs/specs/README.md @@ -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) |