mirror of
https://github.com/Matysh/houseplan-card
synced 2026-10-04 05:41:34 +00:00
313 lines
29 KiB
Markdown
313 lines
29 KiB
Markdown
# SPEC-REVIEW-509-r1
|
|
|
|
Issue: [#509](https://github.com/Matysh/houseplan-card/issues/509) — «Сводная
|
|
панель: при первом показе подвисание и «Source unavailable» вместо значений
|
|
до загрузки metrics-чанка».
|
|
Этап: spec (PROCESS.md §2.4). Заход: r1. Блокирующих циклов израсходовано 0 из 4.
|
|
Материал: тело issue #509, раздел `## ТЗ` (owner-решение #517, 2026-09-10 —
|
|
ТЗ полного трека тоже живёт в теле issue, файл `docs/specs/509-*.md` не
|
|
создаётся), на момент перехода `S3-spec → S4-spec-review`
|
|
(`unlabeled: S3-spec` / `labeled: S4-spec-review` в
|
|
`2026-09-10T07:56:11Z`, тот же момент несёт единственную правку тела после
|
|
S2-комментария). Рабочая копия `dev` на `d204bf5d` — код по этой задаче ещё
|
|
не существует, ветки `issue/509-*` на origin нет; ревью целиком по тексту.
|
|
|
|
## 1. Скоуп ревью
|
|
|
|
Трек — полный: метки issue — `bug`, `S4-spec-review`, `small` отсутствует.
|
|
Явного предложения «трек: small — нет, критерий не пройден» я в S2-комментарии
|
|
не нашёл дословно, но по существу критерий §5 «нет влияния на
|
|
производительность» действительно не пройден — вся задача построена вокруг
|
|
измеренных миллисекунд и мутационных гардов на перф-путь, что и оправдывает
|
|
полный трек фактически (см. Low-1 ниже: это гигиена формы, не решение).
|
|
|
|
Ревью охватывает:
|
|
- тело issue #509 целиком: секцию «Симптом», секцию «Что видно в коде (для
|
|
S2)», комментарий S2-аналитики (владелец/автор, `2026-09-10T07:49:33Z`) и
|
|
секцию `## ТЗ`;
|
|
- соответствие `docs/SCOPE.md` (персона, поверхность, job), `PROCESS.md`
|
|
§7.1/§7.2/§5, `AGENTS.md`;
|
|
- фактические утверждения ТЗ о текущем коде — сверены чтением, не приняты на
|
|
слово;
|
|
- реализуемость каждого AC заявленным способом доказательства.
|
|
|
|
Это первый заход — раздела «Унаследовано из r0» не требуется, разбор полный.
|
|
|
|
## 2. Как проверялось
|
|
|
|
- Прочитаны `docs/SCOPE.md`, `AGENTS.md`, `PROCESS.md` целиком (включая §2.2,
|
|
§2.4, §2.10, §4, §5, §7.1, §7.2, §8).
|
|
- Прочитано тело issue #509 и его единственный комментарий через
|
|
`gh issue view 509 --json body,comments,labels,state`.
|
|
- Прогон таймлайна issue (`gh api .../issues/509/timeline`) и
|
|
`userContentEdits` (GraphQL) — подтверждено: `S1-new → S2-analysis` в
|
|
`07:44:05`, комментарий-аналитика в `07:49:33`, переход в `S3-spec` уже в
|
|
`07:49:35` (через 2 секунды после своего же комментария), переход в
|
|
`S4-spec-review` в `07:56:11` — единственная правка тела issue приходится на
|
|
этот же момент (второй `userContentEdits`-узел). Значит между вопросом
|
|
владельцу («что видно в первые ~200 мс», вариант A по умолчанию) и записью
|
|
ТЗ отдельного ответа владельца физически не было — редакция ТЗ добавлена в
|
|
теле той же ~7-минутной сессией, которая задала вопрос. Это не идёт вразрез
|
|
с §2.2 (аналитик не обязан ждать подтверждения и явно объявил «если ответа
|
|
не будет — реализую A»), но заголовок ТЗ «решение владельца по плейсхолдеру
|
|
— в комментарии S2» точнее было бы читать как «дефолт автора с явным
|
|
объявлением», а не как отдельное решение владельца — содержательно на
|
|
вердикт не влияет (вариант A разумен, задокументирован, проверяем AC1/AC6),
|
|
поэтому не заводится отдельной находкой, но фиксирую здесь для
|
|
прослеживаемости следующего раунда.
|
|
- Каждое фактическое утверждение ТЗ и S2-комментария о текущем коде
|
|
перепроверено чтением на `dev` `d204bf5d`, а не принято на слово:
|
|
- `LoadedSummaryPanelRuntime.value()` (`src/summary-panel-runtime-loaded.ts:715-720`):
|
|
подтверждено — `resolved ?? this.t('summary.unavailable')`, и `resolved`
|
|
равно `null` как при отсутствующем `module` (`metricsModule` ещё не
|
|
загружен), так и при легитимно недоступном источнике — состояния
|
|
неразличимы в возвращаемом значении, ровно как описывает симптом.
|
|
- `metrics()` (`:687-712`): подтверждено — вызывается синхронно, мемо по
|
|
`cfgEpoch`/`layoutRev`/`registryRev` (device) и `cfgEpoch` (area), при
|
|
первом вычислении после загрузки модуля выполняется целиком в кадре.
|
|
- `totalCleanFloorAreaM2` (`src/summary-panel-metrics.ts:59-84`):
|
|
подтверждено — цикл по `space.rooms` вызывает `innerContourForRoom` без
|
|
`sharedRoomWallGeometry`/`sharedMultiWallNodes` на каждой комнате; сама
|
|
`innerContourForRoom` (`src/wall-thickness.ts:2750-2779`) в этом случае
|
|
заново вызывает `wallBodiesGeometry`/`multiWallNodesForGeometry` — точно
|
|
описанная причина 7,5-кратной разницы.
|
|
- Утверждение «карточка уже передаёт общую геометрию через `_innerContour`»
|
|
подтверждено: `_innerRoomContour` (`src/houseplan-card.ts:9219-9240`)
|
|
передаёт `roomWalls`/`multiWallNodes` из `_wallUnionGeometry()` (общий,
|
|
закешированный по эпохе объект) — карточка действительно не пересчитывает
|
|
геометрию стен на каждой комнате, а сводная панель — пересчитывает.
|
|
Не выдумка, реальный прецедент в том же файле.
|
|
- `SummaryPanelPresentation`/анимация 190 мс (issue #505) — подтверждено:
|
|
класс существует и подключён (`src/summary-panel-runtime-loaded.ts:23,93`),
|
|
#505 закрыт и смёржен (`gh issue view 505` → `state: CLOSED`, статусных
|
|
меток нет) — AC5 действительно проверяет уже существующий, но
|
|
(по симптому) заблокированный синхронным расчётом путь, а не выдуманную
|
|
фичу.
|
|
- `summary.unavailable` — реальный существующий ключ
|
|
(`src/summary-panel-i18n.ts:16,84,152,220`, en/ru/de/fr) — заявление
|
|
«новых i18n-ключей нет» корректно, скелет действительно чисто визуальный.
|
|
- Персона «житель дома» — используемый в проекте перевод `Household
|
|
members` из `docs/SCOPE.md` (совпадает с формулировкой в
|
|
`SPEC-REVIEW-212-r1.md`, `SPEC-REVIEW-374-r1.md`, `SPEC-REVIEW-505-r2.md`)
|
|
— не изобретённый термин.
|
|
- Проверена техническая реализуемость AC3 («один вызов… через инъекцию
|
|
счётчика»): `wallBodiesGeometry`/`multiWallNodesForGeometry` — экспортируемые
|
|
чистые функции (`src/wall-thickness.ts:2434,3662`), `innerContourForRoom`
|
|
уже принимает их как необязательные параметры — заявленный план (передать
|
|
общую геометрию один раз на пространство) технически реализуем без нового
|
|
паттерна, здесь это чистая инженерная перестановка вызовов, а не гипотеза.
|
|
- Проверено соответствие обязательных разделов §7.1 (таблица ниже).
|
|
- Гейты (`typecheck`/`test`/`build`) не прогонялись — кода в материале нет
|
|
(ветки `issue/509-*` не существует), на этапе `spec` эти гейты неприменимы
|
|
к пустому диффу; смотри также §5 «Чего не проверял».
|
|
|
|
### Проверка обязательных разделов (§7.1)
|
|
|
|
| Требование | Где в теле issue |
|
|
|---|---|
|
|
| Сценарий | `## ТЗ` → «Сценарий»: персона «житель дома», поверхность View + kiosk, момент — первое открытие/включение панели |
|
|
| Что человек увидит до/после | «Что человек увидит до и после» — одной фразой, без терминов реализации |
|
|
| Проблема | Секция «Симптом» + «Что видно в коде» (тело issue, до `## ТЗ`) |
|
|
| Скоуп и не-скоуп | «Контракт» (объём правки) + «Не-скоуп» |
|
|
| Контракт поведения | «Контракт», пп. 1–5 |
|
|
| UX | Явного отдельного раздела нет, но по существу покрыт («Контракт» пп.1,2,5 — нет новых элементов управления, только визуальные состояния уже существующих строк) |
|
|
| Модель данных и миграция | Преамбула ТЗ: «Модель данных и схема конфигурации не меняются» |
|
|
| i18n | Преамбула: «Новых i18n-ключей нет» — подтверждено чтением, см. выше |
|
|
| AC1…ACn с доказательством | «AC», 7 пунктов, у каждого назван unit/smoke |
|
|
| План автотестов | Не вынесен отдельным разделом, но покрыт AC + таблицей «Чем краснеет» (тот же уровень, что и прецедент #506, признанный там достаточным) |
|
|
| Риски | «Риски», 2 пункта |
|
|
| Откат | «Откат» |
|
|
| Release-артефакты | «Release-артефакты» |
|
|
|
|
Все обязательные разделы присутствуют по существу; отсутствие отдельного
|
|
финального блока «Принятые технические предположения» разобрано как
|
|
Medium-1 ниже.
|
|
|
|
## 3. Находки
|
|
|
|
### Medium-1 (в скоупе) — отсутствует раздел «Принятые технические предположения» (PROCESS.md §7.1)
|
|
|
|
ТЗ делает по ходу текста несколько односторонних технических решений, которые
|
|
пользователь не наблюдает, но которые прямо влияют на то, что именно будет
|
|
реализовано, и ни одно не вынесено в явный финальный блок «принято
|
|
предположительно, поменять свободно» — том самом формате, который PROCESS.md
|
|
§7.1 требует именно для того, чтобы ревьюер мог их оспорить, а не читать как
|
|
уже окончательное решение. Конкретно:
|
|
|
|
1. **Механизм переноса расчёта «вне кадра» назван как альтернатива, не
|
|
решение.** П.4 контракта: «расчёт запускается после кадра
|
|
(микрозадача/`requestIdleCallback` с фолбэком на таймер)». Это не один
|
|
механизм с уточнением, а два содержательно разных: микрозадача выполняется
|
|
до следующей отрисовки (гарантированно быстро), `requestIdleCallback`
|
|
ждёт простоя главного потока и при устойчиво занятом потоке может
|
|
откладываться на неопределённое время — то есть ровно то поведение,
|
|
которое приводило бы к варианту, где готовое число появляется через
|
|
секунды после показа скелета на медленной машине. Ни один AC не
|
|
ограничивает, сколько именно может пройти между показом скелета и
|
|
заменой его значением (см. также Medium-2) — то есть от выбора между
|
|
этими двумя механизмами наблюдаемо зависит поведение, а формулировка ТЗ
|
|
не решает, какой из них, а перечисляет оба через слэш.
|
|
2. Способ технической инъекции счётчика вызовов для AC3 (как именно тест
|
|
заменит/оборачивает `wallBodiesGeometry`/`multiWallNodesForGeometry`,
|
|
учитывая, что они — обычные экспортируемые функции ES-модуля, а не
|
|
параметры) не назван ни как решённый вопрос, ни как «на усмотрение
|
|
реализации».
|
|
3. Форма хранения «прежнего значения» при инвалидации мемо (п.4 контракта:
|
|
«показывает прежнее значение до прихода нового») — остаётся ли это тем же
|
|
полем `deviceMemo`/`areaMemo`, что и сегодня, или требует нового состояния
|
|
(«последнее успешно вычисленное» отдельно от «текущего валидного») — не
|
|
названо явно.
|
|
|
|
Прецедент того же приёма находки, для той же подсистемы, у того же автора:
|
|
`SPEC-REVIEW-505-r1.md`, Medium «отсутствует обязательный раздел «Принятые
|
|
технические предположения»», со ссылкой на `docs/specs/493-...md` как
|
|
образец, где формат уже применялся. В #506 то же требование было закрыто
|
|
записью Low, а не Medium, потому что там содержание уже было распределено
|
|
по тексту («предпочтительный дизайн… точные имена и границы модуля
|
|
допускается уточнить») — здесь же пункт 1 выше не просто не назван свободным,
|
|
а прямо оставляет открытым выбор между двумя семантически разными
|
|
механизмами, поэтому не дотягивает даже до уровня #506 и остаётся Medium.
|
|
|
|
**Как чинится:** добавить в конец ТЗ раздел «Принятые технические
|
|
предположения» и явно решить (или явно пометить «на усмотрение реализации,
|
|
не меняет ни один AC») три пункта выше — в первую очередь пункт 1, так как
|
|
он пересекается с Medium-2.
|
|
|
|
### Medium-2 (в скоупе) — контракт п.4 (прежнее значение при инвалидации мемо, без скелета) не доказан ни одним AC
|
|
|
|
П.4 контракта содержит отдельное поведенческое обязательство: «при
|
|
инвалидации панель показывает **прежнее** значение до прихода нового (не
|
|
скелет — скелет только когда значения ещё не было)». Это прямая защита
|
|
именно от того паттерна мигания, из-за которого заведена задача (значение
|
|
подменяется на «недоступно»/пусто и обратно) — то есть по существу это
|
|
самый близкий к первоначальному симптому кусок контракта, а не второстепенная
|
|
деталь.
|
|
|
|
Ни один из AC1–AC7 его не проверяет:
|
|
- AC1/AC2 — про состояние «`metricsModule` ещё не загружен» vs «источник
|
|
недоступен», не про инвалидацию уже загруженного модуля;
|
|
- AC3 — про идентичность площади и число вызовов геометрии, не про то, что
|
|
показывается в UI между инвалидацией и пересчётом;
|
|
- AC4 — про длительность первого показа, не про повторные показы после смены
|
|
конфигурации;
|
|
- AC5/AC6 — про анимацию появления/исчезновения панели и уважение
|
|
`prefers-reduced-motion`, не про эту конкретную защиту;
|
|
- AC7 — про трейлеры/i18n/golden.
|
|
|
|
**Сценарий проявления:** реализация меняет `cfgEpoch` (правка конфигурации,
|
|
редактор) и на время пересчёта (после п.4, теперь асинхронного) ошибочно
|
|
показывает скелет вместо уже известного старого числа — ни один автотест
|
|
здесь не покраснеет, регрессия обнаружится только ручным глазом на
|
|
следующей задаче, ровно как было с #234 (не этот баг конкретно, но тот же
|
|
класс: незамеченная регрессия смежного поведения, которую в этом процессе
|
|
призвана ловить именно связка «AC + автотест»).
|
|
|
|
**Как чинится:** добавить AC (или явно расширить формулировку одного из
|
|
существующих, например AC1), проверяющий: `cfgEpoch` меняется →
|
|
`deviceMemo`/`areaMemo` уже когда-то были посчитаны → `value()`/рендер строки
|
|
показывает старое число, а не скелет, пока не готово новое. Разумный
|
|
свидетель — unit на `metrics()`/`value()` с последовательностью
|
|
{вычислено → `cfgEpoch++` → запрошено снова до завершения асинхронного
|
|
пересчёта}.
|
|
|
|
### Low-1 (не блокирует, для протокола) — критерий отказа от лёгкого трека не назван явно
|
|
|
|
PROCESS.md §2.2 п.8 требует при выборе полного трека называть конкретный
|
|
критерий §5, который задача не проходит («обычный трек» без названного
|
|
критерия обоснованием не является). S2-комментарий переходит в `S3-spec`
|
|
без такой явной фразы (в отличие от прецедента `SPEC-REVIEW-506-r1`, где
|
|
аналитик прямо процитировал «критерий small «нет влияния на
|
|
производительность» заведомо не выполнен»). По существу критерий
|
|
действительно не пройден — вся задача измеряет и бюджетирует
|
|
производительность (AC3–AC5, таблица «Чем краснеет», мутанты на перф-путь),
|
|
и никакого другого вывода из содержания ТЗ сделать нельзя, поэтому это
|
|
дефект формы аналитического комментария, а не двусмысленность самого ТЗ.
|
|
Не блокирует зелёный вердикт по этой задаче; отдельного действия не требует
|
|
(снимается записью). Упоминается ради единообразия с прецедентами #506/#505.
|
|
|
|
## 4. Что проверено и корректно
|
|
|
|
- **Формат этапа:** полный трек, ТЗ — в теле issue, что верно для задач,
|
|
открытых после решения #517 (2026-09-10); ссылки issue↔ТЗ на месте
|
|
(единственный документ и есть issue).
|
|
- **Персона и SCOPE.md:** «житель дома» (Household members) на View/kiosk —
|
|
укладывается в J1 («live spatial overview») напрямую; никакого конфликта с
|
|
`docs/SCOPE.md` не найдено, задача не расширяет функциональность, а
|
|
устраняет деградацию отзывчивости и вводящий в заблуждение текст ошибки.
|
|
- **Технический диагноз подтверждён чтением, не переписан со слов автора**:
|
|
все пять цитируемых мест кода (`value()`, `metrics()`,
|
|
`totalCleanFloorAreaM2`, `_innerRoomContour`, `SummaryPanelPresentation`)
|
|
проверены построчно на `dev` `d204bf5d` и совпадают с описанием ТЗ и
|
|
S2-комментария; числа (11 045 → 1 488 мс, 306.3 м²) взяты из измерения
|
|
того же комментария, а не изобретены заново в ТЗ.
|
|
- **AC3 технически реализуем** заявленным способом (общие экспортируемые
|
|
функции, `innerContourForRoom` уже принимает shared-параметры) — не
|
|
гипотеза без опоры на код.
|
|
- **AC5 не выдумывает новую фичу**: #505 (анимация 190 мс) уже смёржена и
|
|
закрыта, AC5 проверяет, что расчистка главного потока (пп. 3–4) даёт этой
|
|
уже существующей анимации реально отрисоваться, а не изобретает
|
|
анимацию заново.
|
|
- **i18n:** новых ключей действительно нет, `summary.unavailable` — реальный
|
|
существующий ключ на 4 языках; заявление ТЗ точное.
|
|
- **Модель данных / миграция / touch:** корректно помечены «не меняются» /
|
|
«не относится» — правка не касается конфигурации, хранения или touch-целей.
|
|
- **Риски** называют реальный источник флейка (порог AC4 меряется на CI с
|
|
запасом, не сравнивается между машинами) — соответствует уже принятому в
|
|
проекте паттерну perf-смоков (`PROCESS.md` §8).
|
|
- **Откат** тривиален и точен: правка локальна для `summary-panel-*` плюс
|
|
тесты, данных и миграций нет.
|
|
- Продуктовых вопросов владельцу, кроме уже заданного и закрытого дефолтом
|
|
(плейсхолдер = вариант A), в тексте не осталось; ни одна догадка о
|
|
видимом поведении не подана как факт без пометки (единственная зона
|
|
неопределённости — п.4/AC-разрыв — разобрана явно как находка, а не как
|
|
скрытая догадка).
|
|
|
|
## 5. Чего не проверял
|
|
|
|
- Не прогонялись `typecheck`/`test`/`build`/`process-gate` — кода в
|
|
материале нет, ветки `issue/509-*` не существует; это гейты этапа `code`,
|
|
не `spec`.
|
|
- Не запускались браузерные смоки/`golden`/`performance_smoke` — на этапе
|
|
ТЗ реализации не существует; они станут обязанностью код-ревью, когда
|
|
появятся файлы `demo/smoke_*`, названные в AC1/AC4/AC5/AC6.
|
|
- Не проверялась синтаксическая корректность будущего кода
|
|
(`requestIdleCallback`-план, структура нового состояния «прежнее
|
|
значение») — его ещё не существует; оценивалась только согласованность
|
|
плана с уже существующим кодом, на котором план основан.
|
|
- Не оценивалось качество будущих мутационных гардов из таблицы «Чем
|
|
краснеет» сверх того, что они названы и привязаны к конкретным функциям —
|
|
реализация появится в коде.
|
|
- Не проверялся сам механизм подсчёта хеша тела issue в блоке якорей
|
|
документа (`scripts/review-doc-guard.mjs`/`process.yml`, #517) — это
|
|
инфраструктура конвейера, а не предмет ревью ТЗ конкретной задачи.
|
|
|
|
## 6. Вердикт
|
|
|
|
High: 0. Medium: 2, обе в скоупе задачи, чинятся правкой того же текста без
|
|
нового технического анализа (обе опираются на уже написанные автором факты
|
|
и код, который я перепроверил). Low: 1, не блокирует, снимается записью.
|
|
|
|
Обязательные разделы присутствуют по существу; технические утверждения о
|
|
текущем коде подтверждены чтением, а не приняты на слово; единственный
|
|
реальный пробел — контракт описывает защиту от повторного мигания при
|
|
инвалидации мемо (ровно то, из-за чего заведена задача), но не проверяет её
|
|
ни одним AC, и один из технических путей реализации (микрозадача vs
|
|
`requestIdleCallback`) оставлен как «или-или» без решения и без пометки
|
|
«на усмотрение реализации».
|
|
|
|
Вердикт: жёлтый · заход r1 · блокирующих циклов 1/4 · High: 0 · Medium: 2 → в задаче
|
|
|
|
---
|
|
|
|
<!-- material-anchors: сгенерировано конвейером (#414) -->
|
|
|
|
## Материал раунда
|
|
|
|
- Ветка: `dev`, коммит `d204bf5d7737` — ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет.
|
|
- Дерево материала: `cf36bd3b2a3141c427a40f9e10aac607faf02a1f`
|
|
```
|
|
git log --all --format='%H %T' | grep cf36bd3b2a31
|
|
```
|
|
- Тело issue: `8d514ac196bbc453d5b25b352421dc27f9925b8668a89e44c00971f304f2c3e4`
|
|
- Вердикт конвейера: `yellow` · High 0
|