23 KiB
SPEC-REVIEW-520-r1
Якоря
- Issue: #520
«Первый устойчивый кадр большого дома вырос до 3.36 с при потолке 3.0 с —
регрессия после #509/#500», метка
S4-spec-review(подтвержденоgh issue view 520 --json labels, 2026-09-10). - ТЗ: тело issue #520, раздел
## ТЗ (полный трек — критерий §5 ...), строки 30–67 полученного тела (см. ниже). - sha256 нормализованного тела issue (как получено
gh issue view 520 --repo Matysh/houseplan-card --json body -q .body, без дополнительной нормализации переводов строк):5477d052e0e87182f51a9b3b05d37fce0a7ada3d9dc66c10f927f64564ccc6e4 - Материал ветки:
issue/520-first-slug→ фактическиissue/520-first-frameна27ea23c1ec25e6421542c9901eb6e8e2484d0665— по собственному признанию автора в комментарии2026-09-10T15:09:06Z: «пока только диагностика бенчмарка, продуктовой правки в ней ещё нет; она появится послеS5-ready». Подтверждено чтениемgit log/git show --statна трёх последних коммитах — все три с префиксомtest(perf):, правок вsrc/**нет. - Заход r1. Бюджет циклов §4 до этого ревью: 0/4. Вердикт ниже — жёлтый, поэтому расходует цикл: после этого ревью бюджет 1/4.
Примечание по маршрутизации. Шапка задания содержит блок инструкций
для этапа code (диапазон SHA, требование не делать git fetch,
Зелёного Validate на этом SHA нет и т.д.) и фразу «Для этапа spec: ТЗ
живёт в теле issue (#517)», где #517 — не опечатка про номер текущей
задачи, а ссылка на решение владельца от 2026-09-10 (issue #517),
которым сам архив docs/specs/ закрыт для новых ТЗ (см. PROCESS.md
§2.3, строка 125). Реальный этап подтверждён меткой S4-spec-review на
#520 — далее в разборе применяются правила §2.4/§7.1, а раздел про
git diff origin/dev...HEAD/SHA к этому раунду не относится: код
продукта ещё не менялся.
Скоуп ревью
Ревью ТЗ (PROCESS.md §2.4) для полного трека — задача сама называет
нарушенный критерий лёгкого трека («нет влияния на производительность»),
выбор трека верен. Предмет: раздел ## ТЗ тела issue #520 — контракт,
AC1…AC5, откат, release-артефакты — против §7.1 (обязательные разделы),
§2.5 (DoR) и docs/SCOPE.md (продуктовая рамка).
Как проверялось
- Прочитано тело issue #520 целиком и оба комментария (
S2-анализ, «Причина найдена…», переход вS3-spec) — понадобилось для контекста диагностики, на которой стоит контракт. - Сверено с PROCESS.md §1 (класс A), §2.3–2.5, §4, §5, §7.1.
- Сверено с
docs/SCOPE.md: персона/поверхность ТЗ (администратор, первый показ большого дома) сопоставлены с J1 и таблицей Target audience («large flat, 20–200 devices»; фикстура 60 комнат/200 устройств — то же самое). - Технические утверждения о поведении Lit перепроверены чтением
исходника, а не приняты на веру — это и есть весь фундамент
контракта:
node_modules/@lit/reactive-element/development/reactive-element.js:249-252—wrapped = trueдействительно ставится вcreateProperty, когдаthis.prototype.hasOwnProperty(name), до проверкиnoAccessor(строка чуть нижеif (!options.noAccessor)) — подтверждает пункт 2 контракта дословно;:880-886— принудительная записьwrapped-свойства вchangedPropertiesсо значениемundefinedдействительно происходит внутри блока первого обновления (hasUpdatedещё не выставлен; комментарий в самом Lit: «only for the case of properties created viacreatePropertyon accessors») — подтверждает механизм лишней эпохи;:339-341(getPropertyOptions) и:697-712(requestUpdate) — необъявленное имя свойства действительно получаетdefaultPropertyDeclaration, сравнивается черезnotEqualи уходит в_$changePropertyнаравне с объявленным — подтверждает пункт 1 контракта («requestUpdateработает с необъявленным именем»).
- Цитаты по
src/houseplan-card.tsсверены построчно с текущим файлом: аксессоры_layout/_serverCfg(923–940),static propertiesсо_layout/_serverCfg: { state: true }(2556, 2560),willUpdateи_cfgEpoch++/preserveGeometry(4104–4114), ключ памятки модели`${_cfgEpoch}|${_cfgFingerprint()}`(3919), ключcache.entries.cleanFloor`${space.id}|${_cfgEpoch}|${roomKey}`(9916) — все точны, расхождений с текстом ТЗ нет. - Оба файла бюджетов из AC5 (
demo/performance/budgets-interaction-smoke.json,demo/performance/budgets-large-house-interaction.json) прочитаны:cleanFloor: 180в обоих подтверждён, других budgets-файлов сcleanFloorв дереве нет — список AC5 полон и точен. Дополнительно сверен коммит914e8402, поднявший потолок: егоgit show --statтрогает ровно эти два файла. - Пересчитаны относительные лимиты
modelReadyMs(формулаdemo/performance/evaluate.mjs:73-118,min(hardMaxMs, max(base*(1+ratio), base+noiseAllowanceMs))) для профилейlarge-house-interactionиlarge-house-isometric, чтобы понять, действительно ли изометрия тоже нарушает свой бюджет (см. находку Medium-3) — числа изометрии из «Симптома» не были проверены автором на предмет реального превышения, я это сделал сам. - Проверено содержимое
test/config-adoption-ownership.test.mjs(техника — регэксп-скан исходников на предмет прямой записи identity/полей вне владельца) иtest/config-adoption.test.mjs(host-стаб, юнит без реального Lit-компонента) — чтобы оценить, насколько реалистичны заявленные способы доказательства AC1/AC2. - Проверено, куда пишет диагностика
bootDiag(demo/benchmark_large_house.mjs:401,1142,1213-1218) — толькоconsole.logстроки прогона, не бюджетируемое поле — это и есть основание находки Medium-2. git status— рабочая копия чистая относительно продукта (кроме отсутствующегоnode_modules, не связано с этим ревью).
Находки
Medium-1 — обязательный раздел «Риски» отсутствует (§7.1)
Файл: тело issue #520, раздел ## ТЗ (строки 30–67 полученного тела).
PROCESS.md:535-538 перечисляет обязательные разделы ТЗ, включая
риски; DoR (PROCESS.md:164) отдельно требует «риски перечислены»
как условие перехода в «Готово к разработке». В тексте ТЗ #520 такого
раздела нет вовсе — ни явного заголовка, ни даже строки вида «риски:
минимальны, потому что …». Для сравнения — прямой прецедент того же
класса задачи, docs/specs/506-startup-performance.md:149-161
(«Техническая гипотеза, риски и откат»), перечисляет риски явным
списком («двойное подключение при overlapping promises…», «исправление
только glow без устранения isometric regression» и т.д.) и привязывает
каждый к AC, который его закрывает.
Сценарий, который упущен без раздела риска. Контракт снимает
_serverCfg/_layout с объявленных реактивных свойств Lit. Реальный
риск есть и не назван: любой код, читающий
houseplan-card.elementProperties — Lit devtools, будущий рефлектор
атрибутов, сторонний код, полагающийся на hasOwnProperty в
options/reflect — перестанет видеть эти два поля в перечне
реактивных свойств. Ничего в контракте не говорит, проверено ли
отсутствие такого потребителя (или почему он невозможен) — риск
следовало либо назвать и снять, либо принять с обоснованием.
Чем чинится: одним абзацем в теле issue — не блокирует исполнимость контракта, но обязателен для DoR.
Medium-2 — AC3 не имеет дешёвого воспроизводимого способа доказательства
Файл: тело issue #520, строка 56 (AC3 в таблице AC).
DoR (PROCESS.md:154-155) требует, чтобы у каждого AC был назван способ
доказательства из набора unit/backend/smoke/golden/«ревью кода».
AC1 (source-scan тест по образцу уже существующего
test/config-adoption-ownership.test.mjs), AC2 (юнит), AC4/AC5 (уже
существующие budgeted-гейты Validate) — все аккуратно попадают в этот
набор. AC3 — нет. Его «чем доказан» — «диагностика bootDiag в
полном сравнении против a44fbd37, числа в комментарии issue»; «чем
краснеет» — «лишняя эпоха вернётся — числа разойдутся с базой».
Проверено чтением demo/benchmark_large_house.mjs:1213-1218: bootDiag
только печатается в лог прогона (console.log) и явно исключён из
бюджетируемой записи — сам текст ТЗ подтверждает это разделом «Принято
предположительно» («печатается в лог прогона, в бюджетируемую запись не
попадает»). Значит AC3 нельзя перепроверить иначе, чем вручную читая лог
дорогого прогона Full Performance (семь образцов, сравнение с базой) —
ровно того класса гейтов, который по прямому указанию в задании этого
ревью «предрелизный, а не гейт ревью». Автор и на этом ревью, и на
будущем код-ревью вынужден либо доверять уже вставленным в issue числам
без способа их независимо переснять дёшево, либо вручную гонять полное
сравнение при каждом цикле.
По существу AC3 не даёт ничего, чего не даёт уже AC4 (тот же корень
регрессии закрывает и хронометраж, и счётчик эпох) — это не лишний
критерий, а лишний недоказуемый критерий. Не блокирует контракт:
вариантов чинения минимум два и оба дёшевы — либо явно понизить AC3 до
«проверено чтением кода при код-ревью (AC1+AC2 логически влекут AC3) +
однократная сверка bootDiag на конкретном SHA перед публикацией беты,
не гейт CI», либо убрать AC3 из таблицы как поглощённый AC1/AC2/AC4 и
оставить числа диагностики просто иллюстрацией в тексте причины.
Medium-3 — регрессия профиля изометрии показана в «Симптоме», но не отражена в контракте/AC
Файл: тело issue #520, строка 3 («Профиль изометрии — то же:
modelReadyMs 2165.6 → 2626.3») против раздела «Контракт»/«AC и
доказательства» (строки 38–58), где изометрия не упомянута вовсе.
Автор сам приводит эти числа как «то же» (то есть тот же механизм
регрессии) для профиля large-house-isometric, но ни один AC, ни
скоуп/не-скоуп не называют этот профиль. Проверил сам, чтобы понять,
пустая это формальность или реальный пропуск: у профиля
large-house-isometric (demo/performance/budgets-large-house-isometric.json:6)
maxRegressionRatio для modelReadyMs — 0.2, noiseAllowanceMs — 200.
По формуле гейта (demo/performance/evaluate.mjs:73-118,
relativeLimit = max(base*(1+ratio), base+allowance)) эффективный потолок
— max(2165.6*1.2, 2165.6+200) = 2598.72. Кандидат 2626.3 > 2598.72 —
изометрия тоже нарушает свой относительный бюджет (на ~1 %, у
large-house-interaction — на ~7 %), просто это не абсолютный гейт
Validate и потому не горит красным в CI сейчас, точно как и обсуждаемый
в AC4 modelReadyMs профиля interaction.
Раз ТЗ уже включает в AC4 покрытие «мягкого» (не абсолютного, а
относительного) регресса modelReadyMs для одного профиля, тот же
стандарт по симметрии должен применяться и ко второму профилю, который
сам автор привёл как доказательство единого механизма. Сейчас неясно:
это фиксится тем же изменением автоматически и просто не нуждается в
отдельном AC (правдоподобно, раз причина общая для любого первого
обновления карточки), или это осознанно оставлено вне скоупа задачи —
но текст ТЗ не говорит ни того, ни другого явно. Это техническая
неопределённость (не продуктовая — какая часть исправления какой профиль
чинит, решают агенты между собой, не владелец), поэтому вопрос не выносится
владельцу, а возвращается автору как находка: либо расширить AC4/AC5
явным упоминанием large-house-isometric (и его budgets-файлов, если они
регрессировали похожим образом по cleanFloor), либо одной фразой
зафиксировать «то же изменение чинит и профиль изометрии, отдельного
гейта не заводим, потому что…».
Что проверено и корректно
- Причина — измерена, не догадка. Все три опорных технических
утверждения о поведении Lit (
wrapped, форсированная запись вchangedPropertiesна первом апдейте, работаrequestUpdateс необъявленным именем) подтверждены чтением фактического исходника@lit/reactive-element, а не только словами автора. Все цитаты поhouseplan-card.ts(номера строк) сверены с текущим файлом и точны. - AC1 доказуем дёшево и детерминированно: тот же приём (regex-скан
исходников), что уже применяется в этом же тестовом файле для
соседнего инварианта — реалистичный, недорогой способ доказательства с
чётким «чем краснеет» (мутант возвращает
_serverCfg: { state: true }). - AC2 корректно опирается на уже проверенное (мной, чтением исходника
Lit) поведение — сам механизм гарантирован фреймворком, тесту остаётся
подтвердить, что
onBodyReplacedвызывается на замену и не вызывается на идентичном значении; это не новая инфраструктура. - AC4/AC5 привязаны к точным существующим файлам бюджетов; значения
(потолок 180 → 100,
hardMaxMs/maxRegressionRatioдляmodelReadyMs) подтверждены построчно и соответствуют тому, что реально сейчас нарушено (и по абсолютному, и по относительному гейту). - Откат — простой и однозначный (revert двух строк
static properties+ потолка 180), данные и контракты не затронуты. - Release-артефакты корректно называют
User-Visible: noи отсутствие правки changelog, с указанием, какой бета-кандидат разблокирован. - Продуктовая рамка называет персону и поверхность (
docs/SCOPE.mdJ1, «Home admin», «large flat… 20–200 devices») и совпадает с фикстурой (60 комнат/200 устройств); «что человек увидит» сформулировано одной фразой без раздутия. - Выбор полного трека корректен: perf-влияние безоговорочно нарушает критерий лёгкого трека §5, и автор сам это называет.
- Ни одного вопроса владельцу не задано — и не нужно: весь материал технический, продуктовых развилок в задаче нет.
- «Принято предположительно» оформлено ровно по формату §7.1
(сохранение диагностики
bootDiagв дереве помечено как решение, которое ревьюер вправе оспорить) — не поглощено находками выше.
Чего не проверял
- Гейты кода (
tsc --noEmit,npm test,npm run build,check-docs.mjs) — не гонял: на этом SHA нет ни строки продуктового кода (src/**), только диагностика бенчмарка; предмет этапа spec — текст ТЗ, а не код. Это будет предметом код-ревью послеS5-ready. npm run invariants— не гонял по той же причине: геометрия и ссылки на неё не тронуты.- Реальный прогон
Full Performance/Validateна этом SHA — не запускал: продуктовой правки, которую можно было бы измерить, ещё нет. - Регистрацию мутанта
adoption-bodies-declared-reactiveвscripts/mutation-gate.mjs— не проверял; на этапе ТЗ его ещё не существует по определению (мутант тестирует ещё не написанный код), это предмет код-ревью. - Уровень шума в уже приведённых числах
Full Performance(7 образцов) — не переоценивал статистически; принял как измеренные, со ссылками на конкретные прогоны CI, приведённые автором.
Унаследовано / закрытие предыдущего раунда
Не применимо — первый раунд (r1) ревью ТЗ #520.
Вердикт
Жёлтый. High: 0, Medium: 3 (все в скоупе задачи — чинятся автором в том
же тексте ТЗ, без нового issue). Возврат в «ТЗ в работе» (S3-spec).
Материал раунда
- Ветка:
issue/520-first-frame, коммит27ea23c1ec25— ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет. - Дерево материала:
f80bcdd58b72ba2e7b38cfaac3e80e978cbe67c9git log --all --format='%H %T' | grep f80bcdd58b72 - Тело issue:
d370780d07b6217cfaa48aa21213828fc3a6a70ec16dbea7fee15dbf00e22871 - Вердикт конвейера:
yellow· High 0