# 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`. --- ## Материал раунда - Ветка: `dev`, коммит `1ca3b5fae3e2` — ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет. - Дерево материала: `3721f5a271916bfb2cc467947132525042363285` ``` git log --all --format='%H %T' | grep 3721f5a27191 ``` - Тело issue: `77204930eaea05482f2a7da2bac3af79247e7d7356e0449551cb997da2c76fc8` - Вердикт конвейера: `green` · High 0