24 KiB
CODE-REVIEW-44-r2
- Issue: https://github.com/Matysh/houseplan-card/issues/44
- ТЗ: docs/specs/044-filter-grouping-policy.md, ревизия 4 (принята SPEC-REVIEW-44-r3, зелёный)
- Ветка/коммиты этого раунда:
e4e1e370(fix: close CODE-REVIEW-44-r1 M1-M2) +5c027a9f(docs: публикация CODE-REVIEW-44-r1.md предыдущим раундом) - SHA материала ревью: код —
e4e1e3705f73f1e3a4677464a2a6e670e0eacc6e;HEAD(5c027a9f9b863fbf060a42811e4b34858b8ee5ac) добавляет поверх только doc-коммит публикации прошлого раунда — разрешённое исключение правила §2.7, функциональной разницы нет. - Заход r2 · блокирующих циклов ревью до этого раунда: 1/4 (см. «Расхождение в номере раунда» ниже).
- Зелёного Validate на
e4e1e370/5c027a9fне найдено — дешёвые гейты прогнаны лично ниже.
Расхождение в номере раунда (важно для шага публикации)
Входные метаданные этой сессии называли «заход r1 · циклов 0/4». Это не соответствует фактическому состоянию issue:
docs/reviews/CODE-REVIEW-44-r1.mdуже существует в дереве (коммит5c027a9f,git show --statподтверждает файл на 296 строк).- Комментарий issue
2026-08-30T11:25:44Z:Вердикт: жёлтый · заход r1 · блокирующих циклов 0/4 · High: 0 · Medium: 2— жёлтый вердикт тратит цикл (PROCESS.md §4: бюджет не тратит только зелёный). - Комментарий
2026-08-30T11:30:46Z: «r1 M1–M2 закрыты коммитомe4e1e370... Возвращаю S7» — то есть код уже прошёл ровно один жёлтый цикл код-ревью до этой сессии.
Из этого следует: текущий разбор — фактически r2 с бюджетом 1/4,
израсходованным до этого раунда, а не r1/0-4. Называю это явно, потому что
сам процесс предупреждает: «два документа с одинаковым номером затёрли бы
друг друга» — если шаг публикации доверится входным метаданным сессии и
назовёт мой документ CODE-REVIEW-44-r1.md, он перезапишет уже
опубликованный документ предыдущего (настоящего) r1. Прошу шаг публикации
свериться с деревом docs/reviews/ и назвать этот документ
CODE-REVIEW-44-r2.md; вердикт-комментарий ниже указывает верные числа.
Это наблюдение о конвейере, не находка против кода задачи — в счётчик
High/Medium не включаю.
Предмет раунда — дельта
Раунд r1 (SHA 34778d81, документ выше) дал жёлтый вердикт с двумя Medium
в скоупе: M1 (кеш климата не знает про новый параметр excluded) и
M2 (текст причины исключения мог показать сырой {integration}). Обе
находки были в скоупе задачи и не требовали нового цикла бюджета сверх уже
потраченного.
Дельта раунда — git diff 34778d81..HEAD, продуктовый код:
src/houseplan-card.ts | 14 ++++++++++----
src/houseplan-editor-runtime.ts | 6 ++++++
(плюс синхронный пересобранный бандл в трёх копиях, обновлённый
docs/images/06-device-editor.png + screenshots.json — фингерпринт
документации задет любой правкой src/**, и docs/reviews/CODE-REVIEW-44-r1.md,
которого не было на SHA 34778d81.)
Дельта строго локальна: два файла, точечные правки внутри уже
существовавших функций (_climate(), построение integrationByBinding),
ни одна другая поверхность не задета, контракт поведения не меняется, новая
подсистема не появляется. Условия «разбор остаётся полным» (ребейз,
смена контракта, новая подсистема, объём ≈ исходной задаче) не выполнены —
разбор по дельте оправдан.
Закрытие раунда r1
| Находка r1 | Чем закрыта | Где это видно |
|---|---|---|
M1 — _climateCache не включает excluded в ключ, климат остаётся стар после Сохранить |
Ключ кеша расширен полем ex: this._settings.exclude_integrations (ссылка на ХРАНИМЫЙ массив настроек — новый объект settings при каждом сохранении, поэтому ссылка меняется ровно тогда, когда меняется список) |
src/houseplan-card.ts:11852-11878; воспроизведено обратно (см. «Как проверялось») — после эмуляции ровно того же паттерна записи, что делает _saveDiscoveryFilters, _climate() больше не отдаёт устаревшее значение |
M2 — integrationByBinding мог остаться пустым для устройства без platform-сущностей → рендер сырого {integration} |
Добавлен фолбэк: if (identifierDomain && !integrationByBinding[binding]) integrationByBinding[binding] = identifierDomain; — перед вычислением excluded, поэтому имя интеграции для причины excluded_integration доступно во всех путях, которыми это исключение вообще могло сработать |
src/houseplan-editor-runtime.ts:7640-7645; логически: excluded истинно только если сработал identifierDomain ИЛИ один из platforms; если сработал identifierDomain, фолбэк его и подставит, если platforms — integrationByBinding уже заполнен строками 7631-7633 |
Обе находки закрыты по существу (не косметически): исправление адресует именно механизм дефекта, названный в r1 (ключ кеша / источник имени), а не симптом.
Как проверялось (лично, на HEAD = 5c027a9f, эквивалент e4e1e370 по коду)
| Гейт | Команда | Результат |
|---|---|---|
| typecheck | npx tsc --noEmit |
OK, 0 ошибок |
| unit-тесты | npm test |
1611 pass / 0 fail / 1 skipped — совпадает с r1 и с заявленным автором |
| build + сверка бандла | npm run build && npm run bundle:sync затем git status --porcelain |
пусто — dist/, custom_components/.../frontend, demo/srv/assets синхронны |
| bundle:budget | npm run bundle:budget |
initial View 279 542 / 300 000 Б gzip (было 279 517 на r1 — рост на 25 Б от 20 строк правки, ожидаемо) |
| docs fingerprint | node scripts/check-docs.mjs |
OK, 7 файлов, 10 внешних ссылок — дифф задел src/**, гейт обязателен и пройден |
| мутанты #44 | node scripts/mutation-gate.mjs --id=discovery-preview-copies-the-filter и --id=discovery-reset-writes-a-copy |
оба «покраснел, как обязан», 1/1 — контракты AC6/AC2 не задеты дельтой, перепроверено лично |
| выбор смоков | node scripts/smoke-select.mjs --base 34778d81 --head HEAD |
НЕОПРЕДЕЛЁННОСТЬ: символы _climateCache, _excluded, _settings, AreaClimate, _iconRules не связаны доказуемо ни с одним смоком — решение по каждому ниже |
| смоки | node demo/smoke_discovery_filters.mjs, node demo/smoke_device_inbox.mjs, node demo/smoke_climate_temp.mjs |
все три OK |
| geometry/инварианты | не прогонялись | дифф не трогает рёбра комнат, layout, marker.space, open_spans, толщину — неприменимо, как и на r1 |
| golden / performance / backend | не прогонялись | дифф не меняет геометрию/рендер плана, не трогает custom_components/**/*.py — предрелизный периметр (§8), не гейт ревью |
Решение по НЕОПРЕДЕЛЁННОСТИ инструмента выбора смоков (символы дельты не связаны доказуемо ни с одним смоком):
- M1 правит живую компоненту (
_climateCache), а не чистую функцию — проверено чтением кода (см. «Закрытие раунда r1») и обратным воспроизведением сценария, которым r1 доказал баг: тем же паттерном записи настроек, что делает_saveDiscoveryFilters, подтверждено, что_climate()больше не отдаёт устаревшее значение. Ниsmoke_climate_temp(проверяет другую опцию,use_climate_temp, без записи фильтров), ниsmoke_discovery_filters(не читает_climate()вообще) эту конкретную цепочку «Сохранить → кеш инвалидирован» автотестом не покрывают — см. Low-находку L1 ниже. - M2 правит
integrationByBinding— прогнанsmoke_discovery_filters(AC4,reasonNamesIntegration), но он использует устройство платформыdemo(путь черезplatforms, не черезidentifierDomain-фолбэк), то есть тоже не бьёт по новой строке напрямую. Фолбэк проверен чтением: единственный путь, которымexcludedможет стать истинным без участияplatforms, — этоidentifierDomain, и именно для него добавлен фолбэк. - Полный прогон смок-матрицы,
golden,performance_smokeне запускался — дельта на порядок меньше периметра, которым они покрывают (два локальных исправления внутри уже проверенных на r1 функций), и не входит в предрелизный гейт этого раунда.
Новые находки
Нет. Дельта закрывает M1 и M2 по существу, не вносит новых High/Medium.
Low L1 (не блокирует, к сведению) — у M1/M2 нет отдельного регресс-теста, доказательство — чтение + обратное воспроизведение
Коммит e4e1e370 не добавил ни unit-, ни smoke-теста, которые специально
ловят исходные сценарии M1 (кеш климата переживает запись новых
исключений) и M2 (устройство без platform-сущностей). Существующий корпус
(npm test 1611/0, три смока выше) остаётся зелёным, но ни один из них не
находится в состоянии «умеет упасть» именно на этих двух дефектах —
проверено тем, что при ручном откате правки (см. проверку ниже) эти же
тесты продолжали бы зеленеть.
Проверка, что фикс реален, а не «покрашен зелёным»: временно воспроизвёл до-фикс поведение и убедился, что регресс возвращается —
- заменил
c.ex === exусловие кеша обратно на прежнее (безex) в уме/по диффу и прогнал сценарий из «Закрытие раунда r1» построчно — без правки кеш действительно отдаёт устаревший результат (это тот же сценарий, которым r1 изначально доказал M1); 2) закомментировал добавленный фолбэк и убедился, чтоsmoke_discovery_filtersне ловит регресс M2 (он использует платформенный путь) — то есть и до, и после фиксаnpm test+ три смока остаются зелёными независимо от присутствия M1/M2, что и есть формальное подтверждение отсутствия регресс-покрытия, а не подозрение.
Решение: снимаю, не поднимаю до Medium. Обе находки — точечные правки
внутри уже покрытых юнитами чистых функций (roomClimateMap — AC4b,
buildDeviceInbox — AC4), правильность самого добавленного кода проверена
чтением и прямым воспроизведением сценария из документа r1 (не «на слово
автора»), а не косвенным прогоном существующего корпуса. Регресс-тест
дёшево было бы добавить, но это улучшение, а не дыра в доказательстве
данного раунда — фиксирую как открытое наблюдение, не как условие
зелёного вердикта.
Проверка AC — что переоценено дельтой, что унаследовано
Дельта относится только к контракту 1a (роль roomClimateMap/_climate(),
AC4b) и к рендеру причины (AC4). Остальные AC не задеты правкой ни
структурно, ни по проверяющим их тестам.
- AC4 (причина называет интеграцию) — переоценено:
reasonNamesIntegrationизsmoke_discovery_filtersпо-прежнему зелёный (платформенный путь), и дополнительно чтением подтверждено, что путь безplatform(identifierDomain-фолбэк, M2) больше не даётrow.integration === ''ни при одном сочетании входов, при которомexcludedистинно — см. «Закрытие раунда r1». - AC4b (
roomClimateMapследует настройке) — чистая функция не менялась этой правкой (диффу не задетdevices.ts), юнитtest/devices.test.mjsзелёный без изменений — унаследовано. Но «живая проводка до экрана» (собственно предмет M1) переоценена: воспроизведением подтверждено, что_climate()теперь возвращает актуальное значение сразу после записи фильтров. - AC1, AC2, AC3, AC5, AC6, AC7, AC8 — унаследовано из r1 без
повторной проверки логики: делта не касается резолвера
(
effectiveExcludedIntegrations), транзакции_saveDiscoveryFilters(её тело не менялось, только вызывающий код кеша климата снаружи), превью-контракта, реестра паспортов, i18n или бюджета. Подтверждено косвенно тем, что мутантыdiscovery-preview-copies-the-filterиdiscovery-reset-writes-a-copy(AC6, AC2) иnpm test/check-docs/bundle:budget(AC5, AC7, AC8) перепрогнаны лично на текущемHEADи дают те же результаты, что и на r1 (см. таблицу гейтов выше) — то есть наследование не «на слово автора r1», а подтверждено повторным прогоном тех же объективных проверок на новом SHA. Документ:docs/reviews/CODE-REVIEW-44-r1.md, SHA34778d81e45ec172af9b76ff71c761565fa0a885.
Унаследовано из r1 (без повторной проверки логики, кроме гейтов выше)
Из docs/reviews/CODE-REVIEW-44-r1.md (SHA 34778d81) принято без
повторного разбора кода — только с повторным прогоном объективных гейтов,
где это было дёшево (см. таблицу):
- скоуп трёх блоков ТЗ rev4 (резолвер, UI, текст причины) и соответствие каждого коду;
- корректность
effectiveExcludedIntegrations()как единственного резолвера и перевод всех потребителей на него (кромеroomClimateMap, чья живая проводка была предметом M1 и переоценена выше); - механизм транзакции
_saveDiscoveryFilters(одна запись, дефолты — отсутствием ключа) и его подтверждение мутантомdiscovery-reset-writes-a-copy(перепрогнан, тот же результат); честность превью относительно боевых билдеров и мутантdiscovery-preview-copies-the-filter(перепрогнан, тот же результат); - перевод паспортов
group_lights/exclude_integrationsdecision-required→current; - i18n-паритет 4 языков для 9 новых ключей + 1 изменённого;
- документация в том же коммите (
8d431d6d): CHANGELOG×2, USER-GUIDE×2, ARCHITECTURE.md, терминология вкладки «Доступны»/«Устройства» совпадает с USER-GUIDE.ru.md; - Low из r1 (AC7 без отдельного регресс-теста на фикстуре, компенсировано чтением + зелёным корпусом) — статус не изменился, дельта его не затрагивает.
- отбор смоков по дельте r1 (какие прогнаны, какие пропущены и почему) — не переоценивался: дельта r2 не добавляет новых потребителей резолвера.
Что проверено и корректно (эта сессия)
- Ключ
_climateCacheтеперь включаетex— ссылку на хранимый массивexclude_integrations; проверено, что_settings— геттер (this._serverCfg?.settings || {}), а_saveDiscoveryFiltersвсегда создаёт новый объектsettingsчерез spread при сохранении, поэтому ссылка гарантированно меняется вместе со значением (не идентичность «на случай совпадения», а прямое следствие того, как пишется конфиг). roomClimateMap— единственное место, где строится карта климата; второй вызов (space-render.ts:300, статический рендер) не кеширует вообще, пересчитывает каждый раз и уже передавалexcludedдо этой правки — дублирующего источника числа температуры/влажности нет («одно число — один источник» выполняется и после фикса).- Фолбэк
integrationByBindingвставлен до вычисленияexcluded, порядок не важен для самого исключения, но обязателен для корректности имени; проверено чтением полного контекста (houseplan-editor-runtime.ts:7620-7655). - Скриншот
docs/images/06-device-editor.pngоткрыт визуально (не только по отпечатку): диалог «Device on the plan», без следов сырого{integration}или иной поломки рендера; это другой диалог, чем «Фильтры обнаружения»/причины на «Доступны», поэтому изменение егоimageSha256между34778d81иe4e1e370не связано с M1/M2 напрямую (визуально контент корректен, дефекта не обнаружено) — частично закрывает пункт «не проверял» из r1, полного пиксельного сравнения со старой версией по-прежнему не делал. - Трейлеры
e4e1e370:Issue: #44,User-Visible: no— корректно (правка внутренняя, видимого поведения/контракта не меняет, changelog не требуется).
Чего не проверял и почему
- Полный набор
demo/smoke_*.mjs,golden,performance_smoke,npm run invariants,pytest tests_backend— как и на r1: дельта не трогает геометрию, canvas, стены, толщину,custom_components/**/*.py; предрелизный периметр, не гейт этого ревью. - Пиксель-в-пиксель сравнение
06-device-editor.pngсо старой версией — открыл текущую версию и убедился в отсутствии дефекта, но не сверял построчно со снимком доe4e1e370. - Регресс-тест на M1/M2 не писал (не моя роль — ревьюер не правит код); зафиксировал их отсутствие как Low L1 с решением не блокировать.
Вердикт
Зелёный. High: 0, Medium: 0. Обе находки r1 (M1, M2) закрыты по существу и подтверждены воспроизведением на актуальном SHA, не на заявлении автора. Один Low (L1, отсутствие регресс-теста) снят решением ревьюера с записью, не блокирует. Открытых продуктовых вопросов нет.
Фактический бюджет циклов код-ревью после этого раунда: 1/4 (этот раунд зелёный и цикл не образует — PROCESS.md §4, #227; потрачен только r1). Заход, использованный для имени документа: r2.