20 KiB
CODE-REVIEW-330-r3
Issue: #330 — «Ограничения стыков (#329) блокируют event loop HA и дёргают ресайз на больших планах»
Этап: code (PROCESS.md §2.7)
Заход: r3 · блокирующих циклов израсходовано 2 из 4 (до этого раунда)
Ветка: issue/330-junction-limits-performance, HEAD ревью — 04bb54ae
0. Разбор по дельте — почему и от какого SHA
Предыдущий вердикт (code-review r2, 2026-08-28T00:30:25Z) назван на HEAD
85636a65. Проверено, что этот SHA — прямой предок текущего HEAD и что
между ними ребейза не было:
$ git merge-base --is-ancestor 85636a65 HEAD && echo ancestor
ancestor
$ git log --oneline 85636a65..HEAD
04bb54ae fix: the baseline cache keys on geometry, and the smoke tells all three worlds apart (#330 r2-M1)
294d7047 docs: review document for #330
Два коммита, оба ожидаемые: 294d7047 — публикация документа r2 (не
код), 04bb54ae — правка автора на единственную находку r2 (M1). Дельта
локальна: один файл продуктового кода, один смок, один юнит-тест, один
мутант-гвард. Контракт поведения не менялся, новая подсистема не задета,
объём дельты много меньше исходной задачи. Условия §2.10/§7.2 для полного
разбора не выполнены — разбираю по дельте git diff 85636a65..HEAD.
1. Скоуп дельты
git diff 85636a65..HEAD --stat:
.../houseplan/frontend/houseplan-card.js | 4 +-
demo/smoke_junction_limits.mjs | 56 ++++-
dist/houseplan-card.js | 4 +-
docs/reviews/CODE-REVIEW-330-r2.md | 257 +++++++++++++
scripts/mutation-gate.mjs | 9 +-
src/houseplan-card.ts | 25 +-
test/junction-limits.test.mjs | 6 +-
7 files changed, 336 insertions(+), 25 deletions(-)
Только r2-M1 (АС4 «поведенческая часть кэша не различает рабочий кэш от удалённого»):
src/houseplan-card.ts:7501-7553(_junctionLimitsIntroduced) — ключ кэша_junctionBaselineCacheзаменён с(epoch, spaceId)на(spaceId, fingerprint), гдеfingerprint = spacePhysicalGeometryFingerprint(previousSpace)— существующая, уже применяемая в этом же файле утилита (строки 7212, 7569-7570, 7676, 7751-7752, 7800, 16031), а не новый механизм.demo/smoke_junction_limits.mjs— счётчик теперь считает ТОЛЬКО вызовы_junctionLimitViolationsс документом=== card._serverCfg(вычисления baseline, не кандидата), плюс второй жест ресайза с коммитом между ними, чтобы отличить «кэш никогда не инвалидируется» от «кэш инвалидируется честно».test/junction-limits.test.mjs— regex источника обновлён под новое имя полей (cached.fingerprint === fingerprint,spacePhysicalGeometryFingerprint(previousSpace)).scripts/mutation-gate.mjs— guard мутантаjunction-limit-baseline-cache-staleтеперь запускает и смок (поведенческая часть), и юнит (исходник), было — только юнит.dist/**/frontend-копия — механический пересчёт бандла тем же коммитом.
2. Закрытие раунда r2
| Находка r2 | Чем закрыта | Где это видно |
|---|---|---|
M1 (в скоупе) — заявленная «поведенческая половина» AC4 не различала рабочий и удалённый кэш (11 против 12 вызовов тонули в шуме); реальный баг вскрылся тем же разбором: кэш ключевался на _cfgEpoch, который тикает на каждом принятом превью-оверлее без изменения хранимого документа, поэтому baseline пересчитывался ~4 раза за жест, а не 1 |
Ключ кэша заменён на identity документа + spacePhysicalGeometryFingerprint его пространства — содержание, не счётчик; превью-оверлей fingerprint не трогает, in-place structural commit меняет геометрию и инвалидирует честно. Смок считает отдельно вычисления baseline (документ ===_serverCfg) через два жеста с коммитом между ними: ожидание строго 1, затем строго 2 |
src/houseplan-card.ts:7518-7553; demo/smoke_junction_limits.mjs:186-254; я применил ровно мутант junction-limit-baseline-cache-stale (условие кэша обратно на cached.spaceId === spaceId без fingerprint) и прогнал сам смок — resizeBaselineRecomputedAfterCommit: expected true, got false (см. §3); без мутанта смок и mutation-gate.mjs --id=junction-limit-baseline-cache-stale — оба зелёные (см. §3) |
Находка закрыта по существу и подтверждена исполнением, не только чтением.
3. Как проверялось (дельта, r3)
Зелёного Validate на 04bb54ae не найдено — гоняю дешёвые гейты сам, плюс
check-docs (диф трогает src/**) и целевой смок/мутант по дельте.
| Гейт | Команда | Результат |
|---|---|---|
| Типы | npx tsc --noEmit |
чисто, без вывода |
| Юниты | npm test |
# tests 1420 / pass 1419 / fail 0 / skipped 1 |
| Сборка + идентичность 3 копий бандла | npm run build && npm run bundle:sync, затем cmp попарно всех трёх файлов |
все три cmp — молча, идентичны; git status --short после сборки чист |
Докс-гейт (диф трогает src/**) |
node scripts/check-docs.mjs |
exit 1 — ERROR screenshot source fingerprint is stale; run npm run build && node demo/docs/capture.mjs. Проверил напрямую: записанный sourceFingerprint в docs/images/screenshots.json — 2a5ae6c1…, реальный visualFingerprint() на этом дереве — dbac4a84…. Последний коммит, трогавший screenshots.json, — 85636a65 (ДО фикса); 04bb54ae поменял src/houseplan-card.ts и не пересчитал фингерпринт — см. находку H1 |
| Целевой смок AC4 | node demo/smoke_junction_limits.mjs |
OK (после свежей сборки и bundle:sync) |
| Тест умеет падать (AC4/мутант) | вручную заменил условие кэша в src/houseplan-card.ts на мутант junction-limit-baseline-cache-stale (if (cached && cached.spaceId === spaceId), без fingerprint), пересобрал бандл, прогнал смок |
FAILED (1): resizeBaselineRecomputedAfterCommit: expected true, got false — мутант ловится; источник восстановлен, пересобран, смок снова OK, git status --short пуст |
| Тот же мутант через гвард | node scripts/mutation-gate.mjs --id=junction-limit-baseline-cache-stale |
ok junction-limit-baseline-cache-stale: тест покраснел, как обязан; «поймано 1 из 1» |
| Выборка смоков по дельте | node scripts/smoke-select.mjs --base 85636a65 --head HEAD |
1 файл src/** изменён, 3 символа на изменённых строках. Одна зарегистрированная связь: demo/smoke_wall_union_isolation.mjs ← spacePhysicalGeometryFingerprint. Прогнан отдельно (см. ниже) |
smoke_wall_union_isolation.mjs (по связи из smoke-select) |
node demo/smoke_wall_union_isolation.mjs |
OK, все 11 полей true |
| Перф-контракт §5/AC7 (не трогается дельтой, но это перф-задача — перепроверил) | npm run benchmark:junction-limits |
pass: true, tsFullCandidateMs: 152 (бюджет 400, запас 2.6×) |
Отдельная проверка: не добавила ли дельта скрытую перф-регрессию
_junctionLimitsIntroduced теперь считает spacePhysicalGeometryFingerprint
на КАЖДЫЙ вызов (и при попадании в кэш, и при промахе) — раньше сравнение
было целочисленным O(1). Измерил стоимость самой функции на фикстуре §5
(576 атомов, 12×12) отдельно от бенчей (ни benchmark_junction_limits.mjs,
ни benchmark_safe_resize.mjs не проходят через этот метод класса — оба
вызывают чистые функции/resize.js напрямую, это отмечено ещё в r1/r2):
$ node --input-type=module -e '... spacePhysicalGeometryFingerprint(space) × 200 ...'
avg ms per call: 1.71
~1.7 мс на вызов на самой крупной фикстуре задачи — на два порядка меньше бюджета кадра и на два порядка меньше стоимости, которую §4.4 устраняет (до фикса пересчёт baseline стоил сотни мс — 4.2 с на полный набор до §4.7). Не блокирует, отмечаю как проверенное наблюдение: этот путь остаётся не покрыт числовым перф-бюджетом ни в одном бенче — не находка этой задачи (оба бенча так же не покрывали его в r1/r2), но стоит иметь в виду, если §330 продолжится следующим срезом.
4. Находки
H1 — докс-гейт красный на HEAD ревью (в скоупе, блокирует)
node scripts/check-docs.mjs завершается exit 1:
ERROR screenshot source fingerprint is stale; run npm run build && node demo/docs/capture.mjs
Коммит 04bb54ae меняет src/houseplan-card.ts (комментарий + логика
кэша), но не пересчитывает docs/images/screenshots.json. Отпечаток
считается по всему src/** (PROCESS.md §8), поэтому любая правка
фронтенда делает его устаревшим механически, без вариантов трактовки —
ровно класс дефекта, который уже стоил dev красного джоба docs на
#230/#234 (найдено только на следующей задаче, #237). Сама задача #330
уже дважды проходила через этот гейт (r1-H1 в code-review, тот же класс)
и знает цену пропуска. UI не изменился (изменение сугубо внутреннее,
кэш-логика), поэтому PNG, вероятно, останутся байт-в-байт те же — но
запись sourceFingerprint обязана быть пересчитана командой npm run build && node demo/docs/capture.mjs и принята npm run docs:accept -- --reviewed --from=<артефакт> тем же правилом, что и раньше в этой
задаче; самому пересобирать и коммитить в рабочей копии не в праве
(ревьюер не правит код).
Что нужно поправить: пересчитать фингерпринт/скриншоты и закоммитить
вместе (или отдельным коммитом класса C поверх), убедиться, что
check-docs зелёный на итоговом SHA.
Находок Medium/Low, не закрытых в тексте, нет.
5. Унаследовано из r2 (без повторной проверки)
- Архитектура срезов §4.1–§4.7 (executor-цепочка, rev-кэш
previous, byNode-индекс П3, bucket-решётка П4, §4.6 «v9 как есть», общий проход §4.7) — признана верной по коду и по вердиктам в code-review r2 (docs/reviews/CODE-REVIEW-330-r2.md, HEAD85636a65, разделы 3 и 7). Дельта r3 их не касается ни одной строкой (см. §1) — принимаю без повторного чтения. - AC1 (event loop/executor) — HA-харнесс тест
test_330_config_set_validators_run_in_the_executor, прогнан исполнением в r2 (pytest -k 330→1 passed, документ r2 §5/§6). Дельта r3 не трогаетcustom_components/**. - AC2/AC3/AC5 (линейный П3/П4, rev-кэш бэкенда, эквивалентность §4.6) —
подтверждены исполнением в r2 (паритет-тесты TS↔Python, границы ячеек,
pytest tests_backend -q423 passed). Ни один из файлов бэкенда не в дельте r3. - AC7/перф-контракт §5, включение в
validate.yml— подтверждено в r2 чтением воркфлоу (validate.yml:543-547) и исполнением (pass:true, запас 2.65×). Я перепрогналbenchmark:junction-limitsзаново в этом раунде (см. §3) — тот же результат в пределах шума раннера (152 мс против 150.7 мс в r2), воркфлоу-файл не в дельте, не перечитывал. - golden:verify — не гонял, как и в r2: дельта не трогает рендер, геометрию, стили, слои, только ключ кэша и подсчёт вызовов в смоке.
- Полная матрица
demo/smoke_*.mjs(194 шт.) и полныйmutation-gate.mjs— не гонял целиком, как и в r2: это предрелизные гейты (PROCESS.md §8), не гейт ревью. По дельте прогнал точечно —smoke_junction_limits.mjs(прямое совпадение) иsmoke_wall_union_isolation.mjs(зарегистрированная связь поspacePhysicalGeometryFingerprint, см. §3) — плюс единственный релевантный дельте мутант через--id=. pytest tests_backend— не гонял; дельта не содержит ни одного файла.py(см.git diff --name-only 85636a65..HEAD).npm run invariants -- --config <...>отдельной командой — не гонял; дельта не вводит новую геометрию/фикстуру, только ключ кэша по уже существующемуspacePhysicalGeometryFingerprint;npm testпрогоняет инварианты модели на существующих моделях проекта и прошёл (см. §3).- Одно число — один источник — неприменимо: дельта не вводит и не меняет ни одной пользователю видимой величины (внутренний счётчик вызовов в смоке и внутренний кэш-ключ невидимы пользователю).
- CHANGELOG — не требуется: коммит
04bb54aeнесётUser-Visible: no(внутренняя правка кэша, поведение для пользователя не меняется); трейлеры коммита корректны (Issue: #330,User-Visible: no).
6. Что проверено и корректно (эта дельта)
- Новый ключ кэша (
spaceId + fingerprint) читается изpreviousConfig, вычисляется безопасно черезtry/catchс пустой строкой на ошибке; пустой fingerprint явно исключён из условия попадания в кэш И из записи в кэш (&& fingerprint) — сбой вычисления fingerprint отключает кэширование для этого вызова, а не выдаёт ложное попадание. Прочитано построчно (src/houseplan-card.ts:7526-7553). spacePhysicalGeometryFingerprint— не новый механизм, а уже используемый в этом же файле барьер физической геометрии (6 других мест), корректность и покрытие которого не в скоупе этой находки.- Смок теперь физически различает три мира (рабочий кэш = 1 затем 2, вечный кэш = 1 затем 1, отключённый кэш = ~10 затем ~20) — проверено и мутантом (см. §3), и обычным прогоном.
- Трейлеры коммита
04bb54ae—Issue: #330,User-Visible: no, корректны для внутренней правки.
7. Чего не проверял и почему
pytest tests_backend— дельта не трогает.py, наследую r2 (§5).golden:verify— дельта не трогает визуал, наследую r2 (§5).- Полная матрица смоков (194) и полный
mutation-gate.mjs(все мутанты) — предрелизные гейты, не гейт ревью; прогнал точечно по выводуsmoke-select.mjsплюс единственный релевантный мутант. - Перф-бюджет самой
spacePhysicalGeometryFingerprintвнутри_junctionLimitsIntroduced— не заведён отдельным числом ни в одном бенче (см. §3, «отдельная проверка»); измерил вручную (1.7 мс/вызов на 576-атомной фикстуре) вместо гона несуществующего гейта — не блокирует, записано как наблюдение. - Ручное тестирование в браузере — вне цикла ревью по регламенту; браузерный смок прогнан, это и есть замена.
8. Итог
High: 1 (докс-гейт красный на этом SHA — механический пропуск
check-docs после правки src/**, в скоупе, чинится в этой же задаче).
Medium: 0. Единственная находка r2 (M1) закрыта по существу и
подтверждена мутантом лично. Архитектурная часть решения (§4.1–§4.7)
унаследована из r2 без повторной проверки — дельта её не касается.