Files
houseplan-card/docs/reviews/SPEC-REVIEW-434-r1.md
T
Codex d4dd027b0a build: prepare v1.71.0-beta.2 candidate
Issue: #426
Issue: #427
Issue: #428
Issue: #431
Issue: #432
Issue: #434
User-Visible: no
2026-09-03 15:23:40 +03:00

187 lines
19 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-434-r1
- Issue: https://github.com/Matysh/houseplan-card/issues/434
- Этап: ревью ТЗ (PROCESS.md §2.4)
- ТЗ: `docs/specs/434-v171-polish-audit.md`, проверяемый SHA автора `5566d6f9898e3c6d21f3e92a2ddf94536c86edc4`
- Заход: r1 · блокирующих циклов израсходовано 0 из 4 (полный трек, лимит 4)
- Вердикт: **жёлтый**
## Скоуп
Follow-up к аудиту v1.71.0-beta.1 (`AUDIT-2026-09-03-v1710beta1.md` §3.3): девять
независимых мелких дефектов на нескольких поверхностях — физический учёт/удаление
orphan decor-blob'ов, capability guard в `houseplan-space-card`, revision-scoped
resolve cache, честный `reused`, актуальность locale gate в danger confirmation и
симметричная отмена, отрицательный свидетель Area-snapshot cleanup, bounded
smoke-выполнение (per-route и per-file timeout), отзыв support preview token.
Маршрут — полный (обоснование в ТЗ: несколько независимых контрактов, хранение
пользовательских файлов, rolling compatibility, асинхронный safety lifecycle,
несколько независимых гейтов — сложность выше лимита `small`); обоснование
корректно, критерий лёгкого трека действительно не проходит.
## Как проверялось
Прочитаны: `docs/SCOPE.md`, `AGENTS.md`, `PROCESS.md` §1–§8, §12; тело issue #434
и оба комментария (аналитика + хендофф автора); ТЗ целиком (511 строк);
`docs/CONFIG-COMPATIBILITY.md`, `docs/ARCHITECTURE.md` (разделы про
content-addressed decor store), `docs/SUPPORT-PRIVACY.md`; связанные issue #417,
#419, #432 (включая финальный код-ревью #432 для контекста integrity-кэша) и
issue #435 (правило «таблица чем краснеет»).
Поскольку ТЗ на 90% состоит из утверждений о **текущем** поведении кода
(«Подтверждённые причины»), а не только из предложений на будущее, каждое из
девяти утверждений было сверено построчно с `origin/dev` — своей и параллельным
агентом (независимая перепроверка, без пересечения выводов до сравнения):
| № | Утверждение ТЗ | Файл:строка на `origin/dev` | Результат |
|---|---|---|---|
| 1 | `read_catalog()` обходит только `*.json`, `_read_catalog_row()` требует `blob.is_file()` | `decor_assets.py:407`, `:388` | подтверждено дословно |
| 2 | `HpConfigSnapshot` не переносит `decor_assets_api`; `space-card.ts` вызывает resolve безусловно; `houseplan-card.ts:4306` — с гардом | `config-store.ts:21-29` (нет поля); `space-card.ts:732`; `houseplan-card.ts:4306-4312` | подтверждено дословно |
| 3 | `resolveCache` — одна пара id-set→Map на connection, без ревизии config | `decor-assets.ts:23,88-99` | подтверждено дословно |
| 4 | `test_catalog_ignores_missing_or_malformed_sidecars` не проверяет «валидный sidecar, blob отсутствует» | `tests_backend/test_decor_assets.py:318-325` | подтверждено, кейс в файле отсутствует |
| 5 | Recovery-ветка возвращает `reused:true` без предшествующей catalog-записи | `http_api.py:305-316` (условие `if blob.exists()` → `return row, True`) | подтверждено; фактический `return` на 5 строк ниже цитируемого диапазона — не искажает смысл |
| 6 | `_dangerConfirmLocaleGate` — снимок прошлого рендера; открытый confirm не отменяется при переходе в `warm` | `houseplan-card.ts:2161` (поле), `:2183` (чтение), `:11228` (запись только в `_renderBody()`); ни один из `_cancelDangerConfirm()` call-sites (`:1607,2794,4188,7385,7446`) не привязан к смене locale gate | подтверждено; номера строк ТЗ приблизительные (±1–3), сама механика верна и явно помечена в ТЗ как ориентировочная |
| 7 | `snapshotBindings.has(binding)` в `resolveAreaSnapshotCleanup()` не имеет отдельного отрицательного теста | `device-area-relocation.ts:171`; `test/device-area-relocation.test.mjs` | подтверждено, все существующие тесты либо берут binding из того же snapshot, либо используют пустой снапшот |
| 8 | `germanStarted`/`germanCompleted` асимметричны; отдельный smoke-файл ограничен лишь job-таймаутом | `demo/smoke_danger_confirm_branches.mjs:79-84` — подтверждено дословно | **см. находку ниже** — цифра «20 минут» в самой ТЗ неверна |
| 9 | `_buildSupportPreview()` бросает `support_rejected` до `_discardSupportPreview()` при валидном token, но невалидном другом поле | `houseplan-editor-runtime.ts:9190-9199` (throw), `:9226-9233` (catch без discard) | подтверждено дословно |
Дополнительно проверено: `docs/specs/README.md` — двусторонняя ссылка issue ↔ ТЗ
на месте (строка 183); `scripts/mutation-gate.mjs` уже содержит мутанты для
Python-файлов бэкенда (прецедент для AC1–AC4); `test/validate-workflow.test.mjs`
— прецедент text-based контрактного теста над `.github/workflows/*.yml` (годится
для AC9); `docs/CONFIG-COMPATIBILITY.md` раздел «Custom decor images…» подтверждает,
что #432 не менял схему/URL/capability — согласуется с разделом ТЗ «Модель
данных»; `docs/ARCHITECTURE.md` подтверждает content-addressed модель
(`<64 hex>.<ext>`, sidecar JSON), на которой строится вся глава AC1–AC4; `reused`
нигде не читается в `src/**` — уточнение его семантики действительно не является
изменением публичного/видимого контракта.
## Находки
### [Medium, в скоупе] Неверная цифра «20-минутный timeout» job `smoke` — фактическая ошибка, а не предположение
**Где:** ТЗ, «Подтверждённые причины» п.8; раздел «Контракт поведения» §7
(«Глобальный `timeout-minutes: 20`, детерминированное разбиение… не меняются»);
раздел «Производительность» («не уменьшает 20-минутный общий бюджет job»);
раздел «Риски» («сохраняет глобальные 20 минут»).
**В чём дефект:** в `.github/workflows/validate.yml` `timeout-minutes: 20`
принадлежит **другой** job — `performance_smoke` (строка 715). Job `smoke`
(объявлена на строке 486, шаг цикла `for f in demo/smoke_*.mjs` — строка 554)
**не имеет собственного `timeout-minutes` вообще** — в файле ровно одно
вхождение слова `timeout`, и это не она. Без явного значения GitHub Actions
использует дефолт 360 минут, а не 20. Сам issue #434 в исходной формулировке
пункта (з) написан точно: «у smoke-job в `validate.yml` своего
`timeout-minutes` тоже нет» — то есть корректный факт был в issue, а при
переносе в ТЗ он превратился в конкретную (неверную) цифру, не помеченную как
предположение.
**Почему это находка, а не мелочь:** это утверждение — не проходной
комментарий, а часть контракта, который ТЗ прямо объявляет неизменным
(«не меняются», «не уменьшает»). Реализатор, доверяющий тексту ТЗ, будет
считать, что job уже ограничена 20 минутами, и не задаст себе вопрос, нужно ли
явно выставить `timeout-minutes` на job `smoke` в рамках этой же задачи (item
8/AC9 «bounded execution» — ровно про то, чтобы ни один smoke не мог удерживать
раннер бесконечно). Сейчас после фикса по-прежнему не будет верхней границы на
уровне job — только на уровне отдельного файла (180 c + 10 c kill grace,
максимум ~63 файла), что для 3 шардов и текущего числа смоков даёт время
исполнения, которое ТЗ не оценивает и не ограничивает.
**Воспроизведение:** `grep -n timeout .github/workflows/validate.yml` →
единственное совпадение на строке 715 внутри job `performance_smoke` (строки
705–781); job `smoke` — строки 486–586, `timeout-minutes` в её теле нет.
**Что нужно исправить:** либо (a) явно написать в ТЗ, что у job `smoke`
сейчас нет собственного ограничения (дефолт GitHub 360 минут), и explicit
решить/зафиксировать — фиксируется ли `timeout-minutes` на уровне job этой же
задачей, либо остаётся полагаться только на per-file guard; либо (b) если
решение «job-level timeout не трогаем» осознанное — убрать из «Контракта» и
«Рисков» формулировки, которые ссылаются на несуществующие «текущие 20 минут»
как на неизменную величину. Это техническое решение (§7.1: «где хранится
состояние» — техническое, «что видит пользователь» — нет), поэтому чинится
автором ТЗ без обращения к владельцу.
Без High-находок это жёлтый вердикт: находка в скоупе задачи (это тот же раздел
7/AC9, который задача и меняет), правится в этом же ТЗ, повторный цикл — код не
пишется до зелёного ревью ТЗ.
## Что проверено и корректно
- Обязательные разделы §7.1 присутствуют все: сценарий, что человек увидит
до/после, проблема («Подтверждённые причины»), скоуп и не-скоуп, контракт
поведения (8 подпунктов), UX/accessibility/touch/kiosk/i18n, модель данных и
совместимость, критерии приёмки AC1–AC12 с указанием способа доказательства,
план автотестов (10 шагов), риски, откат, release-артефакты.
- Продуктовые первые два раздела отвечают на оба обязательных вопроса:
персона/поверхность/момент (Home admin, оба редактора и обе карточки,
штатная эксплуатация + редкий аварийный останов HA) и что видно до/после —
без терминов реализации.
- Из девяти пунктов «Подтверждённые причины» восемь с половиной проверены
дословно точным построчным совпадением с `origin/dev` (см. таблицу выше);
ни одна из них не оказалась догадкой, выданной за факт. Автор явно и честно
пометил номера строк как ориентировочные там, где они действительно немного
разошлись (п.6), и отдельным блоком «Принятые технические предположения» —
все решения, которые не являются продуктовыми и не требуют владельца.
- Таблица «чем краснеет» (#435) заполнена для всех десяти AC без пустых
третьих столбцов; для AC5/AC6 (чистые фронтенд-юниты) корректно применена
льгота §2.7 «для чистых юнитов достаточно прогона со снятой защитой» вместо
обязательного постоянного мутанта — автор явно прочитал и применил именно
эту оговорку, а не общее правило.
- Не-скоуп корректно исключает смежные, но более крупные работы: полный
рефакторинг LanguageRuntime/support pipeline/smoke-шардирования, повышение
`decor_assets_api`/schema/export version, изменение видимого текста/UI.
Это не даёт задаче расползтись за пределы девяти найденных дефектов.
- Изменение семантики `reused` не является изменением видимого/публичного
контракта: поле нигде не читается в `src/**` (проверено grep), так что
уточнение не требует продуктового решения владельца и не ломает
`docs/CONFIG-COMPATIBILITY.md`.
- Явное удаление orphan-blob'ов (AC3) не противоречит standing rule
`docs/SCOPE.md` «никогда не удалять файл по предположению»: причиной
остаётся явный вызов `houseplan/assets/delete` с точным asset id, а не
вывод из отсутствия ссылок — ТЗ прямо проговаривает это соответствие.
- AC1–AC12 однозначны и у каждого назван способ доказательства
(`backend/unit`, `backend/HA`, `unit/smoke`, `browser smoke`,
`unit/CI contract`, `review/docs`, `gates`); ни один не описывает решение
расплывчато настолько, чтобы реализация могла разойтись с намерением.
- Технический прецедент для новых механик подтверждён по репозиторию:
мутанты для Python-файлов уже есть в `scripts/mutation-gate.mjs` (AC1–AC4
реализуемы тем же способом); `test/validate-workflow.test.mjs` — рабочий
образец YAML-контрактного теста без внешней зависимости (годится для AC9).
- Раздел «Откат» корректно называет границы: миграции нет, восстановленные
sidecar остаются обычными валидными записями и безопасны при откате.
## Чего не проверял
- Не проверялся сам код реализации — его ещё нет, это ревью ТЗ, не код-ревью;
гейты (`typecheck`/`test`/`build`/backend pytest) не запускались, так как
диапазон `origin/dev...HEAD` для этой ветки — это документация (только
ТЗ и README, класс C), продуктовый код не менялся.
- Не проверялась точность построчных ссылок за пределами девяти утверждений
из «Подтверждённые причины» (например, конкретные номера в «Затронутые
модули») — они не заявлены как проверяемые факты, а как ожидаемый список
файлов, ТЗ прямо говорит «выделение чистых helpers допустимо».
- Не оценивалось время исполнения per-file timeout (180 c × число смоков ×
3 шарда) относительно фактического суммарного бюджета CI — сама находка
выше означает, что этот бюджет ТЗ пока не называет корректно; оценка того,
сколько это должно быть в минутах, — предмет исправления ТЗ, не этого
ревью.
- Не проверялся код `#432` дальше, чем нужно для контекста (не переисследовал
его собственное код-ревью по существу — оно уже принято зелёным на своём
цикле).
---
<!-- material-anchors: сгенерировано конвейером (#414) -->
## Материал раунда
- Ветка: `issue/434-v171-polish-audit`, коммит `5566d6f9898e` — ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет.
- Дерево материала: `1e1861969610794ffa6a9d8458b52ef3695496b8`
```
git log --all --format='%H %T' | grep 1e1861969610
```
- ТЗ `docs/specs/434-v171-polish-audit.md`, блоб `e0229c0cd6550a1c44b978335c52bd2b264b1c52`
```
git log --all --find-object=e0229c0cd6550a1c44b978335c52bd2b264b1c52 -- docs/specs/434-v171-polish-audit.md
```