docs: review document for #535

Issue: #535
User-Visible: no
This commit is contained in:
claude[bot]
2026-09-11 21:42:04 +00:00
parent 1ca3b5fae3
commit a52dcab77a
+280
View File
@@ -0,0 +1,280 @@
# 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