21 KiB
CODE-REVIEW-33-r1
- Issue: https://github.com/Matysh/houseplan-card/issues/33
- ТЗ: docs/specs/033-config-schema-lifecycle.md (ревизия 3, зелёное ревью SPEC-REVIEW-33-r2)
- Ветка:
issue/33-config-schema-lifecycle - SHA материала ревью:
572cf928969fd8849299170ba3d42ff61d7ef423(git rev-parse HEAD) - Заход: r1 (первый код-ревью этого issue; ревью ТЗ не расходует бюджет код-ревью, §10.4/§2.10)
- Диапазон:
origin/dev..HEAD— 4 коммита (24681ee2feat,867f4762feat,f4318721fix,572cf928build)
Скоуп
Ревизия 3 ТЗ, три блока:
- Генерируемый манифест схемы (
scripts/dump-config-schema.py→scripts/config-schema.json, 265 листовых путей), pytest на свежесть. - Parity-тест 8 enum-пар backend↔frontend через
scripts/schema-compat-allowlist.mjs; тест полнотыconfig-field-registry.mjs; новыеas const-декларации (OPENING_TYPES,VACUUM_TRAIL_MODES,ZERO_WALL_STYLES,BG_MODES); паспорта registry (enforcedBy,schema: 'allow-extra'|'lovelace-card', 3 новых current-паспорта). - Lifecycle-фикстуры (oldest/current/future), pytest на lossless/round-trip;
config-audit.mjs— контракт exit-code 0/3/2.
Рантайм-поведение не меняется (заявлено ТЗ, подтверждено чтением: новые as const-массивы нигде не потребляются продуктовым кодом, кроме вывода типа).
Как проверялось
Дешёвые гейты прогнаны лично на SHA 572cf928:
| Гейт | Команда | Результат |
|---|---|---|
| typecheck | npx tsc --noEmit |
чисто |
| unit | npm test |
1608 pass / 0 fail / 1 skipped (совпадает с хендоффом) |
| build + сверка бандла | npm run build && cmp dist/houseplan-card.js custom_components/houseplan/frontend/houseplan-card.js |
идентичны |
| три копии бандла | npm run bundle:sync |
git status чист после — дерево уже синхронизировано |
| docs fingerprint | node scripts/check-docs.mjs |
"Documentation checks passed (7 files, 10 external links)" — обязателен, т.к. diff трогает src/** (sun.ts, types.ts) |
| бюджет | npm run bundle:budget |
initial View: 279147 B gzip (budget 300000 B, headroom 20853 B) — совпадает с заявленным |
| backend, новый файл | python3 -m pytest tests_backend/test_config_schema_manifest.py -q |
3 passed (после pip install pytest pytest-asyncio voluptuous — в песочнице их не было) |
backend, полный tests_backend |
python3 -m pytest tests_backend -q |
падает на сборе (ModuleNotFoundError: homeassistant в test_coordinate_canonicalization.py) — не регрессия этой задачи: файл не тронут диффом и не подпадает под test_ha_* исключение из conftest.py; ограничение окружения без HA, а не дефект #33 |
| дамп свежий | python3 scripts/dump-config-schema.py --check |
manifest fresh: 265 paths |
| parity + audit unit | node --test test/config-audit.test.mjs test/config-schema-parity.test.mjs |
8/8 pass |
| model-invariants | npm run invariants -- --config test/fixtures/config-lifecycle/{oldest-supported,current,future-fields}.json (все три; diff трогает types.ts/geometry-смежные lifecycle-фикстуры) |
все три: "Инварианты выполнены: ссылки разрешимы, записи толщины находятся"; на current/future — 4 информационных наблюдения (exact-endpoints вместо ключа), не нарушения |
| мутанты (по делу, не полный набор — дорогой гейт, PROCESS §8/§4) | node scripts/mutation-gate.mjs --id=schema-manifest-enum-drift и --id=registry-selector-dead-decision |
оба «поймано 1 из 1» |
Не прогонял и почему:
python -m pytest tests_backend -q(полный набор с HA) — недоступен в песочнице (нетhomeassistant, нет.venv-backend); согласно AGENTS.md это ожидаемое ограничение локальной среды, а не гейт этой задачи. Новый файлtest_config_schema_manifest.py— единственный, добавленный диффом, — прогнан отдельно и зелёный.- Полный
npm run mutation-gate(весь реестр мутантов) — дорогой предрелизный гейт (документировано в самом скрипте: «место — перед стабильным релизом»); прогнаны только два новых мутанта, относящихся к диффу. - Браузерные смоки —
node scripts/smoke-select.mjs --base origin/dev --head HEADвернул НЕОПРЕДЕЛЁННОСТЬ: ни один из 205 смоков не связан с изменёнными символами (BG_MODES,OPENING_TYPES,VACUUM_TRAIL_MODES,ZERO_WALL_STYLESи их типы). Проверено чтением:grepпоsrc/*.tsпоказывает, что эти константы нигде не потребляются продуктовым кодом кроме вывода типа — они существуют исключительно радиtest/config-schema-parity.test.mjs. Раз рантайм-поведение этих символов не меняется, смок не может ничего увидеть; решение — не гонять сверх того, что автор уже прогнал (smoke_space_card,smoke_decor— сами по себе не обязательны, но безвредны). npm run golden:verify— diff не меняет рендер/геометрию/стили, только типы и build/test-инфраструктуру; не запускал.
Находки
Все три — Medium, все в скоупе задачи (правятся в этом же issue, без Hig h и без нового issue).
M1 — AC7-тест не может поймать то, для чего он написан
test/config-schema-parity.test.mjs:96-98:
test('#33 AC7: no production source imports the schema manifest', () => {
const source = readFileSync(new URL('../src/houseplan-card.ts', ...), 'utf8')
+ readFileSync(new URL('../src/houseplan-editor-runtime.ts', ...), 'utf8');
assert.ok(!source.includes('config-schema-manifest'), ...);
});
Манифест был переименован в коммите f4318721 (scripts/config-schema-manifest.json →
scripts/config-schema.json, ровно из-за конфликта с глобом HACS *manifest.json), но
эта проверка по-прежнему ищет старую строку 'config-schema-manifest', которой больше
нет нигде в репозитории ни как файла, ни как валидного пути импорта.
Воспроизведение (проверено исполнением, не чтением): дописал в src/houseplan-card.ts
строку fetch('./scripts/config-schema.json') — реальный импорт манифеста под его текущим
именем — и перезапустил тест:
ok 3 - #33 AC7: no production source imports the schema manifest
Тест остался зелёным. AC7 («манифест не импортируется бандлом — контракт-проверка отсутствия импорта») сегодня не проверяет вообще ничего: искомая строка не может встретиться ни в каком корректном имени импорта.
Отдельно: проверка читает только 2 файла из ~60 в src/**, хотя и ТЗ («AC7-контракт
(нет импорта манифеста из src/**)»), и план автотестов говорят про весь src/**. Реальную
защиту от роста бюджета сейчас даёт не этот тест, а эмпирический npm run bundle:budget
(прогнан лично, см. таблицу выше, budget соблюдён) — но именно регрессионный тест,
который должен ловить будущий случайный импорт, сегодня этого не делает.
Правка: искать актуальное имя (config-schema.json либо путь
scripts/config-schema) и читать весь src/** (например, readdirSync рекурсивно или
готовый список файлов сборки), а не два жёстко перечисленных файла.
M2 — Дубликат записи в обоих CHANGELOG и в ARCHITECTURE.md
docs/CHANGELOG.md:5-17, docs/CHANGELOG.ru.md:11-24, docs/ARCHITECTURE.md:1503-1550 — один и
тот же абзац/раздел присутствует дважды подряд, слово в слово.
Причина, видна в истории: git diff 24681ee2 867f4762 --stat показывает, что второй
feat-коммит (867f4762) добавил ровно те же 24/7/7 строк в
ARCHITECTURE.md/CHANGELOG.md/CHANGELOG.ru.md, которые первый (24681ee2) уже добавил —
похоже на случайное повторное применение шага «допиши changelog» при двух подряд идущих
коммитах с идентичным сообщением. Результат в dev-кандидате: пользователь, открывший
CHANGELOG.md, увидит одну и ту же строку изменений дважды подряд; ARCHITECTURE.md несёт
два одинаковых заголовка ## Schema as the source of truth (#33, 2026-08-30).
Правка: убрать вторую копию блока в каждом из трёх файлов (внутри этой же задачи — Medium в скоупе, отдельный issue не заводится, #202).
M3 — config-audit.mjs расширил exit-code 3 за пределы согласованного контракта, и для decision-required это семантически неверно
ТЗ ревизии 3 (согласовано в SPEC-REVIEW-33-r1/r2, зафиксировано в теле issue и в файле
ТЗ, Блок 3): «3 — migration available (найдены поля со статусом
migrate-*/deprecated-read)». Реализация (scripts/config-audit.mjs:83-86) добавляет
ещё два статуса в MIGRATION_STATUSES:
const MIGRATION_STATUSES = new Set([
'migrate-on-write', 'migrate-on-settings-save', 'deprecated-read',
'drop-on-validation', 'decision-required',
]);
drop-on-validation (aspect/segments) можно защитить как «тоже legacy-остаток» — это в духе
контракта, хоть и не названо буквально. Но decision-required — это статус
settings.group_lights и settings.exclude_integrations
(scripts/config-field-registry.mjs): по собственному описанию регистри это ДЕЙСТВУЮЩИЕ,
поддерживаемые поля, чья судьба не решена архитектурно (issue #44), а не легаси-хвосты и
не что-то, что нужно мигрировать. Их migration-текст в registry прямо говорит: «decide
supported UI versus fixed exclusion rules before removal» — то есть миграции для
пользователя СЕГОДНЯ не существует вообще.
Сценарий отказа: владелец экспортирует у себя план, где реально используется
settings.group_lights (обычная включённая фича, не легаси), и прогоняет
npm run audit:config -- house.json для диагностики. Инструмент возвращает exit-код 3
«migration available», хотя мигрировать почему-то нечего — поле продолжит жить как есть,
пока #44 не примет решение. Это вводит в заблуждение ровно того человека
(владельца/разработчика), для которого инструмент существует.
AC6 не ловит это: юнит-тест (test/config-audit.test.mjs) прогоняет только три фикстуры
Блока 3, ни одна из которых не содержит group_lights/exclude_integrations, поэтому
расширение осталось незамеченным собственным тестом задачи.
Правка: либо сузить MIGRATION_STATUSES до буквально согласованного набора
(migrate-on-write, migrate-on-settings-save, deprecated-read, и по желанию
drop-on-validation с явным примечанием почему), исключив decision-required; либо, если
разработчик хочет сохранить более широкий охват, обновить формулировку AC6/контракта в ТЗ
и явно объяснить, почему «decision-required» тоже сигнализирует «migration available» —
на сегодняшний день это фактически неверно для двух из четырёх статусов набора.
Что проверено и корректно
- AC1 (свежесть/детерминизм/полнота манифеста) — доказано исполнением:
python3 scripts/dump-config-schema.py --check→manifest fresh: 265 paths;tests_backend/test_config_schema_manifest.py::test_issue_33_manifest_is_fresh_and_deterministicзелёный (byte-identical +len(fields) > 200). Дамп корректно fail-closed на незнакомые валидаторы (90/265 путейopaque— ожидаемо и не судится parity-тестом, риск явно назван в ТЗ). - AC2/AC3 (parity + анти-гниение allow-list) — зелёный тест, и я лично сверил все
8 пар вручную по
custom_components/houseplan/validation.pyи итоговомуscripts/config-schema.json(fill_mode ×2, display, tap_action, opening.type, trail_mode, zero_wall_style, bg_mode) — значения манифеста совпадают со схемой построчно. Тест умеет падать: мутантschema-manifest-enum-drift(фантомное значение в манифесте) поймал 1/1 при личном прогоне. - AC4 (полнота registry) — зелёный; вручную проверил трансляцию тегированных
вариантов манифеста (
<furniture>,<partition>) в нетегированные пути и совпадение с построением селектора из registry. Тест умеет падать: мутантregistry-selector-dead-decision(испорченный selector) поймал 1/1. Новые записи (decor_default_style,decor kind:'furniture',openings[].host=partition) резолвятся в реальные пути манифеста — проверено чтением и подтверждено прохождением самого AC4-теста (он бы упал на несуществующем пути). - AC5 (lossless фикстуры) —
tests_backend/test_config_schema_manifest.py::test_issue_33_lifecycle_fixtures_pass_the_schema_losslesslyзелёный на всех трёх фикстурах; потери ограничены задокументированнымиspaces[].aspect/spaces[].segments(vol.Remove). Дополнительно прогналnpm run invariantsна всех трёх фикстурах — геометрические инварианты (разрешимость ссылок, ключ записи толщины = ключ решёточного ребра) не нарушены, только информационные наблюдения на exact-endpoints, не связанные с #33.test_issue_33_future_fields_round_trip_exactlyзелёный — будущие поля на всех уровнях (root/settings/space/marker) переживают валидацию точно. - AC6 (exit-коды 0/3/2) — юнит зелёный на всех трёх кодах (см. M3 по существу контракта: коды технически работают, но множество триггеров шире согласованного).
- AC7 (нет импорта в бандл, бюджет не растёт) — эмпирически подтверждено
(
npm run bundle:budget→ 279147/300000, не изменилось от базовой линии — прирост бюджета отсутствует, манифест не тянется рантаймом); регрессионный тест-контракт сломан, см. M1. - Регистри:
enforcedByу 4 реализованных механизмов подтверждён чтением кода (houseplan-card.ts материализация show_all, editor-runtime удаление weather_entity, normalizeDeviceDisplay +_dropLegacySegmentsдля ripple,vol.Removeдля aspect/segments) — соответствует находкам предыдущей ревизии владельца (актуализация бэклога 2026-08-30). - Переименование манифеста (
f4318721) применено последовательно везде, где он упоминается (dump-скрипт, allowlist, mutation-gate, оба теста, ТЗ, ARCHITECTURE.md) — кроме забытой строки в самом AC7-тесте (M1). - Трейлеры всех 4 коммитов корректны (
Issue: #33на каждом,User-Visible: yesна обоихfeat,noнаfix/build); оба changelog правлены в том же коммите, что иfeat(хотя и с дублированием, см. M2). - Класс D (
dist/**,custom_components/houseplan/frontend/**) обновлён отдельнымbuild-коммитом (572cf928), не смешан с продуктовым кодом — соответствует таблице классов AGENTS.md. - Не-скоуп соблюдён: ни рантайм-поведение конфига, ни судьба
group_lights/exclude_integrations(кроме паспорта) не тронуты; миграций и новых ключей i18n нет.
Чего не проверял
- Полный
tests_backendс реальной Home Assistant (нетhomeassistant/.venv-backendв этой песочнице) — прогнан только новый файл задачи, который не требует HA. - Полный набор мутантов (
node scripts/mutation-gate.mjsбез--id) — предрелизный гейт по объёму, прогнаны только два мутанта, введённых этой задачей. golden:verifyи браузерные смоки сверх обоснованного выше — diff не меняет визуал, smoke-select не нашёл доказанной связи.- Оговорённая вне скоупа судьба
group_lights/exclude_integrations(#44) — не судил, это прямо не-скоуп ТЗ.
Вердикт
Жёлтый. High: 0, Medium: 3, все в скоупе — правятся в этом же issue без нового цикла-issue (#202). Ядро задачи (генерируемый манифест, parity+allow-list, полнота registry, lifecycle-фикстуры, exit-коды) реализовано добротно и доказано исполняемыми тестами, включая мутационное доказательство «тест умеет падать» для AC2 и AC4. Три находки — тест, который не может поймать регрессию, которую он называет своей целью (M1), задвоенная документация из-за дублирующего коммита (M2) и семантически неточный exit-код для одного из двух «decision-required» полей (M3).