40 KiB
CODE-REVIEW-160-r1 — Isometric Stage 3
- Issue: https://github.com/Matysh/houseplan-card/issues/160
- Этап: код-ревью (PROCESS.md §2.7)
- Заход: r1 · блокирующих циклов израсходовано 0 из 4 (предыдущая попытка остановилась до чтения кода из-за конфликта ребейза — цикл не потрачен)
- Ветка:
issue/160-isometric-stage3 - SHA материала ревью:
ee7d486924d5b1641a56f671bbf94e965fb76493(провереноgit rev-parse HEADнепосредственно перед подведением итогов, PROCESS.md §2.7) - ТЗ:
docs/specs/160-isometric-stage3.md(commitd82be48d), спек-ревью — зелёное,docs/reviews/SPEC-REVIEW-160-r1.md
Скоуп
Изометрический Stage 3: фиксированный поворот камеры rotDeg 0→4, разделение
floor/raised плоскостей для device marker / room name-card / opening lock badge
(grounding shadow + tether + ограниченный inward nudge), более читаемая глубина
проёмов (jamb/reveal/leaf/frame/sill), общая система теней с одним light vector,
детерминированная theme-aware текстура, согласованный depth order, кэш/fingerprint
с cap 8, degradation для forced-colors/no-filter, полный AC1–AC15, 12 red witnesses
W1–W12. Diff: 90 файлов, +8759/−1374 строк одним продуктовым коммитом
feat: implement isometric stage 3 (+3 doc/process коммита), трейлеры
Issue: #160 / User-Visible: no на всех — сверено git log origin/dev..HEAD.
Материал: git log --oneline origin/dev..HEAD, git diff origin/dev...HEAD.
Диапазон полный, не по дельте — это первый содержательный проход по коду
(предыдущая передача на ревью не читала код из-за конфликта ветки).
Как проверялось — гейты
| Гейт | Команда | Результат |
|---|---|---|
| Typecheck | npx tsc --noEmit |
зелёный |
| Unit | npm test |
2071 passed, 1 skipped, 0 failed (совпадает с заявленным авторским прогоном) |
| Build | npm run build |
зелёный, dist пересобран |
| Bundle sync | npm run bundle:sync |
зелёный |
| Bundle budget | npm run bundle:budget |
зелёный: initial View 299464 B gzip (потолок 300000±2000), lazy isometric 12083 B gzip; известное предупреждение о малом запасе (#367), не относится к #160 |
git diff --check |
— | чисто, конфликт-маркеров/trailing whitespace нет |
node scripts/check-docs.mjs |
обязателен диффом по src/** (PROCESS.md §2.7/§2.9) |
КРАСНЫЙ — см. Находка H1 |
npm run golden:verify (эта среда — Linux) |
диагностика, не приёмка | все 137 незатронутых сценариев passed, 8 isometric-* — different (камера/Stage 3, ожидаемо), 5 новых isometric-stage3-* — missing-baseline (ожидаемо, не принимались) |
scripts/mutation-gate.mjs --id=stage3-w1…w12 |
все 12 witnesses по отдельности | 12/12 подтверждены лично (см. таблицу AC ниже) — не поверено на слово |
node demo/smoke_entry_stale.mjs |
по указанию smoke-select.mjs («зарегистрированная связь» через общий EditorRuntimeLoader) |
зелёный |
node scripts/smoke-select.mjs --base origin/dev --head HEAD |
выбор целевых смоков | см. «Чего не проверял» |
demo/benchmark_large_house.mjs --profile=isometric-stage3-dense-v1 --samples=1 |
однократный sanity-прогон нового профиля | зелёный, см. «Производительность» |
Пояснение к git diff --check, unit-числу и bundle-budget: числа сошлись с тем,
что автор указал в хендоффе issue — независимая проверка, не копирование отчёта.
Таблица «AC · чем доказан · чем краснеет» (PROCESS.md §2.7, #435)
Только защитные AC (валидация/guard/лимит/invariant). Расположенческие/текстовые AC — отдельно ниже без третьего столбца.
| AC | Доказано | Чем краснеет (лично прогнано) |
|---|---|---|
| AC2 камера +4°/20° | test/iso-projection.test.mjs («exact fixed») |
stage3-w1-camera-rotation-reset (rotDeg 4→0) — поймано 1 из 1 |
| AC3 raised/floor матрица (room label не должен остаться на floor) | test/iso-overlays.test.mjs («exact D2 overlay matrix») |
stage3-w2-room-label-left-on-floor — поймано 1 из 1 |
| AC3 vacuum остаётся floor-bound | test/isometric-contract.test.mjs («raises only device») |
stage3-w3-vacuum-raised-with-devices — поймано 1 из 1 |
| AC4 44×44 hit target | demo/smoke_isometric_contract.mjs |
stage3-w4-device-target-loses-44px-floor — поймано 1 из 1 |
| AC5/AC13 nudge никогда не пишет config/layout/storage | demo/smoke_isometric_live_touch.mjs |
stage3-w5-runtime-nudge-writes-storage — поймано 1 из 1 |
AC7 show_borders:false не даёт raised-слоёв |
demo/smoke_isometric_live_touch.mjs |
stage3-w6-no-borders-keeps-raised-plates — поймано 1 из 1 |
| AC8/AC12 HA Sun не входит в structural fingerprint/light vector | demo/smoke_isometric_live_touch.mjs |
stage3-w7-sun-state-enters-structural-key — поймано 1 из 1 |
| AC8 material defs O(1), не per-face | demo/smoke_isometric_contract.mjs |
stage3-w8-material-defs-created-per-face — поймано 1 из 1 |
| AC6 door/gate hinge·face·flip (заявленный контракт W9 — face turn direction) | test/iso-scene-render.test.mjs («gate flips move unhosted structural face») |
stage3-w9-gate-face-flip-reversed — поймано 1 из 1 |
| AC9/AC12 Flat-fallback не засчитывается как успешный Iso-сэмпл | test/performance-workflow.test.mjs («runner fails closed») |
stage3-w10-flat-fallback-counted-as-success — поймано 1 из 1 |
| AC5 tether обязателен после ненулевого nudge | test/iso-overlays.test.mjs («wall-aware nudge») |
stage3-w11-nudged-overlay-loses-tether — поймано 1 из 1 |
| AC1/AC13 нет отдельного Stage 3 expiry/URL-ключа | test/isometric-contract.test.mjs («Labs iso is presentation-only») |
stage3-w12-separate-alpha-url-key-restored — поймано 1 из 1 |
Все 12 команд запущены мной лично (не по описанию автора), каждая дала
поймано 1 из 1 — «тест умеет падать» подтверждено, а не принято на слово.
Пустых третьих столбцов нет, кроме одного отдельно обсуждаемого случая:
AC6-hinge/flip покрыт для заявленного W9-пути (face turn direction в
iso-scene-render.ts), но у смежной функции leafBasis (src/iso-openings.ts)
есть непокрытая ветка того же семейства дефектов — см. Находку M3.
AC10 (live layers), AC11 (lifecycle/touch/kiosk), AC14 (гейты), AC15 (golden/docs) не заявляют отдельной защиты сверх вышеперечисленного — доказаны combined-смоками/golden/гейтами таблицы выше и разобраны ниже по коду с пометкой «проверено чтением».
Находки
H1 — node scripts/check-docs.mjs красный: скриншоты документации не пересняты (High, блокирует)
Воспроизведение (без изменения рабочего дерева — через изолированные
git worktree, затем удалены):
git worktree add --detach /tmp/head-check ee7d486924d5b1641a56f671bbf94e965fb76493
cd /tmp/head-check && node scripts/check-docs.mjs
→ ERROR screenshot source fingerprint is stale; run npm run build && node demo/docs/capture.mjs
→ exit 1
git worktree add --detach /tmp/dev-check origin/dev # afba49a3, ровно родитель ветки
cd /tmp/dev-check && node scripts/check-docs.mjs
→ Documentation checks passed (7 files, 12 external links)
→ exit 0
Отпечаток в docs/images/screenshots.json (manifest.sourceFingerprint)
считается по всему src/** (scripts/source-fingerprint.mjs). Диапазон этой
задачи меняет src/iso-projection.ts, src/iso-overlays.ts (новый),
src/iso-openings.ts, src/iso-scene-render.ts (новый),
src/houseplan-card.ts, src/styles/plan.styles.ts,
src/editor-runtime-loader.ts — отпечаток обязан был устареть, и он устарел, а
docs/images/* не тронуты ни одним коммитом диапазона
(git diff origin/dev...HEAD --stat -- docs/images — пусто).
Это ровно инцидент, названный в PROCESS.md §2.9 и AGENTS.md как прецедент:
«скриншоты не пересняли в #230 и #234, и dev стоял с красным job docs, пока
это не нашли при следующей задаче (#237)». check-docs — обязательная часть
гейта код-ревью «при любом diff'е по src/**» (PROCESS.md §2.7), не
предрелизная опция.
Почему не исправляю сам: пересъёмка выполняется только джобой
Docs screenshots (workflow_dispatch) и принимается локально npm run docs:accept -- --reviewed --from=<артефакт> — ревьюер не редактирует продукт и
не является тем, кто локально снимает кадры (байтовое расхождение
Chromium/окружения аннулировало бы весь набор). Автору нужно: запустить job →
npm run docs:accept -- --reviewed --from=<распакованный артефакт> → закоммитить
вместе с задачей.
Серьёзность: High — сливание в этом виде оставляет dev с красным job
docs, то есть ровно то, что PROCESS.md прямо называет ценой пропуска.
M1 — floor anchor opening lock badge вычисляется двумя независимыми формулами (Medium, в скоупе)
src/houseplan-card.ts:6170—_openingLockAnchor(), используется Flat/show_borders:false-путём (houseplan-card.ts:13097);src/iso-scene-render.ts:853—isoOpeningLockPlacement(), используется raised/Iso-путём.
Обе функции побайтово повторяют одну и ту же математику: rad = (angle+90)·π/180, gateFace через partitionOpeningFace/
openingInnerFaceOffsetFromIndex (в houseplan-card.ts это скрыто за приватным
хелпером _openingFace, houseplan-card.ts:8377, который делает ровно то же
ветвление), lockOffset = gridVisualUnits(16, cellCm),
offset = gateFace ? -lockOffset·side : lockOffset·(flip_v?-1:1),
floorAnchor = [rx+cos(rad)·offset, ry+sin(rad)·offset].
Показательная деталь: iso-scene-render.ts уже экспортирует
isoOpeningLockAnchor() (iso-scene-render.ts:845) — тонкую обёртку именно для
этого случая, а комментарий над _openingLockAnchor в houseplan-card.ts
буквально называет её «Shared Flat/Iso host-face semantics for raised opening
locks». Но экспорт нигде не импортируется (grep -rn isoOpeningLockAnchor src test demo находит только само определение и сборочный chunk) — общая функция
задумана, но не подключена; _openingLockAnchor — независимая копия, а не
вызов.
Это не архитектурная неизбежность лени-чанка: сама формула не использует ничего
специфичного для ленивого iso-scene-render.ts (только
gridVisualUnits/Math.cos/Math.sin/примитивы iso-openings.ts, который уже
загружается eagerly и из houseplan-card.ts, и из iso-scene-render.ts) — её
можно перенести в iso-openings.ts и импортировать из обоих мест без изменения
lazy/eager границы бандла.
Сейчас числовое расхождение отсутствует — обе копии дают одинаковый результат
для всех типов проёма, я сверил формулы построчно. Риск ровно тот, о котором
предупреждает «одно число — один источник» (PROCESS.md, #234/#233): правка
одной копии (например знака gateFace.side для нового кейса) без парного теста
незаметно разведёт Flat/show_borders:false и Iso-положение lock badge.
Парного теста нет — test/iso-scene-render.test.mjs проверяет только
модульную версию.
Фикс в скоупе: перенести формулу в iso-openings.ts (или в уже
существующий экспорт isoOpeningLockAnchor, но из eager-модуля) и заменить обе
копии на вызов; либо, как минимум, добавить parity-тест, вычисляющий обе
функции на одном наборе openings и сравнивающий floor anchor.
M2 — реальный алгоритм isoRoomSafePoint (grid-search fallback) не покрыт ни одним тестом (Medium, в скоупе)
src/iso-overlays.ts:178-222 — isoRoomSafePoint(). Функция сначала проверяет
дешёвый путь (room.safePoint, если он уже валиден), иначе строит кандидатов
(центр bbox, центроид, средние по тройкам соседних вершин контура) и в конце
перебирает выпуклую 15×15 сетку внутри bbox, выбирая точку с максимальным
clearance от границы/дыр — единственный способ получить «доказанную внутреннюю
точку» для вогнутых комнат и комнат с дырами (ссылка на §19.3 ТЗ: «иначе
используется ближайшая доказанная внутренняя точка»).
Продакшен-вызов — src/iso-scene-render.ts:723:
return { room, overlayRoom: { ...base, safePoint: isoRoomSafePoint(base) || undefined } };
base строится без поля safePoint (iso-scene-render.ts:715-722), поэтому
дешёвый путь не срабатывает НИКОГДА — каждый вызов в реальном рендере проходит
полный candidate+grid-search алгоритм.
При этом все фикстуры test/iso-overlays.test.mjs (square(...), строки
20-198) передают safePoint явно готовым ([50,50], [25,50] и т.д.) —
значит каждый юнит-тест резолвера попадает исключительно в дешёвый путь и ни
разу не исполняет сам grid-search/candidate-перебор, включая тест на «room with
hole»/donut (строка 184), где как раз и важна корректность вычисления
внутренней точки для вогнутой/дырчатой геометрии. Я перечитал файл целиком и
подтверждаю: другого вызова isoRoomSafePoint с «пустым» room нет ни в одном
тесте.
Это прямое нарушение дисциплины «тест умеет падать» именно там, где ТЗ (§19.3)
явно требует «pure tests подтверждают» этот алгоритм для использования в
качестве inward-направления nudge (AC5). Не найдено доказательств, что
алгоритм ошибается — но и доказательств обратного тоже нет: если он вернёт
точку в дыре или за пределами вогнутой комнаты, ни один тест этого не заметит,
а pointStrictlyInRoom-проверки в самой функции его подстрахуют лишь частично
(candidate просто не будет учтён consider(), но best может остаться null
для патологической геометрии, и последствия этого в резолвере nudge не
проверены).
Фикс в скоупе: добавить unit-тесты, вызывающие isoRoomSafePoint
напрямую на комнатах БЕЗ предзаданного safePoint, включая вогнутую и
donut/с дырой геометрию, и проверяющие: (а) результат строго внутри комнаты,
(б) детерминированность при повторном вызове, (в) null для вырожденной
комнаты не ломает вызывающий резолвер.
M3 — leafBasis (src/iso-openings.ts:187-209): flipH не тестируется независимо от flipV (Medium, в скоупе)
const sx = input.flipH ? -1 : 1;
const sy = input.type === 'gate' ? 1 : input.flipV ? -1 : 1;
Единственное место во всём test/iso-openings.test.mjs, где flipH: true
вообще встречается — строка 211:
const flipped = buildIsoOpeningBasis(opening({ flipH: true, flipV: true }));
всегда вместе с flipV: true, и проверка — только
assert.notDeepEqual(flipped.leaves.map(l=>l.hinge), normal.leaves.map(l=>l.hinge)),
без точных ожидаемых координат. Если в leafBasis перепутать роли (например,
sx начать читать из input.flipV, а sy — из input.flipH), для двери
(type !== 'gate') при flipH=true, flipV=true подстановка даёт sx=-1, sy=-1 что до, что после путаницы — тест остаётся зелёным. Изолированного
кейса flipH:true, flipV:false (или наоборот) с точным ожидаемым hinge нет.
Важное уточнение: заявленный в §13 ТЗ witness W9 покрывает другой код (face
turn direction в iso-scene-render.ts, я лично подтвердил
stage3-w9-gate-face-flip-reversed — красит тест test/iso-scene-render.test.mjs)
и там всё корректно. Это отдельная, более узкая находка про смежную функцию
того же семейства (visual leaf hinge/vector в iso-openings.ts), не про
отсутствие защиты AC6 в целом.
Фикс в скоупе: добавить opening({ flipH: true, flipV: false }) (и
обратный кейс) с точными ожидаемыми hinge/closedVector для door и gate.
Low — не блокируют, оставлены с записью
IsoDecorationLayers.shadowsне зависит отhideOpenings(src/iso-openings.ts:557), хотя D5/AC7 формулируют «hide_openingsскрывает panels/reveal и их directional shadows». Фактическое поведение верное — тени пропадают косвенно, потому чтоlayers.panels(который зависит отhideOpenings) обнуляет массивopeningSurfaces(src/iso-scene-render.ts:1119), из которого тени строятся; сам флагshadowsвresolveIsoDecoration— общий, не opening-специфичный. Реальной регрессии нет, но название вводит в заблуждение и не защищено собственным тестом: будущий рефакторинг, который возьмёт тени из другого источника геометрии, тихо вернёт directional shadow приhide_openings:true, и ни один существующий тест этого не поймает. Оставляю с запиской, не как блокер.renderIsoRaisedOverlays/renderIsoOverlayGroundsне проверяютlayers.structuralсами (src/iso-scene-render.ts:1032,1049) — W6 фактически ловится caller-гейтом (houseplan-card.ts:6195,!layers?.structural) и golden/contract-тестами, но defense-in-depth на уровне самого модуля отсутствует. Рекомендация на будущее, не находка этого ревью.- Дублирование вычисления fallback-ключа между
_effectiveProjection()(houseplan-card.ts:~6134-6160) и catch-веткой в теле рендера (~11360-11382) — оба независимо собираютdisp.showBorders ? key : \${space}|no-borders``. Сейчас оба пути дают одинаковый ключ (я сверил вручную), но это тот же паттерн риска, что M1, в меньшем масштабе — не поднимаю до Medium, так как последствие расхождения (неверный fallback-latch) менее вероятно и не про пользователем видимую величину напрямую. hovered: trueзахардкожен для всех raised-overlay (iso-scene-render.ts:765) — ветка D4 «tether может быть скрыт в свободной области» никогда не срабатывает в проде. Это разрешённый (не обязательный) выбор по ТЗ («может быть скрыт»), и в коде есть явный комментарий, объясняющий это как осознанное решение («persistent tether… without a second hover-only render pipeline»). Не нахожу нарушения, фиксирую как наблюдение.EPS = 1e-9в геометрических хелперахiso-overlays.ts— абсолютный, не масштабируется от величины координат плана; теоретический риск на очень больших планах, не воспроизведён.
Что проверено и корректно
- Камера (AC2, D1):
ISO_CAMERA.rotDegровно4,tiltDegровно20, единая матрицаisoPlaneMatrix/applyIsoMatrixдля точки/floor-matrix/CSS — никакой второй приближённой формулы (src/iso-projection.ts). Round-trip, diag-frame и raised-height тесты вtest/iso-projection.test.mjsсодержательны (точные числа, не только типы). W1 лично прогнан. - Raised/floor split (AC3, D2): одна матрица для floor anchor, raised
anchor, plate-углов (
src/iso-overlays.ts), никакой параллельной формулы. Vacuum/Glow/room fill/sunlight/decor остаются floor-bound — подтверждено W3 и отдельно кодом_scenePoint/this._posпутей вhouseplan-card.ts. - Owning room и nudge (AC5, D4): правило владения (явная привязка → строго
содержащая минимальная по площади комната → tie-break по id → fail-safe
null, без «угадывания») реализовано и покрыто тестами free/shared-wall/ corner/room-with-hole/outside/no-owner (не считая пробела M2 в подчинённомisoRoomSafePoint). Nudge детерминирован (бинарный поиск по CSS-px, безMath.random), ограничен фиксированным cap, не двигает floor anchor/tether, не пишет данные — grep не находит присваиваний вinput.*/store/save/dispatch. W5, W11 лично прогнаны. - Плейт и content (AC4): подложка floor-parallel, glyph/text screen-facing,
hit target реально не уменьшается (только новые правила
min-width/height: 44pxдобавлены вplan.styles.ts:609-619,752-757, старые не тронуты). W4 лично прогнан. - Проёмы (AC6, D5): ручек нет (grep по
handle|knob|leverне находит ничего в геометрии, только в редакторских resize-инструментах); passage явно исключён из leaf/frame/sill (iso-openings.ts:252,267,414); окно — светлая рама/sill, без тёмного стекла (цвета вplan.styles.tsподтверждены); updateopeningAmount()не трогает structural cache/scene (basis неизменен после проекции,test/iso-openings.test.mjs:214-221assert.deepEqual(normal, basisSnapshot)); hinge/face turn direction (W9) лично прогнан и корректен. Ограничение — M3 выше. - Materials/shadows/fingerprint (AC8, AC12, D6/D7, §7.2/§7.3): layer order
underlay → shadows → walls → grounds → raisedсовпадает с §7.3 и подтверждён z-index тестом (test/isometric-contract.test.mjs:112-124). Structural fingerprint включает ровно нормативный список (geometry, camera+4°/20°, heights, algorithm revision) и явно исключает HA state/theme/ hover/tooltip — подтверждено кодом и W7. LRU cap 8 подтверждён кодом и тестом на eviction/recency. Material defs — фиксированный набор ~9 id, не растущий по face/marker — подтверждено кодом и W8, а также реальным прогоном бенчмарка (materialDefinitionCount: 9, см. «Производительность» ниже). Grounding/plate SVG-корни помеченыaria-hidden="true" pointer-events="none". - Degradation (AC9, §7.5): forced-colors/no-filter снимают только
texture/shadows, geometry/openings/anchors/tether/actions остаются; только
structural exception латчит Flat fallback (W10 подтверждает, что Flat-сэмпл
не засчитывается успешным Iso-сэмплом в перф-раннере) — проверено чтением
resolveIsoDecoration/resolveIsoDecorationLayersи smoke-тестами автора. - Zigbee-топология и live layers (AC10):
hp-zigbee-topology-overlay.tsне менялся и по-прежнему читаетmarker.getBoundingClientRect()— так как_renderDevice/_renderRoomLabel/_renderOpeningLocksтеперь пишутleft/topизisoPlacement?.visualScene(поднятая точка), топология автоматически идёт к новой позиции без второй геометрической модели — подтверждено чтением и новым тестовым блоком вdemo/smoke_zigbee_topology_hover.mjs(+78 строк:isoTopologyUsesRaisedDomCentres,isoTopologyTracksRaisedDomCentresAfterPanZoom). show_borders:false(AC7, §7.4):isoOverlayPlane()возвращает'floor'при!showBorders;_isoOverlayScene()дополнительно гейтится наlayers?.structural; при отсутствии isoPlacement device/room-label/lock откатываются на каноническую матрицу сz=0(не на старую two-corner viewBox-аппроксимацию) — подтверждено кодом и smoke-тестомnoBordersUsesTrueAffineFloor, проверяющим реальные недиагональные элементы матрицы. Замечена и попутная корректная правка бага:_baseVb()для no-borders раньше передавалwallHeightстены вprojectedFrame, теперь —0(убирает невидимые Stage 3 bounds, ровно требование §7.4).hide_openingsvs lock badge: lock badge не зависит отhideOpenings(только от!orphanReason && (door|gate) && lock && entityAvailable) — соответствует «lock badge остаётся raised, пока стена видима».- Одно число — один источник (кроме M1): value/LQI-текст на raised-плейте
использует тот же
_devicePresentation, что и обычный HTML-рендер (общий форматтер, не пересчитан заново);test/single-source-numbers.test.mjsне тронут диффом, что ожидаемо — задача не добавляет новый видимый формат числа, только позицию (см. M1). - Негативный контракт (AC13): нет новых зависимостей
(
package-lock.jsonне менялся), новых i18n/storage/config/network путей — подтверждено grep и чтением; legacyhp-labs/expiry не возвращены (W12). Трейлеры всех 4 коммитов диапазона —Issue: #160/User-Visible: no, свереноgit log. - Golden (AC15), диагностика на этой Linux-машине:
npm run golden:verifyпосле свежегоbundle:sync— 137 незатронутых сценариевpassed(Flat не сдвинулся ни на пиксель), 8 существующихisometric-*—different(ожидаемо, камера/Stage 3), 5 новыхisometric-stage3-*—missing-baseline(ожидаемо, не принимались автором — правильно, приёмка требует--reviewedи полного Linux CI артефакта, не решается на этом этапе). Ни одного неожиданного отличия внеisometric-*. - Документация (AC15, §15):
docs/ISOMETRIC.md,docs/ARCHITECTURE.md, новыйdocs/adr/160-isometric-stage3-overlays.md,docs/STATUS.md,docs/specs/README.mdобновлены и не содержат устаревших упоминаний «expiring iso»; публичныеdocs/CHANGELOG.md,docs/CHANGELOG.ru.md,docs/RELEASE-NOTES.md, README, user guide не тронуты — соответствует «Stage 3 остаётся скрытой». - Бюджеты производительности (AC12, §10):
demo/performance/budgets-isometric-stage3-dense.jsonне мягчеdemo/performance/budgets-large-house-isometric.json(идентичные regression ratio/noise/ceiling, идентичныйisoGeometry: 8/growth 0) — сверено построчно.demo/performance/evaluate.mjsотклоняетeffectiveProjection !== 'iso'и полный boolean rebuild на HA-only update..github/workflows/performance.ymlдобавляет отдельный matrix-профиль для нового бюджета.
Производительность — локальный sanity-прогон
node demo/benchmark_large_house.mjs --profile=isometric-stage3-dense-v1 --samples=1
запущен лично на этом SHA (Linux) как разовая проверка, что новый профиль и
плотная фикстура реально исполняются и дают осмысленные числа — не как замена
канонического 7-sample exact-SHA Linux CI прогона (тот остаётся pre-beta
гейтом §8/§10, вне объёма код-ревью). Результат зелёный и содержательный:
effectiveProjection: "iso"— сэмпл не попал в Flat fallback;renderedOverlayCount: 82иraisedOverlayCount: 82— все overlay реально подняты, ни один не остался «недоподнятым»;raisedVacuumCount: 0— vacuum не поднят, как того требует AC3/W3;materialDefinitionCount: 9(patterns: 3, filters: 4+ фон) — совпадает с оценкой из чтения кода (см. AC8 выше), O(1) для плотной сцены с 82 overlay и 261 opening-поверхностью (door: 182, window: 55, gate: 24);isoStructuralBuilds.haUpdateDelta: 0иstage3Diagnostics.steadyUpdateStructuralBuildDelta: 0— HA-only update не вызывает structural rebuild;cacheGrowth.isoGeometry: 0приcacheEntries.isoGeometry: 3(в пределах cap 8).
Одного сэмпла недостаточно для приёмки exact-SHA бюджета (нужны семь и Linux CI) — это прямо и не заявлено; но сам факт, что профиль и фикстура работают и дают именно те инварианты, которые требует AC12, — установленный факт, а не «verified» без команды.
Чего не проверял и почему
npm run invariants— диапазон не меняет геометрическую модель (комнаты/стены/толщины/layout/marker.space/open_spansвcustom_components/**/config schema); это исключительно рендер-слой поверх уже провалидированной канонической геометрии (подтверждено AC13 и diff-статом — ни одного изменения вcustom_components/**/*.pyили схеме конфига). Инварианты неприменимы к чисто презентационному diff'у.python -m pytest tests_backend— ноль изменений вcustom_components/houseplan/**/*.py(diff stat подтверждает).- Полный
ls demo/smoke_*.mjsнабор (226 файлов) — не прогонял. Ранscripts/smoke-select.mjs --base origin/dev --head HEADпоказал 104 «прямых совпадения» и 35 «слабых связей» почти исключительно из-за общих приватных полейhouseplan-card.ts(_baseVb,_viewOr,_modeи т.п.), не специфичных для Stage 3 логики — полный прогон здесь был бы предрелизным объёмом, не гейтом ревью. Прогнал лично:demo/smoke_isometric_contract.mjsиdemo/smoke_isometric_live_touch.mjs— по многу раз каждый, как чистый baseline-прогон перед каждой из 6 smoke-guarded witness-мутаций W4–W8 (все чистые прогоны зелёные, все мутации красные), иdemo/smoke_entry_stale.mjs(«зарегистрированная связь» через общийEditorRuntimeLoader/safeRuntimeDiagnostic— зелёный). Не прогонял отдельно:demo/smoke_zigbee_topology_hover.mjsи 11 «legacy iso»-смоков, заявленных автором зелёными на точном SHA — полагаюсь на их хендофф-отчёт и на то, что пересекающийся по контрактуsmoke_isometric_contract.mjs/smoke_isometric_live_touch.mjsя исполнил сам многократно и чисто; при необходимости могу прогнать по запросу. - Полный 7-sample exact-SHA
large-house-isometric-v1иisometric-stage3-dense-v1— канонический гейт вынесен в Linux CI Full Performance перед бетой (§8/§10), не в код-ревью; локально прогнан только--samples=1sanity-чек нового профиля (см. «Производительность» выше). - Приёмка golden baseline — не моя роль на этом этапе; выполнил только
диагностический
golden:verify(см. выше), неgolden:accept.
Итог
High: 1 (check-docs красный — блокирует), Medium: 3, все в скоупе
задачи (правятся в этой же ветке, отдельный issue не заводится). Все 15 AC по
существу выполнены и по большей части доказаны автотестами, которые я лично
проверил на способность падать (12/12 red witnesses), плюс независимый
canonical-Linux golden:verify и содержательный sanity-прогон нового
performance-профиля. Задача возвращается автору не из-за архитектурных
проблем, а из-за одного пропущенного, механического, но обязательного шага
(пересъёмка скриншотов документации) и трёх фиксируемых в скоупе усилений
тестового покрытия/устранения дублирования формулы.
Вердикт: красный · заход r1 · блокирующих циклов 0/4 · High: 1 · Medium: 3 → в задаче
Материал раунда
- Ветка:
issue/160-isometric-stage3, коммитee7d486924d5— ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет. - Дерево материала:
41472b7a410b83d6fa8a667df63d680b819d01f6git log --all --format='%H %T' | grep 41472b7a410b - ТЗ
docs/specs/160-isometric-stage3.md, блоб20ed447b8f58bdfe9e48695584ff616c36b79b4dgit log --all --find-object=20ed447b8f58bdfe9e48695584ff616c36b79b4d -- docs/specs/160-isometric-stage3.md