Files
houseplan-card/docs/specs/033-config-schema-lifecycle.md
T
Codex f43187218f fix: the schema dump must not match the HACS *manifest.json glob (#33)
repo-hygiene caught it: HACS globs *manifest.json over the whole clone
and rejects a repository with two. The dump is scripts/config-schema.json.

User-Visible: no
Issue: #33
2026-08-30 12:45:17 +03:00

14 KiB
Raw Blame History

ТЗ #33 — Единый registry схемы и lifecycle compatibility-полей

  • Issue: https://github.com/Matysh/houseplan-card/issues/33
  • Приоритет: P2, tech-debt; полный трек (класс A schema-потребители — решение аналитики 2026-08-15, не оспаривается)
  • Ревизия: 3 (2026-08-30) — по SPEC-REVIEW-33-r1 (M1–M3, Low); замеры ревизии: CONFIG_SCHEMA = 212 листовых путей, LAYOUT_SCHEMA = 2, registry = 27 записей, parity/CI/фикстур нет

Сценарий

Разработчик добавляет значение в enum на одной стороне (Voluptuous или TypeScript) и забывает вторую — сегодня это молча живёт до полевого бага. После задачи CI краснеет с понятным диффом («backend fill_mode знает 'glow', frontend SPACE_FILL_MODES — нет: либо допиши, либо внеси в allow-list компат-расхождений»). Владелец плана ничего нового не видит — задача не меняет ни одного байта поведения конфига.

Что человек увидит до и после

Ничего: чисто инженерная страховка. Единственный видимый след — CI-джоба, падающая при дрейфе схемы, и npm run audit:config, который стал полнее.

Проблема

Схема расползлась по четырём мирам (Voluptuous, TS-типы, const-списки runtime, UI) и синхронизируется руками. Registry (27 записей) покрывает только слой «компат-решений» и отстал даже в нём: 4 решения уже реализованы кодом (show_all, weather_entity, ripple-канонизация, aspect/segments через vol.Remove), а новые поля v1.68–v1.69 (decor_default_style, furniture-decor, host=partition) не имеют паспорта. Полного манифеста persisted-путей нет; узаконенные read-compat расхождения (fill_mode 'glow', display 'ripple', tap_action 'cover') нигде не записаны машиночитаемо.

Ключевое решение ревизии 2

Канонический манифест НЕ пишется руками на 212 путей — он генерируется из Voluptuous-схемы: бэкенд — единственный владелец формы persisted-данных, всё остальное сверяется с ним. Registry остаётся отдельным слоем решений ПОВЕРХ манифеста. Судьбы group_lights/exclude_integrations решает #44 — здесь они получают только паспорт ALLOW_EXTRA.

Скоуп (три блока)

Блок 1 — генерируемый манифест схемы

  • scripts/dump-config-schema.py: обходит CONFIG_SCHEMA/LAYOUT_SCHEMA (Optional/Required, default, vol.In → enum, vol.Range → min/max, vol.Any → варианты, vol.Remove → отметка dropped, ALLOW_EXTRA → флаг узла) и пишет детерминированный scripts/config-schema.json (сортировка путей, стабильная сериализация; имя БЕЗ суффикса *manifest.json — HACS глобит его по всему клону, tест repo-hygiene). Импорт validation.py без homeassistant — заглушки родительских пакетов (приём проверен инвентаризацией этой ревизии).
  • Манифест коммитится; pytest-тест регенерирует его в tmp и сравнивает байт-в-байт — рассинхрон схемы и манифеста ломает CI с diff-ом.

Блок 2 — parity и полнота

  • test/config-schema-parity.test.mjs читает манифест и сверяет enum-пары с const-декларациями фронта. Готовые экспортируемые массивы существуют для 3 из 7 пар: fill_mode ↔ SPACE_FILL_MODES/ROOM_FILL_MODES, display ↔ DISPLAY_MODES, tap_action ↔ TAP_ACTIONS (все — logic.ts). Для остальных четырёх (M2 r1: сегодня это только TS-union'ы или фолбэки, не runtime-значения) декларации ЗАВОДЯТСЯ этим issue: OPENING_TYPES (types.ts-соседство), VACUUM_TRAIL_MODES, ZERO_WALL_STYLES, BG_MODES — экспортируемые as const-массивы, из которых выводятся существующие union-типы (typeof X[number]), чтобы список и тип не могли разойтись; рантайм-поведение не меняется. Расхождение с манифестом падает, если его нет в машиночитаемом allow-list scripts/schema-compat-allowlist.mjs (каждая запись: сторона-владелец, значение, причина, ссылка на issue). Стартовый allow-list: fill_mode 'glow' (backend-only, read-compat #20-эры), display 'ripple' (backend-only, канонизация в icon_ripple), tap_action 'cover' (backend-only, read-compat).
  • Тест полноты registry: каждый id из config-field-registry.mjs разрешается либо в путь манифеста, либо в явный extra-паспорт (show_all, group_lights, exclude_integrations — поля вне схемы, живущие через ALLOW_EXTRA; список — в registry новым полем schema: 'allow-extra'). Registry-запись, не находящая цель, ломает тест (мёртвые решения не накапливаются).
  • Актуализация registry как данных (M1 r1: статусная модель НЕ меняется — существующие статусы описывают механизм и не устаревают с появлением кода): 4 реализованных решения получают новое ОПЦИОНАЛЬНОЕ поле enforcedBy — ссылку на кодовую точку/тест, доказывающую механизм (show_all → houseplan-card.ts материализация; weather_entity → editor-runtime сохранение настроек; ripple → _dropLegacySegments + normalizeDeviceDisplay; aspect/segments → vol.Remove в схеме). Новые паспорта со статусом current — settings.decor_default_style (v1.69), decor kind:'furniture' (v1.69), openings[].host=partition (v1.68, model v9). Registry по-прежнему НЕ зеркалит все 212 путей — только поля с нетривиальной судьбой; полноту обычных полей несёт манифест.

Блок 3 — фикстуры lifecycle

  • test/fixtures/config-lifecycle/: три фикстуры — oldest-supported (легаси-поля: show_all, weather_entity, display 'ripple', walls-проекция), current (срез v1.69 с decor_default_style/furniture/value_source), future (незнакомые поля на всех уровнях).
  • pytest: каждая проходит CONFIG_SCHEMA; future-поля переживают валидацию losslessly (ALLOW_EXTRA-контракт), legacy не отбрасывается кроме задокументированных vol.Remove.
  • config-audit.mjs получает НОВУЮ CLI-логику итогового кода (M3 r1: сегодня exitCode зависит только от ошибок разбора). Контракт: 0 — clean (легаси/миграций не найдено), 3 — migration available (найдены поля со статусом migrate-*/deprecated-read), 2 — invalid input (существующее значение, не меняется). Код 3 выбран, чтобы не конфликтовать с текущим 2. Юнит на фикстурах Блока 3 проверяет и counts, и все три кода — расширение test/config-audit.test.mjs.

Не-скоуп

  • Изменение поведения ЛЮБОГО поля, новые миграции, изменение схемы.
  • Судьба group_lights/exclude_integrations (это #44).
  • Автогенерация TS-типов из манифеста (возможный будущий шаг).
  • Optimize-превью миграций (существующий механизм не трогается).
  • Числовые range-сверки фронта (у фронта нет машиночитаемых range-деклараций; манифест их несёт, сверка появится вместе с декларациями).

Контракт поведения

Ничего в рантайме не меняется. Новые артефакты: манифест (JSON), allow-list (mjs), дамп-скрипт (py), два теста, фикстуры, паспорта в registry.

UX / i18n

Не задето. Новых строк нет.

Модель данных и миграция

Persisted-данные не меняются. Манифест — build-артефакт в git, не в бандле (бюджет initial не растёт).

Критерии приёмки

  • AC1: scripts/config-schema.json детерминирован (два прогона дампа байт-идентичны) и покрывает 100% листовых путей CONFIG/LAYOUT-схем; pytest падает при рассинхроне схемы и закоммиченного манифеста, diff показывает пути.
  • AC2: parity-тест зелёный на текущем дереве; добавление значения в бэкенд-enum без пары и без allow-записи → красный с именем enum и значением (доказательство мутантом м1).
  • AC3: удаление записи allow-list при сохранённом расхождении → красный (расхождения не узакониваются молча).
  • AC4: тест полноты registry зелёный; запись с несуществующей целью → красный (мутант м2: сломать selector одной записи).
  • AC5: все три фикстуры проходят схему; future-поля возвращаются из валидации без потерь (pytest, сравнение по путям).
  • AC6: config-audit.mjs возвращает 0/3/2 по контракту Блока 3 на clean/legacy/битой фикстурах соответственно (юнит).
  • AC7: полный гейт зелёный; бюджет initial без изменений (манифест не импортируется бандлом — контракт-проверка отсутствия импорта).

План автотестов

  • pytest: AC1 (свежесть манифеста), AC5 (фикстуры) — в tests_backend (обходчик без homeassistant, работает и в песочнице).
  • node --test: AC2/AC3 (parity + allow-list), AC4 (полнота registry), AC6 (audit exit-codes), AC7-контракт (нет импорта манифеста из src/**).
  • Мутанты (mutation-gate): м1 — подмена одного enum-списка фронта (убрать значение из DISPLAY_MODES-копии проверяемой пары) → красный AC2; м2 — испортить selector одной registry-записи → красный AC4.

Риски

  • Дамп-скрипт может отставать от новых конструкций Voluptuous (Coerce, кастомные валидаторы): fail-closed — незнакомый валидатор пишется в манифест как opaque с исходным repr, тест свежести это переживает, parity такие узлы не судит.
  • Ложные срабатывания parity на легитимных решениях — гасятся allow-list'ом с обязательной причиной и ссылкой.
  • CI-время: дамп-регенерация — миллисекунды (walk по объектам в памяти).

Откат

git revert: все артефакты — новые файлы + паспорта-данные в registry; рантайм не тронут, откат ничего не теряет.

DoR-примечания: миграция/compatibility — нет (только чтение схемы); touch — не влияет; производительность — не влияет (build/test-time).

Release-артефакты

  • CHANGELOG + CHANGELOG.ru: короткая запись (инфраструктура защиты схемы), User-Visible: yes у финального коммита (пользователь получает гарантию «старый конфиг не портится молча», это честно назвать).
  • docs/ARCHITECTURE.md: абзац «схема как источник истины: манифест, parity, allow-list» со ссылками на скрипты.

Принятые предположения

  • Бэкенд-схема — единственный канонический источник формы persisted-данных; фронтовые типы — потребители (соответствует факту: все записи идут через config/set с валидацией).
  • Card-level Lovelace-поля (fit, light_pools, show_button…) — НЕ persisted store и в манифест не входят; их учёт — вне скоупа (при желании — отдельный issue).
  • Registry остаётся «слоем решений», а не зеркалом схемы, — по исходной шапке файла; полноту несёт манифест.