Files
houseplan-card/docs/reviews/CODE-REVIEW-385-r3.md
T
2026-08-30 08:54:32 +00:00

16 KiB
Raw Blame History

CODE-REVIEW-385-r3

  • Issue: https://github.com/Matysh/houseplan-card/issues/385
  • Заход: r3 · блокирующих циклов израсходовано до этого раунда: 1/4
  • Материал: git log --oneline origin/dev..HEAD / git diff origin/dev...HEAD на SHA e561e5fc5d37d71d583a3c675a83112d50bf3d13 (origin/dev = fbbea475, ветка полностью включает dev — конфликта merge-base нет, S6→S7 без ребейза на этот раз)
  • Предыдущий раунд: docs/reviews/CODE-REVIEW-385-r2.md, вердикт жёлтый, SHA 2e77fe09a554692bc23bf1f2497c2ae6c8c02059 (SHA назван в документе r2 — не находка в этот раз; в самом тексте issue-комментария #10 SHA буквально не напечатан, только в артефакте)

Почему разбор по дельте, а не заново

Единственный новый коммит с момента r2 — e561e5fc («fix: drop the unreachable virtual-radio guard, keep honest evidence (#385 r2-M1)»), адресующий ровно и только находку r2-M1. Delta не ребейз (origin/dev не продвинулся), не меняет контракт поведения и не задевает новую подсистему — локальнее самой задачи. По §2.9/§2.10 разбор ограничен дельтой: переисследуются только AC1 (соседний код в том же файле, проверено, что не задет) и AC2 (предмет находки); AC3–AC6 наследуются из r2 без повторного исполнения там, где сами файлы не изменились.

git diff 2e77fe09a554692bc23bf1f2497c2ae6c8c02059..HEAD --stat

даёт: src/houseplan-editor-runtime.ts (11 строк), demo/smoke_value_face_source.mjs (14 строк), плюс синхронный пересобранный класс D (dist/**, custom_components/houseplan/frontend/**) и docs/images/screenshots.json (fingerprint bump). Класса A/B за пределами этих двух файлов дельта не касается; docs/reviews/CODE-REVIEW-385-r2.md уже был частью дерева на момент r2 (коммит c872c935 лёг между материалом r2 и e561e5fc, публикация документа, не код).

Содержание правки

- if (d.binding === 'virtual') {
-   this.host._markerDialog = { ...d, bindingMode: 'virtual', bindingOpen: false };
-   return;
- }

убран из обработчика @change радиокнопки «virtual» (houseplan-editor-runtime.ts:12251-12266), заменён комментарием, почему сброс здесь всегда легитимен. Синхронно из demo/smoke_value_face_source.mjs убрано поле sameVirtualKeepsSource и шаги, которые его вычисляли (искусственная установка sentinel-source + повторный клик по уже отмеченной радиокнопке).

Это буквально вариант «а» из предложенных r2 (см. r2, находка Medium): убрать недостижимый guard и вакуумную строку смока, а не переквалифицировать AC2 на «проверено чтением» при сохранении мёртвого кода.

Как проверялось (гейты, лично на этом SHA)

Гейт Команда Результат
typecheck npx tsc --noEmit чисто
unit npm test 1593 tests, 1592 pass / 0 fail / 1 skipped
build + 3 копии бандла npm run build; npm run bundle:sync без diff, git status --short пуст после сборки
check-docs node scripts/check-docs.mjs зелёный (7 файлов, 10 внешних ссылок)
bundle:budget npm run bundle:budget initial View 277 994 / 300 000 Б gzip (запас 22 006 Б; на 1 Б меньше, чем в r2 — ожидаемо, дельта убирает код)
no-new-any node scripts/no-new-any.mjs --base origin/dev --head HEAD 20 новых строк в 2 файлах (было 23 в r2 — дельта чистит код), новых any нет
smoke-select node scripts/smoke-select.mjs --base origin/dev --head HEAD тот же результат, что в r2: 33 слабые связи по общему имени _markerDialog (НЕОПРЕДЕЛЁННОСТЬ), ни одного прямого совпадения кроме уже прогнанного smoke_value_face_source.mjs
smoke_value_face_source node demo/smoke_value_face_source.mjs (после bundle:sync) все 15 оставшихся полей true (поле sameVirtualKeepsSource намеренно удалено вместе с кодом, который оно проверяло)
мутант AC1 node scripts/mutation-gate.mjs --id=same-binding-click-resets-source 1/1 поймано — соседний, нетронутый дельтой guard кандидат-листа по-прежнему тестируется честно
golden:verify — не прогонял: imageSha256 во всех сценариях screenshots.json не изменились между r2 и r3 (сверено построчно диффом), поменялся только sourceFingerprint/sourceSha256 — дифф не меняет рендер
инварианты модели — не прогонял: дельта не трогает геометрию/layout/marker.space/толщину стен
backend pytest (AC5) — не прогонял: дельта не касается import_export.py, AC5 не затронут — наследуется из r2 без повторной попытки (см. «Унаследовано»)
мутант AC4, process-gate — не прогонял: дельта не касается process-gate.mjs, AC4 не затронут

Разбор по AC — только то, что дельта задевает

AC1 (клик по тому же кандидату в списке — no-op). Код не тронут дельтой (:12305-12325 идентичен r2). Перепроверено мутантом same-binding-click-resets-source — красный при мутации, зелёный без неё — всё ещё доказано автотестом, который умеет падать. Соседство изменённого кода в том же файле не задело эту ветку — подтверждено чтением диффа и исполнением мутанта.

AC2 (виртуальная радио-ветка). Ровно предмет находки r2-M1. Дельта убирает guard и вакуумную проверку. Структурное обоснование из r2 (радиокнопка не эмитит change, если уже отмечена — проверено r2 голым Playwright-тестом на этом же Chromium) не переисполнялось заново мной, но и не должно: удалённый код был доказательством отсутствия дефекта, а не источником риска — с его удалением исчез сам объект спора. Осталось проверить две вещи: (1) что комментарий, объясняющий инвариант, на месте — да, в обоих файлах; (2) что удаление не задело bindingResetToAuto (реальная смена на virtual всё ещё сбрасывает) — смок подтверждает, поле true.

Итог: AC2 закрыт как «проверено чтением/структурным разбором r2, не автотестом» — честная запись, а не заявление о тесте, которого нет. Смока для этой ветки больше не существует, и это правильно: смок на недостижимую ветку был бы ложным свидетельством, ровно тем, что нашла r2. Это не регрессия дисциплины «тест умеет падать» — ветки, которую можно было бы протестировать честно, больше не существует.

AC3–AC6: файлы, которые их доказывают (devices.ts, process-gate.mjs, import_export.py, гейты), дельтой не затронуты — наследуются из r2 без повторного исполнения (см. ниже). AC6 (гейты/бюджет) переисполнен целиком выше в таблице, так как он не привязан к конкретному файлу, а к состоянию дерева.

Закрытие раунда r2

Находка r2 Чем закрыта Где видно
M1 (Medium, в скоупе): AC2 заявлен доказанным смоком (sameVirtualKeepsSource), но ветка структурно недостижима реальным кликом — тест не умеет падать, хендофф заявил не то, что есть Вариант «а»: guard и вакуумная проверка смока удалены, оставлен объясняющий комментарий; заявление об AC2 неявно понижено до «проверено структурным разбором», без ложной ссылки на тест src/houseplan-editor-runtime.ts:12252-12261 (комментарий вместо guard); demo/smoke_value_face_source.mjs:173-183 (поле и шаги удалены); коммит e561e5fc, трейлер Issue: #385
L1 (Low, r1, оставлена автору): дублирующийся one(name) в process-gate.mjs Не в скоупе этой дельты — process-gate.mjs не менялся без изменений, как и раньше

Унаследовано из r2 (docs/reviews/CODE-REVIEW-385-r2.md, SHA 2e77fe09a554692bc23bf1f2497c2ae6c8c02059)

Принято без повторного исполнения в этом раунде — файлы не менялись дельтой:

  • AC3 (src/devices.ts:889-898, условные спреды в rewriteMarkerControlReferences) — юнит-доказательство и разбор r2 в силе.
  • AC4 (scripts/process-gate.mjs:109-121, общий isReleaseCommit) — юнит со шпионом и мутант release-proof-computed-for-every-commit r2 в силе; L1 (дубль one()) остаётся неисправленной низкоприоритетной находкой на усмотрение автора.
  • AC5 (custom_components/houseplan/import_export.py:504-527, парная нейтрализация при экспорте) — код не менялся с версии, которую r1 гонял исполнением (446 passed), r2 подтвердил чтением; в этой песочнице backend-харнесс так же недоступен (.venv-backend отсутствует), исполнением не перепроверял.
  • Таблица «Закрытие раунда r1» и заключение «третьего места сброса нет» (grep по bindingMode:/binding: ) — дельта не добавляет новых точек записи _markerDialog, наследую без повтора.
  • Оценка «одно число — один источник»: задача не вводит новых видимых пользователю величин — в силе, дельта не меняет вывод.

Что проверено и корректно

  • Единственный коммит дельты несёт Issue: #385, User-Visible: no — корректно: удаление недостижимого кода не меняет наблюдаемое поведение, ченджлоги не тронуты (и не должны быть).
  • Три копии бандла (dist/, custom_components/.../frontend/, demo/srv/assets/) синхронны после сборки на этом SHA.
  • Удаление поля смока не оставляет висячих ссылок: sameVirtualKeepsSource встречается только в исторических документах ревью (r1/r2), не в коде.
  • checkAll (demo/serve.mjs:29-31) итерирует по фактическим ключам возвращённого объекта — удаление поля не создаёт риска ложно-зелёной проверки несуществующего ключа.
  • Гард кандидат-листа (AC1, нетронутая часть того же файла) не задет дельтой — подтверждено мутантом.

Чего не проверял и почему

  • Backend pytest (AC5, tests_backend/test_ha_import_export.py) — файл не в дельте, наследую из r2 (там же не исполнялся из-за отсутствия харнесса в песочнице, закрыт чтением); полный HA-харнесс — предрелизный гейт.
  • golden:verify — не прогонял: imageSha256 во всех сценариях screenshots.json идентичны между r2 и r3, дифф не меняет рендер.
  • Инварианты модели (scripts/model-invariants.mjs) — дельта не трогает геометрию/layout/marker.space/толщину стен.
  • Мутант AC4 (release-proof-computed-for-every-commit) — process-gate.mjs не в дельте, AC4 не затронут.
  • 31 из 33 «слабых» смоков от smoke-select.mjs — та же оценка, что в r2: общее имя _markerDialog, диф не касается ничего вне логики binding-выбора, покрытой прогнанным smoke_value_face_source.mjs.
  • performance_smoke и полный mutation-gate — не в AC, дельта убирает код, а не добавляет чувствительные к перфу пути; предрелизный гейт.

Вердикт

Зелёный. Единственная находка r2 (Medium M1) закрыта по существу — недостижимый код и ложное свидетельство о нём убраны, а не замаскированы; честная классификация доказательства AC2 восстановлена. High и новых Medium нет. Разбор ограничен дельтой согласно §2.10: AC1 подтверждён повторно (мутант в том же файле), AC2 — предмет находки, AC3–AC6 наследуются из r2 без повторного исполнения. Задача готова к очереди на пре-релиз.