docs: revise washer lifecycle specification

Issue: #164
User-Visible: no
This commit is contained in:
Sergey Matyunin
2026-08-16 22:13:15 +03:00
parent 82cf3ad2db
commit d8e3b82da6
+138 -59
View File
@@ -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 в репозиторий не попадают.