# SPEC-REVIEW-520-r3 ## Якоря - 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/r2. - sha256 текущего тела issue (`gh issue view 520 --repo Matysh/houseplan-card --json body -q .body | sha256sum`): `5eca6e230c290483bba634505217d17072f4b4a121c4aa2111b08a0fc6d007c3`. Отличается от хеша r2 (`7f68e4cd99242f44e13e9929cd0a7aa9dc3cb6ae5f871878670 bade18a2436ac`) — тело менялось, как и ожидается после возврата на правки. - Заход r3. Бюджет циклов §4 до этого ревью: **2/4** (израсходован r1 и r2, оба жёлтые). Вердикт ниже — зелёный, цикла не образует, бюджет остаётся **2/4**. - Материал кода не менялся между r2 и r3: `git log --oneline ad2f9afd.. b49721a0` даёт ровно один коммит — публикацию документа r2 (`docs/reviews/SPEC-REVIEW-520-r2.md`, 203 добавленных строки, `git diff --stat ad2f9afd..b49721a0 -- src/ demo/ scripts/ test/` пуст). Рабочая копия сейчас на `b49721a0` (detached HEAD от `origin/issue/520-first-frame`, подтверждено `git status`, `git rev-parse HEAD`). Изменилось только тело issue на GitHub — SHA репозитория тут не причём, доказательство ТЗ находится вне git. ## Скоуп ревью Повторный заход ревью ТЗ (PROCESS.md §2.4, §2.9/§2.10) после жёлтого вердикта r2. Предмет — **дельта тела issue между r2 и r3**: одна правка, заявленная автором в комментарии `2026-09-10T15:33:29Z` («Исправление по ревью ТЗ r2 → заход r3»): AC4 расширен на `firstStableRenderMs` изометрии, плюс отрицательный результат про `cache.entries.cleanFloor` изометрии перенесён из документа r2 в текст «Причины». Автор сам называет объём дельты: «одна строка AC4 и одна фраза в «Причине»» — проверено построчно, а не принято на слово (см. «Как проверялось»). Дельта локальна: правка текста ТЗ, ни ребейза, ни смены контракта, ни новой подсистемы — код продукта между r2 и r3 не менялся вовсе (см. «Якоря»). Полный разбор всей задачи заново не требуется. ## Как проверялось 1. Получено текущее тело issue #520 (`gh issue view 520 --json body`) и комментарий автора после r2 (единственный новый комментарий, метка `S4-spec-review` возвращена). 2. Прочитан `docs/reviews/SPEC-REVIEW-520-r2.md` целиком — вердикт, одна находка Medium-1, материал (SHA `ad2f9afd2bfe`, дерево `cc8e3bd73eeb`, тело `c5e79b63b531…`) — и `docs/reviews/SPEC-REVIEW-520-r1.md` для раздела «Унаследовано». 3. **Находка Medium-1 r2 сверена построчно с текущим текстом**, а не принята на слово автора. r2 требовал: строка AC4 должна покрывать `firstStableRenderMs` изометрии тем же относительным пределом, что и `modelReadyMs`, — либо явно исключить её одной фразой. Текущая строка AC4 (тело issue, строка 59): > «`timing.firstStableRenderMs.median` профиля взаимодействия ≤ 3000 > (абсолютный потолок) **и обе метрики старта — `modelReadyMs` и > `firstStableRenderMs` — внутри относительного лимита к базе > `a44fbd37` на обоих профилях, взаимодействия и изометрии**», «чем краснеет»: «превышение любого из **четырёх** относительных пределов или абсолютного потолка — job красный». Это буквально включает `firstStableRenderMs` изометрии — предмет находки Medium-1. Закрытие полное, не частичное. 4. **Пересчитаны все четыре относительных предела**, которые теперь называет AC4, по формуле `demo/performance/evaluate.mjs:73-118` (`relativeLimit = max(base*(1+ratio), base+allowance)`, `effective = min(hardMaxMs, relativeLimit)`), чтобы убедиться, что счёт «четырёх» верен и не сбит на единицу: - `modelReadyMs` interaction: база 1382, `budgets-large-house- interaction.json` (`maxRegressionRatio: 0.3, noiseAllowanceMs: 200, hardMaxMs: 2500`) → `max(1796.6, 1582) = 1796.6`; кандидат **1930 > 1796.6** — нарушен. - `firstStableRenderMs` interaction: база 2792.7, тот же файл (`ratio 0.3, allowance 250, hardMax 3000`) → `max(3630.5, 3042.7) = 3630.5`, `effective = min(3000, 3630.5) = 3000` — здесь относительный предел поглощён абсолютным потолком, что и заявляет отдельная первая половина AC4; двойного счёта нет. - `modelReadyMs` isometric: база 2165.6, `budgets-large-house- isometric.json` (`ratio 0.2, allowance 200, hardMax 3000`) → `max(2598.72, 2365.6) = 2598.72`; кандидат **2626.3 > 2598.72** — нарушен (установлено ещё в r1). - `firstStableRenderMs` isometric: база 2227, тот же файл (`ratio 0.2, allowance 250, hardMax 3500`) → `max(2672.4, 2477) = 2672.4`; кандидат **2678.2 > 2672.4** — нарушен (предмет находки Medium-1 r2, теперь покрыт текстом AC4). Итого ровно четыре относительных предела, все они действительно сейчас нарушены кандидатом — формулировка AC4 точна, не расширяет и не сужает набор относительно того, что показывает «Симптом». 5. Проверено согласование «Контракта» (строка 46: «изометрия … чинится тем же изменением и проверяется теми же метриками старта (AC4)») с новым текстом AC4 — формулировка «тем же способом» из r2 заменена на «теми же метриками старта», что точнее отражает: AC4 теперь буквально покрывает обе метрики обоих профилей, а не «тот же способ» в общем виде. Расхождений между «Контрактом» и таблицей AC больше нет. 6. Проверена вторая часть дельты — фраза в «Причине» (строка 24): «В бюджетах изометрии этот потолок не поднимался и уже равен 100 — там правка не нужна». Перепроверено чтением файлов (не принято на слово): `grep -n cleanFloor demo/performance/budgets-{isometric-smoke,large- house-isometric,interaction-smoke,large-house-interaction}.json` — оба файла изометрии дают `100`, оба файла взаимодействия — `180`. Совпадает с отрицательным результатом, который сам r2 получил самостоятельно (не то же самое, что просто скопировать вывод r2, — числа сверены заново на этом SHA, хоть код и не менялся). 7. `git log --oneline ad2f9afd..b49721a0` и `git diff --stat` — единственный новый коммит это публикация документа r2; продуктовый код (`src/**`, `demo/**`, `scripts/**`, `test/**`) не менялся между r2 и r3 — гейты кода этому раунду не относятся (см. «Чего не проверял»), как и в r1/r2. 8. Проверены state/labels issue (`OPEN`, `bug`, `P1`, `S4-spec-review`) — соответствуют ожиданиям заголовка задания (заход r3, 2/4 циклов). ## Находки Нет находок, переживших проверку. Единственная находка r2 (Medium-1) закрыта полностью — см. «Закрытие раунда r2» и п.3–5 «Как проверялось». Новых находок делом дельты не выявлено: правка локальна (одна строка AC4, одна фраза «Причины»), обе проверены построчно и не вводят новых противоречий с остальным текстом ТЗ. ## Закрытие раунда r2 | Находка r2 | Чем закрыта | Где это видно | |---|---|---| | Medium-1 — амендированный AC4 не покрывал `firstStableRenderMs` изометрии (второе число того же предложения «Симптома», что и уже покрытый `modelReadyMs`) | AC4 переписан: «обе метрики старта — `modelReadyMs` и `firstStableRenderMs` — внутри относительного лимита… на обоих профилях»; «чем краснеет» — «любого из **четырёх** относительных пределов»; «Контракт» синхронизирован формулировкой «теми же метриками старта (AC4)» | Тело issue, AC4 (строка 59) и «Контракт» (строка 46). Численно перепроверено в п.4 «Как проверялось» — все четыре предела названы верно, счёт не сбит | ## Унаследовано из r2 (и, транзитивно, из r1) Приняты без повторной проверки — дельта r2→r3 их не задевает, код между раундами не менялся: - **Причина регрессии измерена, не догадка**: три технических утверждения о поведении `wrapped`-свойств Lit — r1 сверил дословно с исходником `@lit/reactive-element` (`docs/reviews/SPEC-REVIEW-520-r1.md`, «Как проверялось» п.4, материал SHA `27ea23c1ec25`, дерево `f80bcdd58b72`). - **M1 (раздел «Риски») и M2 (AC3) из r1** — закрыты в r2 и сверены там построчно (`docs/reviews/SPEC-REVIEW-520-r2.md`, «Как проверялось» п.3, «Закрытие раунда r1»), текст этих разделов между r2 и r3 не менялся. - **AC1** (source-scan тест по образцу `config-adoption-ownership. test.mjs`) и **AC2** (юнит на `onBodyReplaced`) — доказуемы дёшево и детерминированно, текст этих строк не менялся с r1 (сверено построчно с текущим телом в этом раунде — строки идентичны цитатам r1/r2). - **AC5** (`cache.entries.cleanFloor` ≤ 100 на двух interaction-файлах бюджетов) — привязан к точным файлам, не менялся; числа перепроверены заново в п.6 «Как проверялось» этого раунда (не просто унаследованы). - **Откат** (revert двух строк `static properties` + потолка 180) и **release-артефакты** (`User-Visible: no`, разблокирует `v1.74.0-beta.1`) — просты, однозначны, текст не менялся с r1. - **Продуктовая рамка** (администратор дома, поверхность — первый показ большого дома) совпадает с `docs/SCOPE.md` J1 и фикстурой 60 комнат/200 устройств — r1 сверил, текст не менялся. - **Выбор полного трека** — верен, не оспаривается ни разу за три раунда. - **Ни одного вопроса владельцу не требуется** — весь материал технический (см. обоснование r1: «продуктовых развилок в задаче нет»), подтверждено в r1 и не оспорено в r2; делта r3 тоже чисто техническая (числа, счёт относительных пределов) — вывод не меняется. Документы-источники: `docs/reviews/SPEC-REVIEW-520-r1.md` (материал — ветка `issue/520-first-frame` на `27ea23c1ec25`, дерево `f80bcdd58b72`), `docs/reviews/SPEC-REVIEW-520-r2.md` (материал — та же ветка на `ad2f9afd2bfe`, дерево `cc8e3bd73eeb`). ## Что проверено и корректно (этот раунд) - Единственная правка фикс-комментария автора (`2026-09-10T15:33:29Z`) подтверждена чтением текущего тела issue, а не принята по заявлению — Medium-1 r2 закрыт полностью, не частично. - Счёт «четырёх относительных пределов» в AC4 арифметически точен: пересчитаны все четыре по опубликованной формуле и файлам бюджетов, все четыре сейчас действительно нарушены кандидатом — AC4 не занижает и не завышает то, что реально показывает «Симптом». - «Контракт» (строка 46) и таблица AC4 больше не расходятся в формулировке охвата изометрии — предыдущая асимметрия («тем же способом» против буквально одной метрики) устранена. - Фраза «Причины» про потолок `cache.entries.cleanFloor` изометрии (100, правка не нужна) подтверждена чтением всех четырёх файлов бюджетов заново на этом SHA, не просто перенесена из документа r2 на слово. - Код между r2 и r3 не менялся (`git log`, `git diff --stat` на диапазоне `ad2f9afd..b49721a0`) — дешёвые гейты кода (`tsc`, `test`, `build`) этому раунду не относятся по той же причине, что и в r1/r2: предмет этапа — текст ТЗ, продуктовой правки в дереве ещё нет. - Остальной текст ТЗ (унаследованное) не тронут дельтой — проверка инвариантов и код-ревью инфраструктуры (мутант, тесты) остаются предметом код-ревью после `S5-ready`. ## Чего не проверял - **Гейты кода** (`tsc --noEmit`, `npm test`, `npm run build`, `check-docs.mjs`) — не гонял: `src/**` не менялся ни разу за три раунда, весь диапазон коммитов ветки — диагностика бенчмарка (`test(perf):`). Предмет этапа spec — текст ТЗ. - **`npm run invariants`** — не гонял: геометрия и её ссылки не тронуты ни кодом, ни правкой ТЗ. - **Реальный прогон `Full Performance`** — не запускал; числа «Симптома» и пересчитанные относительные пределы взяты из текста issue и файлов бюджетов в дереве, перепроверены арифметически по опубликованной формуле, не переизмерены заново на живом рантайме. - **Регистрацию мутанта `adoption-bodies-declared-reactive`** в `scripts/mutation-gate.mjs` — предмет код-ревью, ещё не существует (продуктовой правки AC1/AC2 в дереве ещё нет). - **Полную сверку разделов §7.1, не тронутых дельтой** (Риски, откат, release-артефакты, продуктовая рамка) — заново не перечитывал построчно, только сверил, что их текст байт-в-байт совпадает с тем, что видели r1 и r2 (см. «Унаследовано»). ## Вердикт Зелёный. High: 0, Medium: 0. ТЗ готово к разработке (`S5-ready`). Единственная находка r2 закрыта полностью и точно; новых находок дельта не вносит. Бюджет циклов §4 не расходуется (зелёный вердикт цикла не образует) и остаётся 2/4. --- --- ## Материал раунда - Ветка: `issue/520-first-frame`, коммит `b49721a0bd98` — ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет. - Дерево материала: `50d8affd923b0a5e133ee1e453bb9388a5f44c78` ``` git log --all --format='%H %T' | grep 50d8affd923b ``` - Тело issue: `60492a3aec5ff7fed54f7b32c040b7a118526c209609d736ee68e116eb8f0aa6` - Вердикт конвейера: `green` · High 0