22 KiB
CODE-REVIEW-229-r2
Issue: #229 — «прямая стена, нарисованная в несколько кликов, остаётся одной записью, а не рядом швов».
Заход: r2. Ревью ТЗ прошло тремя заходами (r1 M×3 → r2 M×1 → r3 зелёный, SHA 1ecd267).
Код-ревью: r1 — красный (High×2, SHA 50a00e4), r2 — этот документ, SHA ed13b4a.
Блокирующих циклов до этого раунда: 1/4.
Скоуп раунда (§2.10)
Предыдущий заход (r1) получен на диапазоне origin/dev...HEAD при HEAD = 50a00e4
(коммиты e6be43b feat + 50a00e4 docs-скриншоты). SHA не был назван в самом
тексте вердикта — фиксирую его здесь по факту: комментарий-вердикт содержит только
имена коммитов, точку HEAD восстановил из истории веток (git log --oneline origin/dev..HEAD, коммит непосредственно перед фикс-коммитом ed13b4a).
Дельта раунда — git diff 50a00e4..HEAD (коммит ed13b4a, один коммит):
custom_components/houseplan/frontend/houseplan-card.js | 6 +- (сгенерировано)
demo/srv/assets/houseplan-card.js | 6 +- (сгенерировано)
dist/houseplan-card.js | 6 +- (сгенерировано)
docs/images/screenshots.json | 22 +- (отпечаток источника, ожидаемо)
docs/reviews/CODE-REVIEW-229-r1.md | 282 ++ (публикация документа r1, не код)
demo/smoke_wall_chain_merge.mjs | 22 +-
scripts/mutation-gate.mjs | 25 ++
src/houseplan-card.ts | 9 +-
src/plan-optimizer.ts | 7 +-
test/plan-optimizer.test.mjs | 20 ++
Дельта локальна: один коммит, пять содержательных файлов, оба High из r1 и
только они. Продуктовая рамка не менялась, новая подсистема не задета, ребейза
не было (50a00e4 — предок HEAD напрямую). Полный повторный разбор не требуется
по правилу §2.10 — разбор ограничен дельтой плюс тем, до чего дельта дотягивается
(AC2 и §8.6/AC8, единственные AC, чьё доказательство эта правка меняет).
Закрытие раунда r1
| Находка r1 | Чем закрыта | Где это видно |
|---|---|---|
High-1: _mergeSpacePartitions (houseplan-card.ts) и optimizePlans (plan-optimizer.ts) делили уже сырой полигон комнаты (roomPoly) на NORM_W второй раз — комната «улетала» в область ~0.0001, T-стык к её стороне не находился |
Обе обвязки перестали делить roomPolygons на NORM_W: src/houseplan-card.ts:6557-6560, src/plan-optimizer.ts:507-510. Комментарий на месте объясняет, что roomPoly уже возвращает координаты в масштабе перегородок |
Прочитано и перепроверено: roomPoly (src/logic.ts:103-108) отдаёт r.x/r.y/r.w/r.h без масштабирования; sp.partitions[i].a в _finishWallChain — тоже сырые координаты (segment.a[0] / NORM_W, houseplan-card.ts:6609). Т.е. деление было лишним объективно, не только по утверждению автора. Дополнительно воспроизведено исполнением: новый юнит test/plan-optimizer.test.mjs:497-516 («a node on the side of a room survives the sweep») зелёный на текущем коде, красный при ручном возврате деления (см. «Как проверялось») |
High-2: _finishWallChain вызывает слияние (houseplan-card.ts:6612 тогда) раньше, чем убирает завершаемый черновик из sp.room_drafts, поэтому собственные концы черновика ложно считались чужим примыканием |
draftEnds теперь исключает запись с id === this._activeDraftId (src/houseplan-card.ts:6566), тем же паттерном, что plan-snap-overlay.ts |
Прочитано: исключение стоит до this._activeDraftId = null (строка 6626), то есть значение ещё актуально в момент вызова. Воспроизведено исполнением: demo/smoke_wall_chain_merge.mjs теперь рисует продолжение реальными кликами (click(), строки 53-62) вместо присвоения _path, и это зелёное; при ручном откате исключения (снятие условия if (draft?.id === this._activeDraftId) return [];) смок красный — подтверждено запуском мутанта chain-merge-sees-own-draft через node scripts/mutation-gate.mjs --id=chain-merge-sees-own-draft (см. ниже) |
Обе High-находки закрыты по существу, не заплаткой: в обоих случаях автор
объяснил причину (общая система координат roomPoly/partitions; порядок
операций в _finishWallChain) и добился падения теста на прежнем коде, а не
подобрал совпадающие числа постфактум — я это проверил самостоятельно (см. ниже).
Унаследовано из r1
Из документа docs/reviews/CODE-REVIEW-229-r1.md (SHA 50a00e4) принято без
повторной проверки — дельта не касается их доказательной поверхности:
- AC1 (прямая цепочка — одна запись) — не тронут;
demo/smoke_wall_chain_merge.mjsпроверкиstraightRunIsOneRecord/straightRunSpansTheChain/straightRunKeepsThicknessне менялись. - AC3 (проём не двигается, оба представления —
host.tи legacyx/y/angle) —wall-merge.tsи его юниты не менялись в этой дельте. - AC4 (разная толщина не сращивается), AC5 (допуски
EPS_JOIN), AC6 (детерминизм по порядку) — та же пара,wall-merge.ts/wall-merge.test.mjsвне дельты. - AC7 (оптимизатор сращивает накопленное, идемпотентность) — логика сведения
накопленных швов в
plan-optimizer.tsне менялась, изменилась только область видимости комнат внутри той же функции. - AC9 (release-артефакты) — не применим к этому коммиту:
User-Visible: no, changelog не требуется; сам факт трейлера перепроверен (см. «Гейты»). - Общая корректность структуры ТЗ, i18n, touch-контракт (
Touch editor: not exposed) — вне дельты кода, r1 их не пересматривал.
Что проверялось и как
Всегда (быстрые гейты, прогнаны заново на ed13b4a):
| Гейт | Команда | Результат |
|---|---|---|
| Типы | npx tsc --noEmit |
чисто |
| Юниты | npm test |
1004/1004, 0 fail |
| Сборка + сверка бандла | npm run build + cmp трёх копий (dist, custom_components/houseplan/frontend, demo/srv/assets) |
идентичны байт-в-байт, дерево чистое после копирования |
По необходимости, определённой дельтой:
node demo/smoke_wall_chain_merge.mjs— единственный смок, прямо изменённый дельтой (H2) и покрывающий сценарий High-1/High-2 на живом клик-пути. Зелёный, 9/9 проверок.- Соседние смоки того же семейства (тронутый
_finishWallChain/_mergeSpacePartitionsи оптимизатор):optimize_coordinate_canonicalization,wall_junctions,partition_openings,free_walls— зелёные, регрессий не внесено. node scripts/check-docs.mjs— прошёл (7 файлов, отпечатокdocs/images/screenshots.jsonсинхронен с пересобранным бандлом — сам факт изменения этого файла в дельте ожидаем, не дефект).node scripts/process-gate.mjs— пройден, 0 предупреждений; трейлерыIssue: #229/User-Visible: noнаed13b4aкорректны, changelog не требовался и не менялся.- Дисциплина «тест умеет падать» — проверена исполнением, не на словах, для обеих
High-находок:
- вручную вернул деление на
NORM_Wвsrc/plan-optimizer.ts, пересобралtest-build(npx tsc -p tsconfig.test.json && node scripts/fix-test-build.mjs) и прогналnode --test --test-name-pattern="issue 229" test/plan-optimizer.test.mjs: новый тест «a node on the side of a room survives the sweep» красный (1 !== 0, ровно симптом из r1 —partitionsMergedвместо ожидаемых 0); восстановил файл — тест снова зелёный, дерево чистое; - прогнал мутант
node scripts/mutation-gate.mjs --id=chain-merge-sees-own-draftчерез штатный харнесс (git worktree + пересборка бандла): чистый прогон зелёный, мутант красный, «поймано 1 из 1» — H2 подтверждён штатным гейтом.
- вручную вернул деление на
Не прогонялось и почему: полный набор из 127 смоков, golden:verify,
pytest tests_backend — дельта не трогает Python, рендер, слои и любую другую
поверхность вне рисования перегородок и оптимизатора; полные наборы — предрелизный
гейт (§8), не гейт ревью. Performance-профили не запускал — в AC не названы,
дельта не касается путей, отмеченных как чувствительные к перфу.
Находки
Medium-1 (в скоупе задачи): регрессионное покрытие High-1 закрывает только один из двух исправленных вызовов
src/houseplan-card.ts (_mergeSpacePartitions, живой путь рисования) и
src/plan-optimizer.ts (optimizePlans, «Оптимизировать планы») содержали
одинаковый дефект лишнего деления roomPolygons на NORM_W, и оба
исправлены в этом коммите идентичным образом. Но новый регрессионный тест
(test/plan-optimizer.test.mjs:497-516) и сторожащий его мутант
partition-merge-rescales-rooms (scripts/mutation-gate.mjs:718-730) проверяют
только путь plan-optimizer.ts. Путь houseplan-card.ts — тот самый, для
которого r1 привёл второе, самостоятельное воспроизведение («в браузере через
реальные клики, space.partitions.length === 1 вместо 2») — не получил ни
юнита, ни смок-сценария, ни мутанта.
Воспроизведение (исполнением): вручную вернул то же лишнее деление
только в src/houseplan-card.ts (оставив plan-optimizer.ts в исправленном
виде), пересобрал бандл и test-build, прогнал весь набор:
npm test— 1004/1004 pass, ни один тест не покраснел;node demo/smoke_wall_chain_merge.mjs— все 9 проверок зелёные, дефект не затронут ни одной из них (в этом смоке ни разу не участвует комната — только голые перегородки).
Восстановил файл — поведение снова корректно (проверено чтением: roomPoly
и координаты перегородок в _finishWallChain/_mergeSpacePartitions — один и
тот же масштаб, деление объективно лишнее в обоих местах). Сам текущий код
корректен; дефект — в отсутствии автоматической защиты именно этого вызова:
следующая правка _mergeSpacePartitions может тихо вернуть тот же баг на живом
пути рисования, и ни npm test, ни штатный набор смоков этого не заметят.
Почему это Medium, а не High: правка, которая лежит в этом коммите, сама по себе корректна и доказана чтением плюс ручным воспроизведением (я его провёл сам, а не поверил заявлению автора) — AC2 для этого вызова не «предположительно доказан», а доказан наблюдением. Дефект — в будущей защите, а не в текущем поведении, поэтому не блокирует, но должен быть закрыт в этой же задаче: она и её мутационный гейт — единственное место, где это естественно сделать, и Medium в скоупе чинится в текущем issue (§2.7, #202).
Предложение: добавить в demo/smoke_wall_chain_merge.mjs сценарий с
комнатой, к середине стороны которой рисуется примыкающая перегородка через
настоящие клики (по образцу нового юнита в plan-optimizer.test.mjs, но через
живой _finishWallChain), и зарегистрировать для src/houseplan-card.ts
второй мутант-близнец partition-merge-rescales-rooms, гвардом на этот смок.
Low (снято без действия): точный SHA предыдущего вердикта не был назван явно
Комментарий-вердикт r1 называет коммиты (e6be43b, 50a00e4), но не отдельно
итоговый SHA HEAD на момент разбора — пришлось восстанавливать по истории
(см. «Скоуп раунда»). Не влияет на проверяемость: коммиты названы однозначно,
диапазон восстанавливается детерминированно. Снимаю без действия, отмечаю для
будущих раундов этой же задачи.
Находка вне скоупа — заводится отдельным issue
При проверке «тест умеет падать» для High-1 через штатный харнесс
(node scripts/mutation-gate.mjs --id=partition-merge-rescales-rooms) прогон
завершился на этапе чистого прогона:
FAIL чистый прогон: node --test --test-name-pattern="issue 229" test/plan-optimizer.test.mjs
красный без мутанта
Error [ERR_MODULE_NOT_FOUND]: Cannot find module '.../test-build/plan-optimizer.js'
Причина — не в мутанте и не в тесте: makeWorktree()/buildBundle()
(scripts/mutation-gate.mjs:964-990) создают чистый git-worktree и пересобирают
только rollup (бандл), но никогда не выполняют npx tsc -p tsconfig.test.json && node scripts/fix-test-build.mjs. Любой мутант, чей guard — короткая форма
node --test --test-name-pattern="…" test/<файл>.test.mjs, где <файл>.test.mjs
импортирует ../test-build/*.js (а не читает src/* текстом), в свежем
worktree не находит test-build/ вообще и падает с ERR_MODULE_NOT_FOUND — то
есть «гейт словил поломку» на самом деле означает «гейт не может исполниться»,
а runCleanGuards() (базовый прогон без мутанта) тоже красный по той же причине,
поэтому полный прогон встаёт на первом же таком мутанте с кодом выхода 2.
Это не специфично для #229: тот же паттерн гварда уже используют мутанты issue
#220 (chain-merge-sweeps-... — нет, конкретно stale-space-position-... и три
мутанта у test/space-order.test.mjs, scripts/mutation-gate.mjs:848-894,
внесены коммитом a8aeecc, который старше #229 целиком) и старые мутанты
самого #229 из e6be43b (test/wall-merge.test.mjs, шесть мутантов,
scripts/mutation-gate.mjs:757-820) — я прогнал один из них
(partition-merge-ignores-thickness) и получил ту же ошибку. Значит недостаток
живёт в общей инфраструктуре scripts/mutation-gate.mjs, предшествует этой
задаче и не создан её диффом (дифф только добавил ещё один мутант того же,
уже дефектного, вида). Правка чужого скоупа с этой ветки запрещена (#202) —
заведён отдельный issue:
Новый issue: #235
«mutation-gate.mjs не пересобирает test-build в worktree-мутантах — гварды,
читающие ../test-build/*.js, всегда красны на чистом прогоне» → метки
tech-debt, P2, S1-new, ссылка на #229 и #220 как источники затронутых
мутантов.
Само поведение фиксов High-1/High-2 это не отменяет: я подтвердил обе находки
без штатного харнесса — прямым воспроизведением в основном рабочем дереве
(ручной откат патча → пересборка test-build в реальном, не временном,
дереве → тест красный → восстановление) и, для High-2, штатным прогоном
мутанта, чей guard не зависит от test-build (браузерный смок).
Итог по AC
| AC | Статус | Доказательство |
|---|---|---|
| AC1 | не тронут дельтой | унаследовано из r1 |
| AC2 | подтверждён повторно | юнит plan-optimizer.test.mjs (комната) + смок (перегородка↔перегородка) — оба зелёные и оба доказано умеют падать; путь houseplan-card.ts доказан чтением кода, автотеста нет (Medium-1) |
| AC3 | не тронут дельтой | унаследовано из r1 |
| AC4-AC6 | не тронуты дельтой | унаследовано из r1 |
| AC7 | не тронут дельтой | унаследовано из r1 |
| AC8 (§8.6) | подтверждён повторно | смок chain_wall_merge, мутант chain-merge-sees-own-draft через штатный харнесс — красный на откате, зелёный на текущем коде |
| AC9 | не применим к этому коммиту | User-Visible: no, трейлеры проверены process-gate.mjs |
Вердикт
Обе High-находки r1 закрыты по существу и перепроверены исполнением, а не по
заявлению автора. Найден один новый Medium в скоупе задачи (регрессионное
покрытие High-1 однобокое — защищён только путь оптимизатора, путь живого
рисования нет) и один Medium вне скоупа (инфраструктурный дефект
mutation-gate.mjs, предшествующий этой задаче, заведён отдельным issue).
High нет — вердикт жёлтый.
Вердикт: жёлтый · заход r2 · блокирующих циклов 2/4 · High: 0 · Medium: 1 → в задаче