From d8e3b82da6cd5ae56fa6461f5ffb6e33afb2b48a Mon Sep 17 00:00:00 2001 From: Sergey Matyunin Date: Sun, 16 Aug 2026 22:13:15 +0300 Subject: [PATCH] docs: revise washer lifecycle specification Issue: #164 User-Visible: no --- docs/specs/164-washer-active-cycle.md | 197 ++++++++++++++++++-------- 1 file changed, 138 insertions(+), 59 deletions(-) diff --git a/docs/specs/164-washer-active-cycle.md b/docs/specs/164-washer-active-cycle.md index 0bd32d0b..8d3aa937 100644 --- a/docs/specs/164-washer-active-cycle.md +++ b/docs/specs/164-washer-active-cycle.md @@ -1,6 +1,6 @@ # Issue #164 — активный цикл стиральной машины должен быть жёлтым -Статус: **первая редакция, S3-spec; на ревью не отправлено** +Статус: **вторая редакция после ревью, S3-spec; повторно на ревью не отправлено** Дата: 2026-08-16 @@ -63,10 +63,13 @@ paused, stopped или terminal, жёлтая подложка снимаетс В задачу входят: -1. строгий generic resolver явной appliance lifecycle-сущности; -2. её приоритет над auxiliary switches и над нейтральным `Power=on`; -3. дополнение actual-work и terminal/idle словарей минимальными значениями, - необходимыми для отчёта и симметричного завершения цикла; +1. строгий generic resolver явной appliance lifecycle-сущности в уже + распознанной составной топологии с выделенным Power; +2. её приоритет над auxiliary switches и над нейтральным composite `Power=on` + без изменения семантики lone relay; +3. отдельное lifecycle-расширение actual-work словаря и дополнение общего + terminal/idle словаря минимальными значениями, необходимыми для отчёта и + симметричного завершения цикла; 4. явная матрица совместной работы lifecycle и выделенного Power; 5. одинаковый результат в full card, `houseplan-space-card`, desktop View, touch View и kiosk; @@ -103,18 +106,24 @@ paused, stopped или terminal, жёлтая подложка снимаетс ### 6.1. Порядок ролей -Действующий resolver сохраняет приоритеты whole-device доменов и semantic -binary. Новая appliance lifecycle-роль вставляется **после** них и **до** -switch fallback: +Действующий resolver сохраняет приоритеты whole-device доменов, semantic +binary и lone relay. Новая appliance lifecycle-роль включается только тогда, +когда существующее топологическое правило уже распознало составной прибор с +выделенным Power: среди entities больше одного uncategorised switch, а +выбранный switch подтверждён `isDevicePowerSwitch()`. В этой ветке порядок +такой: 1. canonical whole-device domain (`vacuum`, `climate`, `cover`, ...); 2. semantic binary (`running`, `power`, presence/contact/alarm, ...); -3. строгая appliance lifecycle-сущность; -4. representative switch / выделенный Power; +3. строгая appliance lifecycle-сущность + выделенный Power как gate; +4. действующий composite Power fallback; 5. passive fallback. Так lifecycle не перехватывает устройство, для которого HA уже предоставляет -более сильную стандартную state machine. +более сильную стандартную state machine. Если топология не распознана как +composite Power, существующий representative switch остаётся источником: +одиночное реле `on` не теряет working из-за соседнего `Status=connected`, +`Status=idle` или другого lifecycle-похожего sensor. ### 6.2. Что считается явной lifecycle-сущностью @@ -136,9 +145,16 @@ Power не должно скрывать от resolver точный `Status` т - `run_state` / `running_state` / `machine_state`; - `job_state` / `operation_state` / `activity_state`. -Русская локализация отображаемого имени (`Статус`, `Состояние работы`) может -поддерживаться тем же строгим словарём, но английский `translation_key`/ -`original_name`, когда он есть, остаётся предпочтительным доказательством. +Русские отображаемые имена (`Статус`, `Состояние работы`) в первой итерации не +являются доказательством lifecycle-роли. Локализованный `friendly_name` не +мешает выбору, если canonical `translation_key`, `original_name`, registry name +или object id уже доказывает роль, но сам по себе роль не создаёт. + +Конечный сегмент `_status` матчится, поэтому до allowlist применяется явный +connectivity stop-list. Кандидаты, чьи нормализованные metadata/object-id +сегменты содержат `wifi`, `connection`, `signal` или `battery`, исключаются: +`wifi_status`, `connection_status`, `signal_status` и `battery_status` не +являются lifecycle прибора. Общий токен `state` без уточнения, substring внутри произвольного слова и эвристика по имени всего устройства запрещены. `mode`, `program`, `stage`, @@ -157,46 +173,62 @@ Live state не меняет выбранную сущность: временн ## 7. Контракт actual-work значений Сравнение state и поддерживаемых action attributes остаётся case-insensitive и -whitespace-insensitive. Существующий `WORKING_STATES` сохраняется и получает -минимальный набор lifecycle-глаголов: +whitespace-insensitive. В одном модуле хранятся два уровня working vocabulary: + +1. базовый `WORKING_STATES` без изменений — для generic entity projection и + действующих action attributes; +2. `LIFECYCLE_WORKING_STATES` = базовый набор плюс минимальный набор + lifecycle-глаголов, применяемый **только** к выбранной appliance + lifecycle-роли: - `start`, `started`, `run`, `active`, `in_progress`; - короткие формы физических этапов, уже представленных длительными формами: `wash`, `rinse`, `spin`, `dry`. -Существующий `IDLE_STATES` сохраняется и получает terminal-пары: +Существующий общий `IDLE_STATES` сохраняется и получает terminal-пары: - `stop`, `end`, `done`, `inactive`. -`paused` остаётся неработающим состоянием. `unknown`, `unavailable`, пустое и -missing не становятся working. Новые значения применяются только к уже -выбранной lifecycle-роли либо к действующим action attributes; они не дают -права сканировать произвольные Mode/Program/Stage sensors. +`paused` остаётся неработающим состоянием. Глобальное расширение idle-набора +допустимо и применяется по действующим веткам, включая climate; глобальное +расширение базового working-набора запрещено. `unknown`, `unavailable`, пустое +и missing не становятся working. + +Новые active-значения применяются только к уже выбранной lifecycle-роли и не +расширяют классификацию generic sensors либо action attributes. Поэтому +`dry`, `start` или `active` у устройства без lifecycle-роли остаются neutral. +`wash`, `rinse`, `spin` и `dry` описывают допустимое **значение выбранной +lifecycle-роли**; сущность Stage/Phase сама по себе по-прежнему не является +authority и не сканируется. Неоднозначные значения вроде `normal`, `auto`, `eco`, `mixed_wash`, чисел и температур остаются neutral. ## 8. Матрица lifecycle + Power -Для составного прибора выделенный Power остаётся availability/lifecycle gate, -но не источником actual work: +Новая матрица применяется только к топологии, которую существующее правило +распознало как составной прибор с выделенным Power. В ней Power остаётся +availability/lifecycle gate, но не источником actual work: | Power | Явный lifecycle | Итог | |---|---|---| | `on` | active (`start`, `washing`, ...) | available + yellow working/running | | `on` | idle/terminal (`idle`, `stop`, `done`, ...) | available + neutral | | `on` | unknown/unavailable/missing | available + neutral | -| `on` | lifecycle отсутствует | действующий composite Power: available + neutral | +| `on` | lifecycle отсутствует | действующий composite Power fallback: available + neutral | | `off` | любое, включая stale active | действующий Power-off вид: unavailable/faded + neutral | | unavailable/missing | любое | unavailable/faded + neutral | -| Power отсутствует | active lifecycle | available + yellow working/running | -| Power отсутствует | idle lifecycle | available + neutral | -| Power отсутствует | unavailable lifecycle | unavailable/faded + neutral | Auxiliary switches не могут сделать прибор working и не перебивают выбранный lifecycle. Alarm critical sources сохраняют действующий высший визуальный приоритет над working. +За пределами распознанной composite Power топологии новая роль не меняет +источник состояния. В частности, lone relay `on` остаётся working даже при +соседнем lifecycle-похожем sensor с neutral/unknown значением; устройство без +выделенного Power следует прежнему resolver и не получает новых +lifecycle-specific active tokens по контракту #164. + ## 9. Presentation и UX - Результат использует существующий `status: working` и существующую жёлтую @@ -248,14 +280,18 @@ neutral результат, что desktop View. Новых жестов или Обязательные инварианты: -1. один classifier владеет working/idle vocabulary; +1. один модуль classifier владеет базовым working-набором, его scoped + lifecycle-расширением и общим idle-набором; второй список в presentation + запрещён; 2. один role resolver используется full и static cards; 3. выбор lifecycle не зависит от текущей доступности/значения и не скачет между peers на state update; 4. анализ выполняется по уже построенному списку entities устройства, без полного сканирования `hass.states` для каждого marker; 5. integration/model/device-name исключения запрещены; -6. Power-off и alarm precedence не размножаются отдельными render-ветками. +6. Power-off и alarm precedence не размножаются отдельными render-ветками; +7. lifecycle override существует только внутри уже распознанной composite + Power топологии и не меняет lone relay/обычный switch fallback. ## 13. Критерии приёмки @@ -264,7 +300,9 @@ neutral результат, что desktop View. Новых жестов или switches) детерминированно выбирает lifecycle `Status` плюс Power gate, а не auxiliary switch; uncategorised Status выигрывает у diagnostic, config исключается, а registry order и локализованный friendly name не меняют - результат при наличии canonical metadata. + результат при наличии canonical metadata. Без canonical metadata русские + `Статус`/`Состояние работы` роль не создают; `wifi_status`, + `connection_status`, `signal_status` и `battery_status` всегда исключаются. - **AC2 (`unit`):** `Status=start` при `Power=on` даёт `{ availability: available, status: working, activity: running }`; те же assertions проходят для существующих длительных и новых коротких active @@ -272,21 +310,29 @@ neutral результат, что desktop View. Новых жестов или - **AC3 (`unit`):** при `Power=on` значения `idle`, `paused`, `stop`, `done`, `finished` дают available + neutral; `Power=off/unavailable` подавляет даже stale active lifecycle и сохраняет действующий faded neutral вид. -- **AC4 (`unit`):** Power-on без явной lifecycle-сущности остаётся neutral; - Mode/Program/Stage/positive remaining time и auxiliary switches сами по себе - не создают working. -- **AC5 (`unit`):** без Power активный lifecycle даёт working, idle — neutral, - unavailable — unavailable; live availability не ретаргетит выбранную роль. -- **AC6 (`unit`):** регрессии сохраняют действующий статус lone relay, climate - actual action/fallback, passive media, vacuum cleaning/returning, cover - transition, automation enabled, semantic binary, alarm priority и static - display suppression. +- **AC4 (`unit`):** в распознанной composite Power топологии `Power=on` без + явной lifecycle-сущности остаётся neutral; Mode/Program/Stage/positive + remaining time и auxiliary switches сами по себе не создают working. + Generic sensor со state `dry`, `start` или `active` у устройства без + выбранной lifecycle-роли также остаётся neutral. +- **AC5 (`unit`):** при `Power=on` временно unavailable lifecycle остаётся + выбранной ролью и даёт neutral, не ретаргетясь на peer. Топологии без + выделенного composite Power следуют прежнему resolver: новые + lifecycle-specific tokens их состояние не меняют. +- **AC6 (`unit`):** lone relay `on` плюс lifecycle-похожий sensor со значением + `connected`, `idle` или unavailable по-прежнему даёт working от relay; + semantic binary остаётся выше lifecycle. Остальные регрессии сохраняют + climate actual action/fallback, passive media, vacuum cleaning/returning, + cover transition, automation enabled, alarm priority и static display + suppression. - **AC7 (`unit` + `ревью кода`):** full card и `houseplan-space-card` получают один и тот же resolved presentation для active/idle washer snapshot; preview использует тот же reason без нового classifier. -- **AC8 (`golden`):** synthetic composite washer в active snapshot визуально - имеет существующую жёлтую подложку, а парный idle snapshot — нейтральный; - эталон не содержит реальных пользовательских данных и проверяется по обычному +- **AC8 (`golden`):** synthetic composite washer добавлен в + `demo/fixtures/visual-matrix.mjs`; active snapshot визуально имеет + существующую жёлтую подложку, а парный idle snapshot — нейтральный. Сцены + перечислены в `GOLDEN_SCENARIOS`, `GOLDEN_MATRIX_VERSION` увеличен; эталон не + содержит реальных пользовательских данных и проверяется по обычному baseline-review процессу. - **AC9 (`ревью кода`):** desktop View, touch View и kiosk не имеют отдельных state branches; поведение не зависит от pointer/viewport и не меняет editor @@ -308,15 +354,40 @@ neutral результат, что desktop View. Новых жестов или 1. Добавить device fixture, повторяющий состав сущностей пользовательского отчёта, с canonical registry metadata и локализованными display names. 2. Покрыть role priority и deterministic tie-break независимо от registry order. -3. Таблично покрыть active/idle/unknown словари и Power-матрицу из раздела 8. -4. Оставить негативную матрицу Mode/Program/Stage/time/aux switches. -5. Расширить regression tests существующих device roles и presentation modes. +3. Таблично покрыть базовый working-набор, scoped lifecycle-расширение, общий + idle-набор и Power-матрицу из раздела 8. +4. Оставить негативную матрицу Mode/Program/Stage/time/aux switches и добавить + generic `dry`/`start`/`active` без lifecycle-роли. +5. Добавить lone relay `on` с neutral lifecycle-похожим peer и connectivity + `_status` candidates. +6. Расширить regression tests существующих device roles и presentation modes. + +### Mutation gate (#85) + +До код-ревью исполнитель вручную вносит каждый мутант по одному и фиксирует, +что указанный тест краснеет; после возврата корректного кода весь unit suite +снова зелёный: + +1. lifecycle-specific tokens добавлены в базовый working-набор вместо scoped + набора → краснеет + `keeps lifecycle-only active tokens neutral outside lifecycle role` (AC4); +2. снят Power-off gate для stale `Status=start` → краснеет + `suppresses stale active lifecycle when composite Power is off` (AC3); +3. lifecycle-роль поднята выше semantic binary → краснеет + `keeps semantic binary ahead of appliance lifecycle` (AC6); +4. unavailable lifecycle ретаргетится на соседнюю сущность → краснеет + `keeps lifecycle role identity stable while live state is unavailable` + (AC5); +5. в `device-presentation`/space-card добавлен второй vocabulary/classifier → + краснеет + `shares one lifecycle presentation between full and space cards` (AC7). ### Golden -Targeted synthetic scenario показывает рядом active и idle варианты одного -составного прибора либо делает два детерминированных capture. Меняется только -уже существующая working-подложка; новые visual tokens не принимаются. +В `demo/fixtures/visual-matrix.mjs` добавляются synthetic active и idle варианты +одного составного прибора. Оба capture регистрируются в `GOLDEN_SCENARIOS`, а +`GOLDEN_MATRIX_VERSION` увеличивается. Меняется только уже существующая +working-подложка; новые visual tokens не принимаются. ### Цикл реализации и pre-beta @@ -339,19 +410,26 @@ registry/state snapshots. Новых service calls, permissions, URLs и HTML н ## 16. Риски -1. **Ложный yellow от режима/программы.** Закрывается строгим role allowlist и +1. **Ложный yellow от lifecycle-глагола у generic sensor.** Базовый working + vocabulary не расширяется, lifecycle tokens scoped выбранной ролью; AC4 и + первый мутант защищают границу. +2. **Ложный yellow от режима/программы.** Закрывается строгим role allowlist и AC4. -2. **Stale stage после завершения.** Stage не является authority в #164; +3. **Stale stage после завершения.** Stage не является authority в #164; terminal Status и Power-off закрывают цикл. -3. **Ложный neutral из-за слишком узкого metadata matcher.** Fixture покрывает - object id, translation key, original name и локализованный display name. -4. **Power-off проигрывает stale Status=start.** Явная матрица и AC3 задают +4. **Ложный neutral из-за слишком узкого metadata matcher.** Fixture покрывает + object id, translation key и original name. Русское отображаемое имя без + canonical metadata намеренно не поддерживается первой итерацией. +5. **Connectivity status ошибочно принят за lifecycle.** Stop-list и AC1 + исключают wifi/connection/signal/battery candidates. +6. **Power-off проигрывает stale Status=start.** Явная матрица и AC3 задают жёсткий gate. -5. **Регрессия soundbar/composite switches.** Существующий Power-on neutral - сохраняется и защищён AC4/AC6. -6. **Retarget на state update.** Выбор роли опирается на metadata, не live +7. **Регрессия lone relay или soundbar/composite switches.** Lifecycle override + ограничен распознанной composite Power топологией; AC4/AC6 защищают обе + стороны границы. +8. **Retarget на state update.** Выбор роли опирается на metadata, не live значение; AC5 защищает identity. -7. **Расхождение full/static card.** Запрещён второй classifier, AC7 проверяет +9. **Расхождение full/static card.** Запрещён второй classifier, AC7 проверяет один resolved presentation. ## 17. Release-артефакты @@ -392,5 +470,6 @@ registry/state snapshots. Новых service calls, permissions, URLs и HTML н не поводом ослаблять matcher без доказательства. 4. Lifecycle и Power могут быть представлены двумя resolved samples либо одним составным result; наблюдаемая матрица раздела 8 обязательна, форма — нет. -5. Golden может расширить существующую visual-matrix fixture либо добавить - узкую новую; реальные screenshot/user data в репозиторий не попадают. +5. Golden расширяет существующую `demo/fixtures/visual-matrix.mjs`, регистрирует + обе сцены в `GOLDEN_SCENARIOS` и увеличивает `GOLDEN_MATRIX_VERSION`; реальные + screenshot/user data в репозиторий не попадают.