# SPEC-REVIEW-520-r2 ## Якоря - Issue: [#520](https://github.com/Matysh/houseplan-card/issues/520) «Первый устойчивый кадр большого дома вырос до 3.36 с при потолке 3.0 с — регрессия после #509/#500», метка `S4-spec-review`, состояние `OPEN` (подтверждено `gh issue view 520 --json labels,state`, 2026-09-10). - ТЗ: тело issue #520, раздел `## ТЗ`, продолжает жить в теле issue (#517: архив `docs/specs/` закрыт для новых ТЗ) — то же место, что и в r1. - sha256 текущего тела issue (`gh issue view 520 --repo Matysh/houseplan-card --json body -q .body | sha256sum`): `7f68e4cd99242f44e13e9929cd0a7aa9dc3cb6ae5f871878670bade18a2436ac`. Отличается от хеша r1 (`5477d052e0e87182f51a9b3b05d37fce0a7ada3d9dc66c10f92 7f64564ccc6e4`) — тело менялось, что и ожидается после возврата на правки. - Заход r2. Бюджет циклов §4 до этого ревью: **1/4** (израсходован r1, жёлтый вердикт). Вердикт ниже — снова жёлтый, поэтому расходует цикл: после этого ревью бюджет **2/4**. - Материал кода не менялся: `git log --oneline -5` на момент этого ревью — тот же диапазон, что видел r1 (`ad2f9afd` — публикация документа r1, `27ea23c1`/`3d439805`/`37ef970d` — диагностика бенчмарка, `test(perf):`, без единой строки в `src/**`). Изменилось только тело issue. ## Скоуп ревью Повторный заход ревью ТЗ (PROCESS.md §2.4, §2.10) после жёлтого вердикта r1. Предмет — **дельта тела issue между r1 и r2**: три правки, заявленные автором в комментарии `2026-09-10T15:23:01Z` («M1 — раздел «Риски»», «M2 — AC3», «M3 — изометрия»), плюс всё, до чего эта дельта дотягивается (AC4, симметрия профилей). Остальной текст ТЗ (продуктовая рамка, AC1/AC2/AC5, откат, release-артефакты, «принято предположительно») дельту не задевает — наследуется из r1 без повторной проверки (см. раздел «Унаследовано»). Дельта локальна: правка текста ТЗ, ни ребейза, ни смены контракта, ни новой подсистемы — полный разбор всей задачи заново не требуется. ## Как проверялось 1. Получено текущее тело issue #520 (`gh issue view 520 --json body`) и три новых комментария после r1 (фикс-комментарий автора, метка `S4-spec-review` возвращена). 2. Прочитан `docs/reviews/SPEC-REVIEW-520-r1.md` целиком — вердикт, три находки Medium, материал (SHA `27ea23c1ec25`, дерево `f80bcdd58b72`, тело `5477d052e0e8…`). 3. **Каждая находка r1 сверена построчно с текущим текстом**, а не принята на слово автора: - M1 (риски) — раздел `### Риски` в теле issue теперь существует, 4 пункта. Проверил сам утверждение «потребителей `elementProperties` в дереве нет»: `grep -rn "elementProperties" src test scripts demo --exclude-dir=srv` — 0 совпадений, подтверждает пункт 1 раздела. - M2 (AC3) — строка AC3 в таблице теперь помечена `(не гейт, запись)`, колонка «чем краснеет» заменена на «— · критерий поглощён AC1+AC2+AC4»; AC3 больше не выдаёт себя за независимо доказуемый гейт. - M3 (изометрия) — AC4 расширен до «`modelReadyMs` обоих профилей — взаимодействия и изометрии — внутри относительного лимита»; в «Контракте» добавлено «изометрия чинится тем же изменением и проверяется тем же способом (AC4)». 4. **Симметрию M3 перепроверил количественно, а не по формулировке.** Пересчитал по формуле `demo/performance/evaluate.mjs:73-118` (`effective = min(hardMaxMs, max(base*(1+ratio), base+allowance))`) не только `modelReadyMs` (как это сделал r1), но и второе число из того же предложения «Симптома» — `firstStableRenderMs` профиля изометрии. Прочитаны бюджеты `demo/performance/budgets-large-house-isometric.json:6-7`: `firstStableRenderMs`: `maxRegressionRatio: 0.2`, `noiseAllowanceMs: 250`, `hardMaxMs: 3500`. См. находку Medium-1 ниже — результат разошёлся с тем, что покрывает амендированный AC4. 5. Проверено, что потолок `cache.entries.cleanFloor` в `budgets-large-house-isometric.json` и `budgets-isometric-smoke.json` уже равен 100 (не поднимался в `914e8402`, который трогал только два файла interaction-профиля) — значит для AC5 симметрии с изометрией не требуется, вопрос из хвоста M3 («и его budgets-файлов, если они регрессировали похожим образом по cleanFloor») закрыт отрицательным результатом: там регресса нет. 6. `git log --oneline -5` и `git status` — рабочая копия на `ad2f9afd`, той же, что видел r1; продуктовый код (`src/**`) не менялся между раундами, гейты кода этому раунду не относятся (см. «Чего не проверял»). ## Находки ### Medium-1 — амендированный AC4 не покрывает второе число из того же предложения «Симптома»: `firstStableRenderMs` изометрии **Файл:** тело issue #520, AC4 (строка 59 полученного тела) против «Симптома» (строка 3) и «Контракта» (строка 46). «Симптом» приводит для профиля изометрии **два** числа одним предложением: «Профиль изометрии — то же: `modelReadyMs` 2165.6 → 2626.3 при относительном потолке 2598.72, `firstStableRenderMs` 2227 → 2678.2». Правка M3 (закрытие находки r1) добавила в AC4 только первое из них — `modelReadyMs` «обоих профилей». Второе число того же предложения контракт не упоминает вовсе. Пересчитано по той же формуле, которую сам автор и r1 применили к `modelReadyMs` (`demo/performance/evaluate.mjs:107-118`, `budgets-large-house-isometric.json:7`: `maxRegressionRatio: 0.2`, `noiseAllowanceMs: 250`, `hardMaxMs: 3500`): ``` relativeLimit = max(2227 * 1.2, 2227 + 250) = max(2672.4, 2477) = 2672.4 effective = min(hardMaxMs=3500, relativeLimit=2672.4) = 2672.4 ``` Кандидат **2678.2 > 2672.4** — превышение на 5.8 мс (≈0.22 %). Тот же класс «мягкого» (не абсолютного) нарушения, что и `modelReadyMs` изометрии (2626.3 против 2598.72, превышение ≈1.05 %), который M3 признал достаточным основанием для правки AC4. По одинаковому стандарту оба числа одного предложения должны попасть в контракт одинаково — либо оба, либо ни одного с явным объяснением почему. Это не формальность: относительные пределы (в отличие от абсолютного `firstStableRenderMs ≤ 3000` профиля interaction) не гейтуют обычный `Validate`, они видны только при ручном чтении «полного сравнения» — единственный документ, который скажет будущему код-ревьюеру, что именно сверять в этом логе, — это текст AC4. Формулировка «Контракта» («изометрия … проверяется тем же способом (AC4)», строка 46) сейчас **утверждает больше**, чем буквально требует таблица AC4: в контракте — «тем же способом» для изометрии в целом, а в AC4 — только одна из двух её метрик. **Чем чинится:** одной правкой AC4 — добавить `firstStableRenderMs` изометрии в тот же относительный чек, либо явно исключить его одной фразой (например, «в пределах шума 250 мс, отдельно не гейтуем») там же, где ТЗ уже объясняет статус AC3. Не блокирует контракт — правка одна и дешёвая. ## Закрытие раунда r1 | Находка r1 | Чем закрыта | Где это видно | |---|---|---| | Medium-1 — отсутствует раздел «Риски» (§7.1) | Добавлен раздел `### Риски`, 4 пункта; названный ревьюером риск (`elementProperties`) снят с указанием способа проверки | Тело issue, раздел `### Риски`; проверено `grep -rn "elementProperties" src test scripts demo --exclude-dir=srv` → 0 совпадений | | Medium-2 — AC3 не имеет дешёвого способа доказательства | AC3 явно понижен до записи: помечен `(не гейт, запись)`, «чем краснеет» заменено на «поглощён AC1+AC2+AC4» — больше не выдаёт себя за независимый гейт | Тело issue, строка AC3 таблицы AC | | Medium-3 — регрессия изометрии не отражена в контракте/AC | AC4 расширен на `modelReadyMs` обоих профилей; в «Контракте» явно сказано, что изометрия чинится тем же изменением | Тело issue, AC4 (строка 59) и «Контракт» (строка 46). **Частично** — см. новую находку Medium-1 этого раунда: второе число того же предложения «Симптома» (`firstStableRenderMs` изометрии) осталось непокрытым | ## Унаследовано из r1 Приняты без повторной проверки — дельта их не задевает: - **Причина регрессии измерена, не догадка**: три технических утверждения о поведении `wrapped`-свойств Lit (`createProperty`, форсированная запись в `changedProperties` на первом апдейте, работа `requestUpdate` с необъявленным именем) — r1 сверил дословно с исходником `@lit/reactive-element` (`docs/reviews/SPEC-REVIEW-520-r1.md`, раздел «Как проверялось» п.4, материал SHA `27ea23c1ec25`, дерево `f80bcdd58b72`). - **AC1** (source-scan тест по образцу существующего `config-adoption-ownership.test.mjs`) и **AC2** (юнит на вызов `onBodyReplaced`) — доказуемы дёшево и детерминированно, текст этих строк между r1 и r2 не менялся. - **AC5** (`cache.entries.cleanFloor` ≤ 100 на двух interaction-файлах бюджетов) — привязан к точным файлам, числа сверены построчно; не менялся. - **Откат** (revert двух строк `static properties` + потолка 180) и **release-артефакты** (`User-Visible: no`, разблокирует `v1.74.0-beta.1`) — просты, однозначны, не менялись. - **Продуктовая рамка** (администратор дома, поверхность — первый показ большого дома) совпадает с `docs/SCOPE.md` J1 и фикстурой 60 комнат/200 устройств — r1 сверил, текст не менялся. - **Выбор полного трека** — верен, не оспаривается. - **Ни одного вопроса владельцу не требуется** — весь материал технический, подтверждено и в r1, и в этом раунде. Документ-источник: `docs/reviews/SPEC-REVIEW-520-r1.md`, материал — ветка `issue/520-first-frame` на `27ea23c1ec25`, дерево `f80bcdd58b72`. ## Что проверено и корректно (этот раунд) - Все три пункта фикс-комментария автора (`2026-09-10T15:23:01Z`) подтверждены чтением текущего тела issue, а не приняты по заявлению — M1 и M2 закрыты полностью, M3 закрыт частично (см. Medium-1 выше). - Потолки `cache.entries.cleanFloor` в бюджетах изометрии (`budgets-large- house-isometric.json`, `budgets-isometric-smoke.json`) не поднимались в `914e8402` и уже равны 100 — симметричная правка AC5 для изометрии не требуется, это установлено чтением файлов, а не предположением. - Код между раундами не менялся (`git log`, `git status` на `ad2f9afd`) — дешёвые гейты (`tsc`, `test`, `build`) этому раунду не относятся по той же причине, что и в r1: предмет этапа — текст ТЗ, продуктовой правки в дереве ещё нет. ## Чего не проверял - **Гейты кода** (`tsc --noEmit`, `npm test`, `npm run build`, `check-docs.mjs`) — не гонял: `src/**` не менялся ни в r1, ни между r1 и r2, весь диапазон коммитов — диагностика бенчмарка (`test(perf):`). - **`npm run invariants`** — не гонял: геометрия и её ссылки не тронуты. - **Реальный прогон `Full Performance`** — не запускал; числа Симптома (включая пересчитанный `firstStableRenderMs` изометрии) взяты из текста issue и перепроверены только арифметически по опубликованной формуле и файлам бюджетов, не переизмерены заново на живом рантайме. - **Регистрацию мутанта `adoption-bodies-declared-reactive`** в `scripts/mutation-gate.mjs` — предмет код-ревью, ещё не существует. ## Вердикт Жёлтый. High: 0, Medium: 1 (в скоупе задачи — чинится автором в тексте ТЗ, без нового issue). Возврат в «ТЗ в работе» (`S3-spec`). --- ## Материал раунда - Ветка: `issue/520-first-frame`, коммит `ad2f9afd2bfe` — ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет. - Дерево материала: `cc8e3bd73eeb7274db2725b8b308e12dc59a36e4` ``` git log --all --format='%H %T' | grep cc8e3bd73eeb ``` - Тело issue: `c5e79b63b5312eb40c5243fba19bd003f9b72e11b83fed51c1b1522547cc349c` - Вердикт конвейера: `yellow` · High 0