26 KiB
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, а также спорный вопрос классификации трека,
который автор прямо адресовал ревьюеру.
Как проверялось
- Прочитан
docs/SCOPE.md— задача не вводит новую функциональность, это правка доставки уже существующего продукта (сходится по духу с J6 «Keep the plan true», хотя формально это баг доставки бандла, а не пользовательской фичи — отдельного пункта J-карты не требует). - Прочитан
PROCESS.md§1 (границы классов файлов), §2.4, §2.5 (DoR-чеклист, чтобы понять, что должно быть закрыто до перехода вS5-ready), §7.1 (обязательные разделы ТЗ), §7.2 (формат вердикта). - Прочитано тело issue #535 целиком и оба комментария.
- Прочитан текущий код
entryFallbackPluginвscripts/bundle-manifest.mjs(строки 300–386) — как сегодня панель переписывается на импорт фасада, как считаетсяimports/initialPanelFiles/initialPanelOnlyGzipBytes(reachable(),initialPanelOnly). - Прочитаны оба существующих теста
#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) и действительно ли они «переворачиваются» предложенной правкой, а не остаются случайно истинными. - Прочитан фрагмент
docs/ARCHITECTURE.mdпро два стабильных входа (строки 69–86) — проверить, не противоречит ли К3/АС6 уже зафиксированному описанию маршрутизации. - Прослежена логика графа вручную (
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]. ОтсюdainitialViewFiles.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–358bundle-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 (сняты решением ревьюера, без правки)
- Не-скоуп не продублирован внутри
## ТЗ. Перечень «чего в этой задаче нет» (формулировка плашки, побочная находка #536, заголовки прокси) есть в комментарии S2-аналитики и повторён в комментарии «ТЗ готово», но не переписан внутри самого раздела ТЗ в теле issue. Содержательно вопрос закрыт и однозначен — при чтении всего issue целиком путаницы не возникает, поэтому не считаю это основанием для возврата. Снимаю с записью: для исполнителя это не создаёт риска, потому что ветка/PR будут ссылаться на тот же issue целиком. - Явное «миграция: нет» и «touch: нет» не написаны отдельной строкой, как
того просит чек-лист DoR §2.5. По содержанию задачи (только
scripts/**/test/**/документация/три копии бандла, никакого нового конфига, никакого UI/editor-контракта) оба пункта очевидно «нет». Снимаю с записью: не считаю нужным гонять задачу на ещё один цикл ради двух строк текста; ревьюер кода или сам автор могут дописать их при переводе вS5-readyбез нового цикла ревью ТЗ. - Риск №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.
Материал раунда
- Ветка:
dev, коммит1ca3b5fae3e2— ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет. - Дерево материала:
3721f5a271916bfb2cc467947132525042363285git log --all --format='%H %T' | grep 3721f5a27191 - Тело issue:
77204930eaea05482f2a7da2bac3af79247e7d7356e0449551cb997da2c76fc8 - Вердикт конвейера:
green· High 0