Files
houseplan-card/docs/reviews/CODE-REVIEW-400-r3.md
T
2026-08-31 05:31:46 +00:00

17 KiB
Raw Blame History

CODE-REVIEW-400-r3

Issue: #400 · заход r3 · блокирующих циклов израсходовано 2 из 4 HEAD ревью: 6a0bb985e78b622833e6b5c01b0f0b5453aa9314 Верхний коммит с продуктовым кодом: d153b20a («fix: make the handle paint order a named decision and mutate it»), пересборка bd6722cc.

Скоуп раунда

Единственный открытый вопрос, унаследованный из r2: Medium M1 («мутационный гейт furniture-edge-handles-steal-the-corner правит текст JS-комментария, а не порядок отрисовки, и потому не воспроизводит регрессию AC1»).

r2 закрыл диапазон 73ecfbd1..HEAD, но HEAD на момент того прогона фактически останавливался на f07e3c54 («docs: review document for #400», коммит r1) — коммит с самой правкой, d153b20a, лёгший на ветку позже по UTC-времени (d153b20a закоммичен 2026-08-31T05:14:55Z, f07e3c54 — 2026-08-31T05:06:43Z, а следующий за ним доковый коммит r2, 6a0bb985, — 2026-08-31T05:22:03Z). Иными словами, дерево, на котором физически считался диапазон r2, не содержало правки, хотя она уже существовала на ветке к моменту публикации вердикта r2. Автор указал на это в issue и попросил перезапуск на текущей вершине — это и есть предмет r3.

Дельта r3 = git diff f07e3c54..HEAD (не 73ecfbd1..HEAD, как в r2 — 73ecfbd1 уже входит в f07e3c54 без изменений кода между ними):

demo/smoke_furniture.mjs                | 13 +++++++++----
scripts/mutation-gate.mjs               |  7 +++++--
src/houseplan-card.ts                   | 45 +++++++++++++++++++--------------
+ докатка dist/, custom_components/houseplan/frontend/, demo/srv/assets (class D)
+ docs/images/*.png, docs/images/screenshots.json (пересъёмка скриншотов, class D)
+ docs/reviews/CODE-REVIEW-400-r2.md (публикация прошлого документа, class C)

Только три файла несут смысл; остальное — сгенерированное (D) или опубликованный артефакт прошлого раунда (C). Продуктовый код в этой дельте касается исключительно порядка отрисовки ручек ресайза мебели и тестовой инфраструктуры вокруг него — той же точки, что и M1. AC1/AC2 (приоритет угла), AC3/AC6 (решение по help-строкам) и AC4/AC5 (источник в alignGuides) дельта не трогает содержательно: help-строки и alignGuides в диапазоне не изменились вовсе.

Как проверялось

Дешёвые гейты (зелёного Validate на 6a0bb985 нет — прогнал сам):

Гейт Команда Результат
Типы npx tsc --noEmit чисто, без вывода
Юниты npm test 1667/0 (1 skipped), совпадает с числом, заявленным автором
Сборка npm run build ok, 15.6 с
Сверка трёх копий бандла npm run bundle:sync без диффа в рабочем дереве после — три копии совпадают с закоммиченными байт-в-байт
Бюджет npm run bundle:budget initial View 285 471 / 300 000 B gzip, headroom 14 529 B
Докскрин-отпечаток node scripts/check-docs.mjs --external «Documentation checks passed (7 files, 10 external links)»

src/** в дельте тронут (src/houseplan-card.ts), поэтому check-docs обязателен — прогнан, зелёный.

Геометрия комнат/толщины/layout/marker.space/open_spans дельтой не затронута (ручки ресайза мебели — не структура помещений), npm run invariants не требуется и не запускался.

Мутационные гейты, привязанные к предмету M1 — прогнаны штатным раннером, не руками:

$ node scripts/mutation-gate.mjs --id=furniture-edge-handles-steal-the-corner
ok   чистый прогон: node demo/smoke_furniture_polish.mjs
ok   furniture-edge-handles-steal-the-corner: тест покраснел, как обязан
поймано 1 из 1

$ node scripts/mutation-gate.mjs --id=align-guides-exclude-dead-source
ok   чистый прогон: node demo/smoke_align_guides.mjs
ok   align-guides-exclude-dead-source: тест покраснел, как обязан
поймано 1 из 1

Браузерные смоки — выбор через инструмент плюс собственное суждение:

$ node scripts/smoke-select.mjs --base f07e3c54 --head HEAD
Изменено файлов src/**: 1 · символов проекта на изменённых строках: 2
НЕОПРЕДЕЛЁННОСТЬ: дифф исполняемый, но ни один смок не связан доказуемо.
Символы, которых нет ни в одном смоке: HANDLE_PAINT_ORDER, _dtStart

Инструмент даёт «неопределённость» (новый идентификатор HANDLE_PAINT_ORDER инструменту не известен), но связь доказана иначе — тем же мутационным гейтом, который бьёт ровно по этой константе, и смоками, которые он вызывает как предусловие. Прогнал их напрямую:

$ node demo/smoke_furniture.mjs       → OK (includes reworked SE-handle selector)
$ node demo/smoke_furniture_polish.mjs → OK, smallFurnitureCornerWinsTheHit: true,
                                          largeFurnitureKeepsBothHandles: true
$ node demo/smoke_align_guides.mjs     → OK, devGuideComesFromAnotherMarker: true

golden:verify не запускал: правка d153b20a не меняет итоговый визуальный порядок отрисовки — он был установлен ещё в fe3b85c0 (corners после sides) и остаётся тем же после переноса в HANDLE_PAINT_ORDER = ['edges', 'corners']; меняется только то, как порядок выражен в коде и как его ловит мутатор. Пиксельного риска в дельте нет.

pytest tests_backend не запускал: custom_components/**/*.py дельта не трогает.

Performance-профили не запускал: не названы в AC, дельта не касается путей, чувствительных к перфу (мебель рендерится тем же числом фигур, порядок в разметке не влияет на стоимость кадра).

Разбор находки M1 по коду

src/houseplan-card.ts:825:

const HANDLE_PAINT_ORDER = ['edges', 'corners'] as const;

и рендер (:8480-8492) вместо инлайновых sides.map(...) + corners.map(...) в фиксированном порядке теперь итерирует HANDLE_PAINT_ORDER, диспетчеризуя на role === 'edges'. Порядок для реального рендера не изменился (edges перед corners — corners по-прежнему красятся последними и выигрывают хит), но теперь это значение переменной, а не расположение блоков в шаблоне — то, что и требовал вердикт r1.

scripts/mutation-gate.mjs (мутант furniture-edge-handles-steal-the-corner) теперь патчит саму константу:

- find: '      ${/* #400: corners LAST.',
- replace: '      ${/* mutant: corners no longer last.',
+ find: "const HANDLE_PAINT_ORDER = ['edges', 'corners'] as const;",
+ replace: "const HANDLE_PAINT_ORDER = ['corners', 'edges'] as const;",

Прежний патч менял текст внутри JS-комментария — компилируемый код не менялся, поэтому регрессия не воспроизводилась (поймано 0 из 1, найдено в r1, подтверждено ещё раз в r2 независимо). Новый патч меняет порядок ролей на ['corners', 'edges'], отчего edges красятся последними и перехватывают хит на мелкой мебели — именно то поведение, которое AC1 запрещает. Живой прогон подтверждает: поймано 1 из 1, а смок-предусловие (smoke_furniture_polish) на немутированном коде зелёное.

demo/smoke_furniture.mjs больше не берёт юго-восточную ручку по индексу handles[3] (зависел от порядка отрисовки, что и уронило шард 3 CI на d153b20a), а фильтрует по роли (!dtrot && !dtedge) и берёт третий элемент массива corners. В исходнике corners = [[-1,-1,'nwse'], [1,-1,'nesw'], [1,1,'nwse'], [-1,1,'nesw']] (:8466-8468) — индекс 2 действительно юго-восточный угол (sx>0, sy>0). Комментарий смока («SE — второй nwse») и код согласуются.

M1 закрыта фактически, не только по утверждению автора.

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

Находка r2 Чем закрыта Где это видно
M1: мутант правит комментарий, регрессия AC1 не воспроизводится (поймано 0 из 1) Порядок вынесен в HANDLE_PAINT_ORDER, мутант теперь патчит саму константу src/houseplan-card.ts:825,8480-8492; scripts/mutation-gate.mjs:757-762; живой прогон → «поймано 1 из 1» (выше)
Побочный эффект: smoke_furniture.mjs ловил SE-ручку по индексу handles[3], что зависело от порядка отрисовки Выбор ручки переведён на роль + позицию в массиве corners demo/smoke_furniture.mjs:357-363,405-413; node demo/smoke_furniture.mjs → OK
Мета-находка r2: заявленные автором детали правки не находились ни в одном коммите ни одной ветки на момент прогона r2 Правка физически существует на ветке начиная с d153b20a (создан раньше публикации вердикта r2, но диапазон r2 его не захватил из-за момента снятия дерева) git log, время коммитов см. раздел «Скоуп раунда»; git grep -c HANDLE_PAINT_ORDER / git grep -c cornerHandles на origin/issue/400-beta-polish (оба autor приводил в issue, подтверждено независимо чтением файлов выше)

Унаследовано из r1/r2 (без повторной проверки в этом раунде)

Дельта f07e3c54..HEAD не касается кода этих пунктов, поэтому доказательства r1 остаются в силе без повторного исполнения:

  • AC1/AC2 (приоритет угловой ручки на мелкой мебели) — функционально не менялись в этой дельте (визуальный порядок тот же), но я всё же перепрогнал smoke_furniture_polish.mjs как побочный эффект проверки мутанта: smallFurnitureCornerWinsTheHit: true, largeFurnitureKeepsBothHandles: true. Документ: CODE-REVIEW-400-r1, SHA 73ecfbd1.
  • AC4/AC5 (_alignCandidates использует актуальный источник перетаскивания вместо мёртвого _drag?.id) — код src/logic.ts в дельте не тронут; повторно прогнал smoke_align_guides.mjs только как предусловие второго мутанта: devGuideComesFromAnotherMarker: true. Документ: CODE-REVIEW-400-r1, SHA 73ecfbd1.
  • AC3/AC6 (help-строки #86 остаются в initial-чанке — зафиксированное решение в docs/ARCHITECTURE.md, бюджет в пределах потолка) — файлы решения дельтой не тронуты. Число бюджета пересчитано в этом раунде (см. ниже) и остаётся в пределах потолка. Документ: CODE-REVIEW-400-r1, SHA 73ecfbd1.
  • Две Low-находки из r1 (AC4 доказан e2e-смоком вместо юнита; вес 38 help-строк не перемерян отдельно) — сняты ревьюером r1 с записью, дельта их не затрагивает. Документ: CODE-REVIEW-400-r1, SHA 73ecfbd1.

Проверено и корректно (новое в этой дельте)

  • HANDLE_PAINT_ORDER — именованная константа, используется в единственном месте рендера, тип readonly ['edges','corners'] через as const, tsc чист.
  • Мутационный гейт furniture-edge-handles-steal-the-corner теперь бьёт по контракту, а не по комментарию — подтверждено живым запуском, «умеет падать» и падает.
  • Мутационный гейт align-guides-exclude-dead-source не задет дельтой, перепрогнан для контроля регрессии — без изменений, 1 из 1.
  • smoke_furniture.mjs: выбор SE-ручки по роли согласован с индексами массива corners в источнике.
  • Три копии бандла синхронны после пересборки, бюджет не превышен.

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

  • Полная матрица смоков (209 файлов) — задача не задевает ничего за пределами мебельных ручек и направляющих; smoke-select.mjs подтверждает узкий диапазон (src/** изменено файлов: 1). Полный прогон — предрелизная обязанность.
  • golden:verify — итоговый визуальный порядок отрисовки не изменился этой дельтой (см. обоснование выше); риск пикселей не вижу.
  • npm run invariants — дельта не касается геометрии комнат/толщины/ layout/marker.space/open_spans.
  • pytest tests_backend — custom_components/**/*.py не тронут.
  • Performance-профили — не названы в AC, путь не чувствителен к перфу.
  • Разница бюджета (+46 Б: 285 425 → 285 471) — не проверял отдельно происхождение (лишний байт от структуры HANDLE_PAINT_ORDER.map против инлайновых .map), потому что рост укладывается в потолок с запасом и не меняет вывод AC3/AC6; предупреждение скрипта про запас <15000 Б — известная, не новая для этого раунда ситуация (#367), не следствие правок r3.

Вердикт

M1 — единственная блокирующая находка, унаследованная из r1/r2 — закрыта по существу и подтверждена исполнением, а не только заявлением автора. Новых находок в дельте f07e3c54..HEAD нет. Зелёный.