Files
houseplan-card/docs/reviews/SPEC-REVIEW-535-r1.md
T
2026-09-11 21:42:04 +00:00

281 lines
26 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# SPEC-REVIEW-535-r1
**Issue:** #535 — «Страница панели грузит карточку по URL без версии: плашка о несовпадении версий переживает перезагрузку на /houseplan»
**Этап:** S4-spec-review (ревью ТЗ, PROCESS.md §2.4)
**Заход:** r1 · блокирующих циклов израсходовано 0 из 4
**Ревьюер:** Claude (Sonnet 5), роль «ревьюер ТЗ»
**Автор ТЗ:** Matysh (в теле issue, раздел `## ТЗ`)
## Материал раунда
- Тело issue #535, раздел `## ТЗ` (включая «Продуктовая рамка», К1–К6, «Принято
предположительно», таблица AC1–AC6, «Риски», «Затронутые файлы», «Откат»).
- SHA256 нормализованного тела issue (как получено через `gh issue view --json body`,
без дополнительной нормализации переносов строк):
`77204930eaea05482f2a7da2bac3af79247e7d7356e0449551cb997da2c76fc8`
- Оба комментария issue (S2-аналитика, «ТЗ готово — на ревью»).
- Код на момент ревью: `dev` @ `1ca3b5fae3e2df9fbd41ead35d98c8d58c87f5b8` (рабочая
копия чиста, HEAD == origin/dev). Читался только для проверки выполнимости и
однозначности ТЗ, продуктовый код не менялся.
## Скоуп ревью
Проверялись: обязательные разделы ТЗ (PROCESS.md §7.1), однозначность и
проверяемость каждого AC, наличие способа доказательства и «чем краснеет»,
отсутствие невыделенных догадок о поведении, соответствие `docs/SCOPE.md`,
согласованность контракта К1–К6 с текущей реализацией `entryFallbackPlugin`
(`scripts/bundle-manifest.mjs`) и существующими тестами `#486` в
`test/bundle-assets.test.mjs`, а также спорный вопрос классификации трека,
который автор прямо адресовал ревьюеру.
## Как проверялось
1. Прочитан `docs/SCOPE.md` — задача не вводит новую функциональность, это
правка доставки уже существующего продукта (сходится по духу с J6 «Keep the
plan true», хотя формально это баг доставки бандла, а не пользовательской
фичи — отдельного пункта J-карты не требует).
2. Прочитан `PROCESS.md` §1 (границы классов файлов), §2.4, §2.5 (DoR-чеклист,
чтобы понять, что должно быть закрыто до перехода в `S5-ready`), §7.1
(обязательные разделы ТЗ), §7.2 (формат вердикта).
3. Прочитано тело issue #535 целиком и оба комментария.
4. Прочитан текущий код `entryFallbackPlugin` в `scripts/bundle-manifest.mjs`
(строки 300–386) — как сегодня панель переписывается на импорт фасада, как
считается `imports`/`initialPanelFiles`/`initialPanelOnlyGzipBytes`
(`reachable()`, `initialPanelOnly`).
5. Прочитаны оба существующих теста `#486` в `test/bundle-assets.test.mjs`
(`both stable entries install a visible stale-load fallback`,
`panel entry routes through the card facade and fails loudly too`), чтобы
проверить, действительно ли их утверждения пиннуют именно фасадную
маршрутизацию (К5) и действительно ли они «переворачиваются» предложенной
правкой, а не остаются случайно истинными.
6. Прочитан фрагмент `docs/ARCHITECTURE.md` про два стабильных входа
(строки 69–86) — проверить, не противоречит ли К3/АС6 уже
зафиксированному описанию маршрутизации.
7. Прослежена логика графа вручную (`reachable()` от `entry`=facade и от
`panelEntry`), чтобы проверить арифметику риска №2 (бюджет) и корректность
К4/AC4 до и после предложенной правки.
Гейты (typecheck/test/build/golden/backend) не гонялись — это этап ревью ТЗ,
продуктовый код ещё не написан; §2.4 их не требует.
## Разбор по существу
### Обязательные разделы ТЗ (§7.1)
Присутствуют по содержанию, хотя не всегда под буквальными заголовками:
| Раздел §7.1 | Есть? | Где |
|---|---|---|
| Сценарий | ✅ | «Продуктовая рамка»: персона — домочадцы и администратор, поверхность — пункт меню House Plan |
| Что человек увидит до/после | ✅ | «До.» / «После.» |
| Проблема | ✅ | Симптом + «Причина найдена и измерена» + «Как это даёт ровно наблюдаемое поведение» в теле issue до ТЗ |
| Скоуп и не-скоуп | ⚠️ частично | Не-скоуп явно перечислен в комментарии S2 («Чего в этой задаче нет»: формулировка плашки, #536, заголовки прокси) и повторён в комментарии «ТЗ готово», но **не продублирован внутри самого раздела `## ТЗ`** |
| Контракт поведения | ✅ | К1–К6 |
| UX | ✅ | К2 (текст фолбэка дословно, 4 языка, класс элемента) |
| Модель данных и миграция | ⚠️ подразумевается | Нет явного «миграция: нет»; из контекста (только `scripts/**`, `test/**`, документация, три копии бандла) очевидно, что миграции нет |
| i18n | ✅ (по факту «нет новых ключей») | К2: текст фолбэка не меняется ни в одном из 4 языков |
| AC1…ACn с доказательством | ✅ | Таблица AC1–AC6, у каждого «чем доказан» и «чем краснеет» |
| План автотестов | ✅ | Встроен в таблицу AC (имена тестов/файлов) |
| Риски | ✅ | 4 пункта |
| Откат | ✅ | Один абзац, простой и точный |
| Release-артефакты | ✅ | Названы в преамбуле ТЗ: `docs/ARCHITECTURE.md`, оба changelog |
| Влияние на touch | ❌ не названо явно | Задача не трогает ни один UI/editor/touch-контракт (только сборочный конвейер и панельный/карточный загрузчик); фактическое влияние — «нет», но фраза отсутствует |
Два ⚠️/❌ пункта (модель данных/миграция и touch — явные «нет» не написаны)
разбираю ниже как находки Low.
### Техническая состоятельность контракта (К1–К6) и AC
Проверил код `entryFallbackPlugin` вручную, чтобы понять, действительно ли
предложенная правка (импортировать `cardAsset` напрямую вместо фасада,
сохранив `try/catch`) реализуема ровно так, как описано, и действительно ли
существующие тесты `#486` меняют смысл, а не остаются проходить случайно.
- **К1/AC1/AC2 реализуемы буквально.** Сегодня `panelEntry.code` переписывается
regex'ом `panelPattern` (совпадающим с уже существующим статическим импортом
`cardAsset`, который туда кладёт сам Rollup) на
`try{await import("./houseplan-card.js")}catch{...}`, и `panelEntry.imports`
жёстко ставится в `[CARD_ENTRY_FILE]` (`scripts/bundle-manifest.mjs:377–383`).
Предложенная правка меняет ровно эти две строки: подстановка в `try{await
import("${cardAsset}")}...` и `panelEntry.imports = [cardAsset]`. Никакого
скрытого усложнения нет — это буквально «убрать один слой переадресации»,
как и заявлено в «Откате».
- **К5/тесты `#486` действительно переворачиваются, а не остаются случайно
зелёными.** Прочитал оба теста построчно (`test/bundle-assets.test.mjs:305–360`
и `:522–546`). Первый матчит `panel` на `try{await
import("./houseplan-card.js")}catch` и `bundle['houseplan-panel.js'].imports
=== ['houseplan-card.js']` — оба утверждения станут ложными на новом коде и
должны замениться на `.../houseplan-assets/card-HASH.js` и
`['houseplan-assets/card-HASH.js']` соответственно, что ровно и обещает AC1.
Второй тест читает **собранный** `dist/houseplan-panel.js` и матчит ту же
строку `./houseplan-card.js` — тоже переворачивается, ровно как обещает AC2
(«проверка по `dist`, а не по плагину»).
- **К4/AC4 — риск №1 в ТЗ корректно предсказывает реальную поломку
существующего утверждения о манифесте**, а не выдуманную. Прошёл вручную
логику `reachable()`: сейчас `initial` = reachable(entry=`houseplan-card.js`)
включает сам фасад как корень; `initialPanel` = reachable(panel) = {panel} ∪
`initial`, потому что `panelEntry.imports=[CARD_ENTRY_FILE]`. Отсюda
`initialViewFiles.every(p => initialPanelFiles.includes(p))` истинно
тривиально. После правки `panelEntry.imports=[cardAsset]`, и `initialPanel`
перестаёт включать сам файл фасада (`houseplan-card.js`) — утверждение в
строке 542 текущего теста station станет **ложным**, именно это и называет
риск №1 ТЗ («написано под фасад... надо переформулировать через граф
реализации»). Это открытое место корректно помечено как риск, а не выдано за
решённое — соответствует правилу «размытое место выносится, а не
додумывается». Как именно переформулировать сравнение (через граф от
`cardAsset`, а не от `entry`) — техническая деталь реализации, которую §7.1
прямо отдаёт исполнителю («агенты решают сами... стратегия тестов»), поэтому
не считаю это находкой ревью ТЗ.
- **Риск №2 (бюджет) арифметически неточен, но не вреден.** По той же
логике `initialPanelOnlyGzipBytes` = `initialPanel − initial` — множество,
которое **и до, и после** правки состоит только из самого файла панели
(фасад всегда был частью `initial`, а не эксклюзивным для панели), то есть
сам метрический бюджет `initialPanelOnlyGzipBytes`, скорее всего, не
сдвинется на «размер фасада», сдвинется общий размер `initialPanelFiles`
(граф целиком). Это неточность в обосновании риска, а не в контракте: ТЗ уже
предусматривает митигацию («рекалибруется с датированной записью, а не тихо
поднимается») независимо от того, какая метрика реально изменится и на
сколько. AC4 всё равно остаётся проверяемым (`bundle-budget.mjs` плюс
утверждения теста), поэтому не блокирую — но называю ревьюеру кода: при
проверке AC4 стоит явно свериться, какая метрика в итоге изменилась, и что
это совпадает с тем, что записано в риске/changelog.
- **К3 (фасад карточки не трогается) не имеет отдельного AC, и это
корректно** — правка физически не касается кода, рисующего `cardEntry.code`
(строки 344–358 `bundle-manifest.mjs`), и существующая (переписываемая, но не
удаляемая) часть тех же двух тестов `#486`, которая проверяет
карточную сторону, продолжает быть тем самым свидетелем регрессии.
- **AC3 (фолбэк дословно)** — проверил, что механизм построения текста
(`fallbackDefinition`) вообще не тронут К1; он используется одинаково для
обоих контрактов и не зависит от того, что импортируется. Корректно.
- **AC5 (общедистрибутивный инвариант)** — новый тест, скоуп ясен
(«ни один входной файл не ссылается на карточку без версии» по `dist/*.js`),
однозначен и проверяем.
- **AC6** помечен как доказываемый «ревьюером кода построчно» — это
недефенсивный AC (расположение текста в документации), правило §2.7 про
таблицу «чем краснеет» на него не распространяется по собственной оговорке
процесса.
### Классификация трека — вопрос, адресованный ревьюеру
Автор прямо просит решить: инфраструктурная это задача (по механическому
признаку §1 класса файлов — да, только `scripts/**`/`test/**`/документация) или
полный трек (по факту видимого поведения). Довод автора: на устаревшем
загрузчике сегодня пользователь **молча** получает старую карточку; после
правки он **всегда** увидит громкий фолбэк в тех случаях, где раньше молча
работал (или не работал вовсе) старый код. Проверил механику вручную
(см. выше) — это не гипотеза: до правки возможен путь, где старый `houseplan-panel.js`
из кэша браузера подтягивает **свежий** фасад `houseplan-card.js` (тоже без
версии, тоже подверженный эвристике, но не обязательно ровно такой же
устаревший), который в свою очередь тянет уже новый чанк — рабочая, но
рассинхронизированная версия без единой ошибки. После правки этот путь
исчезает: старый панельный вход всегда просит старый (и, по контракту сервера
из `docs/ARCHITECTURE.md:82–84`, отсутствующий после деплоя) чанк напрямую и
надёжно показывает фолбэк. Это действительно новый наблюдаемый контракт (не
новый текст, но новое условие срабатывания), и довод автора о нарушении
критерия §5 «нет нового UX-контракта» подтверждается чтением кода, а не
принимается на веру. **Соглашаюсь с автором: полный трек оправдан.** Это
решает технический спор в пользу продолжения ревью, как и предписывает §7.1
(«технический спор автора и ревьюера решается вердиктом»).
### Проверка «не выдана ли догадка за решение»
Прошёлся по всем формулировкам поведения на предмет непомеченных допущений:
- Утверждение «загрузчик выбирает версию всей карточки» и таблица заголовков
кэша — это измерение на боевом стенде (проверяемый факт, не догадка).
- «Через какое-то время окно истекает... и всё само чинится» — помечено как
объяснение уже наблюдавшегося поведения («не всегда», «точных шагов не
нашёл»), не выдаётся за новый AC.
- Три пункта «Принято предположительно» корректно помечены как техническое
предположение, которое можно менять свободно, включая явную причину отказа
от вариантов B и C.
- Риск №1 и №2 корректно поданы как риски, а не как решённые факты (см. выше).
Непомеченных догадок о поведении, которого нет ни в одном документе, не нашёл.
## Находки
Блокирующих (High) находок нет. Находок Medium в скоупе или вне скоупа нет.
### Low (сняты решением ревьюера, без правки)
1. **Не-скоуп не продублирован внутри `## ТЗ`.** Перечень «чего в этой задаче
нет» (формулировка плашки, побочная находка #536, заголовки прокси) есть в
комментарии S2-аналитики и повторён в комментарии «ТЗ готово», но не
переписан внутри самого раздела ТЗ в теле issue. Содержательно вопрос
закрыт и однозначен — при чтении всего issue целиком путаницы не возникает,
поэтому не считаю это основанием для возврата. Снимаю с записью: для
исполнителя это не создаёт риска, потому что ветка/PR будут ссылаться на тот
же issue целиком.
2. **Явное «миграция: нет» и «touch: нет» не написаны отдельной строкой**, как
того просит чек-лист DoR §2.5. По содержанию задачи (только
`scripts/**`/`test/**`/документация/три копии бандла, никакого нового
конфига, никакого UI/editor-контракта) оба пункта очевидно «нет». Снимаю
с записью: не считаю нужным гонять задачу на ещё один цикл ради двух строк
текста; ревьюер кода или сам автор могут дописать их при переводе в
`S5-ready` без нового цикла ревью ТЗ.
3. **Риск №2 (бюджет) арифметически не точен** (см. разбор AC4 выше): скорее
всего сдвинется не `initialPanelOnlyGzipBytes`, а общий размер
`initialPanelFiles`. Митигация в риске («рекалибруется явно, а не тихо»)
покрывает оба случая одинаково, поэтому не блокирую, но передаю ревьюеру
кода как пункт для проверки при разборе AC4.
## Что проверено и корректно
- Все обязательные по существу элементы §7.1 присутствуют (сценарий, что
человек увидит, проблема, контракт, AC с доказательством, риски, откат,
release-артефакты).
- Каждый AC1–AC6 однозначен, имеет названный способ доказательства и (где
применимо как защитный) — «чем краснеет».
- К1/К5 реализуемы буквально существующим кодом `entryFallbackPlugin`;
проверил вручную, что оба переписываемых теста `#486` действительно меняют
истинностное значение своих утверждений на новом коде, а не остаются
случайно зелёными.
- К4/риск №1 корректно определяет реальную поломку существующего утверждения
о манифесте (арифметика `reachable()` проверена вручную) и корректно
оставляет способ переформулировки на усмотрение реализации.
- Классификация полного трека, оспоренная самим автором, подтверждена
независимым разбором механики: изменение действительно вводит новое условие
срабатывания видимого фолбэка, а не только смену текста.
- Непомеченных догадок о недокументированном поведении не найдено.
- Названные затронутые файлы (`scripts/bundle-manifest.mjs`,
`test/bundle-assets.test.mjs`, `scripts/mutation-gate.mjs`,
`docs/ARCHITECTURE.md`, оба changelog, три копии бандла) соответствуют
реальному расположению кода, который придётся менять.
## Чего не проверял
- Гейты (`typecheck`/`test`/`build`/golden/backend) — не требуются на этапе
ревью ТЗ (§2.4), кода ещё нет.
- Реальный прогон сборки/тестов с внесённой правкой (её не существует; это
этап кода-ревью).
- Формулировку самой плашки о несовпадении версий и побочную находку про
`disconnect()`/`_hideBanner()` — явно вне скоупа этой задачи (вынесены
автором в отдельный разговор и в #536 соответственно), не проверял по
существу.
- Заголовки прокси/reverse-proxy перед Home Assistant — вне скоупа по тексту
ТЗ, не проверял.
- `scripts/mutation-gate.mjs` — не проверял, существует ли уже похожий по духу
мутант или конфликт имени `panel-imports-unversioned-card-entry`; это
деталь реализации, а не предмет ревью ТЗ.
## Вердикт
Вердикт: зелёный · заход r1 · блокирующих циклов 0/4 · High: 0 · Medium: 0 → в задаче
Issue #535 переходит в `S5-ready`.
---
<!-- material-anchors: сгенерировано конвейером (#414) -->
## Материал раунда
- Ветка: `dev`, коммит `1ca3b5fae3e2` — ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет.
- Дерево материала: `3721f5a271916bfb2cc467947132525042363285`
```
git log --all --format='%H %T' | grep 3721f5a27191
```
- Тело issue: `77204930eaea05482f2a7da2bac3af79247e7d7356e0449551cb997da2c76fc8`
- Вердикт конвейера: `green` · High 0