Files
houseplan-card/docs/reviews/CODE-REVIEW-44-r1.md
T
2026-08-30 11:42:51 +00:00

24 KiB
Raw Blame History

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, три смока выше) остаётся зелёным, но ни один из них не находится в состоянии «умеет упасть» именно на этих двух дефектах — проверено тем, что при ручном откате правки (см. проверку ниже) эти же тесты продолжали бы зеленеть.

Проверка, что фикс реален, а не «покрашен зелёным»: временно воспроизвёл до-фикс поведение и убедился, что регресс возвращается —

  1. заменил 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, SHA 34778d81e45ec172af9b76ff71c761565fa0a885.

Унаследовано из 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_integrations decision-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.