21 KiB
SPEC-REVIEW-626-r1
- Issue: https://github.com/Matysh/houseplan-card/issues/626
- Этап:
S4-spec-review(ревью ТЗ, PROCESS.md §2.4) - Трек: полный (по аналитике автора, комментарий
#issuecomment-5805305722: «лёгкий трек: нет (две поверхности, публичный контракт прав)») - Материал: тело issue #626, раздел
## ТЗ(снимок на момент ревью, 2026-09-25, второй комментарий владельца «Решения владельца зафиксированы: Q1–Q3 — defaults») - Заход: r1 · блокирующих циклов израсходовано 0 из 4
- Роль: ревьюер ТЗ (не автор)
Скоуп ревью
ТЗ чинит три независимых дефекта прав доступа, найденных аудитом 22.09 (T8, часть C «ACL-матрица»):
may_write()приadmin_only=falseделает writer-ом любого вошедшего пользователя, включая HA-группуsystem-read-only— нужно исключить read-only группу из writer-набора.houseplan/plans/listотдаёт список файлов планов (имена, размеры,used_by) без проверки прав вообще — нужно закрыть тем же_check_write().houseplan/trail/getотдаёт сырые записи трейлов вместе с полемsource(внутренний источник карты) без проекции — нужно вырезатьsourceиз ответа, не трогая хранилище.
Проверялось: обязательные разделы §7.1, однозначность и проверяемость
каждого AC (включая колонку «чем краснеет» для защитных AC), соответствие
описания текущего бага коду «как есть», согласие продуктовых решений
владельца (Q1–Q3) с итоговым текстом ТЗ, отсутствие домыслов вместо фактов,
DoR-пункты §2.5 (миграция/touch/performance/i18n/откат), согласованность с
docs/USER-GUIDE.ru.md и docs/ARCHITECTURE.md.
Как проверялось
- Прочитан
docs/SCOPE.md— задача не добавляет функциональность, а чинит дефект существующей модели прав (may_write, admin-only опция), которая обслуживает все персоны/поверхности разом (домашний админ настраивает права, домочадцы/гости получают View). Отдельной строки Core user jobs под «ACL» нет, но это ожидаемо для security bug fix в существующем механизме, а не новой фичи — вне зоны «нужна строка в SCOPE». - Прочитан
docs/process/REVIEWER.md, разделы PROCESS.md §2.3–§2.5, §7.1, §4, §7.2, §10.4,AGENTS.mdцеликом. - Прочитано тело issue #626 (аналитика + полное ТЗ) и оба комментария — вопросы владельцу и их разрешение.
- Сверены умолчания из вопросов владельца с текстом ТЗ построчно:
- Q1 «кто пишет при
admin_only=false» → умолчание «любой, кромеsystem-read-only» — совпадает со «Скоуп» и АС1 ТЗ дословно. - Q2 «что видно из служебного» → умолчание «
plans/list— только писателям;trail/get— оставить, но безsource» — совпадает со «Скоуп», АС3, АС4. - Q3 «фильтровать ли
config/getпо entity ACL» → умолчание «нет, задокументировать явно» — совпадает с «Решения владельца» п.3 и ACL- таблицей ТЗ. Расхождений между тем, что владелец согласовал, и тем, что записано в ТЗ как контракт, не найдено.
- Q1 «кто пишет при
- Прочитан код, который ТЗ описывает как текущий баг:
custom_components/houseplan/auth.py:15-31—may_writeвозвращаетis_admin if admin_only else True; приadmin_only=falseгруппы пользователя (system-read-onlyвключительно) действительно не смотрятся. Совпадает с «Фактами» ТЗ дословно.custom_components/houseplan/websocket_api.py:1052-1096(ws_plans_list) — подтверждено отсутствие_check_write()/may_write()в теле функции; каталог планов сканируется (hass.async_add_executor_job(_scan)) и отдаётся любому аутентифицированному клиенту. Совпадает с фактами.custom_components/houseplan/websocket_api.py:2391-2394(ws_trail_get) — подтверждено:connection.send_result(msg["id"], {"trails": rec.book.data if rec else {}})без проекции, без гварда.rec.book.data— прямая ссылка на живое хранилищеTrailBook.data(trails.py:95-103), не копия.custom_components/houseplan/websocket_api.py:1191-1193(ws_assets_list) — подтверждено, что этот эндпойнт уже writer-only, как заявляет ТЗ в «Фактах» («ws_assets_list закрыт для не-писателей»); АС7 верно требует не трогать этот контракт.custom_components/houseplan/websocket_api.py:1469(ws_config_get) —"can_write": may_write(hass, ...)уже сейчас единственный источник флага;src/config-adoption.ts:381на фронтенде просто копируетcfgResp.can_writeвhost._serverCanWrite, второго вычисления нет. Значит требование АС2 «config/get.can_writeсовпадает сmay_write» не описывает будущий рефакторинг, а фиксирует уже верную связь — тест на неё осмыслен и, что важно, не будет тривиально «всегда зелёным»: он покраснеет, если кто-то продублирует вычисление на клиенте или разъединит вызовы.custom_components/houseplan/trails.py:104-127(TrailBook.on_point) —sourceпишется ровно в одном месте на запись (cur["source"] = source, плоское поле на уровнеcurrent/previous, не вложено вpoints). Формулировка ТЗ «рекурсивно удалить ключsource» сильнее, чем требует реальная структура (хватило бы двух плоских удалений), но это не ошибка и не двусмысленность — просто более защитный, чем необходимо, способ; выбор конкретного метода прямо отдан автору реализации («форму… функции безопасной проекции trail выбирает автор реализации»).
- Проверена доступность тестовой инфраструктуры, на которую опирается план
автотестов:
hass_read_only_access_token— уже существующая fixture, используется вtests_backend/test_ha_upload.py,test_ha_virtual_lights.py,test_ha_websocket.py. АС1/АС2/АС5 не описывают несуществующий механизм. - Проверены реальные вызовы
houseplan/plans/listна фронтенде —src/houseplan-editor-runtime.ts:7988иsrc/houseplan-onboarding-runtime.ts:165, оба в admin/writer-only потоках (редактор плана, онбординг), у обоих уже есть обработка ошибки (toast.plans_list_failed). Введениеunauthorizedдля read-only не ломает ни одного легитимного текущего вызова UI — read-only пользователь и так не видит этих экранов (can_write=falseпрячет редакторы). - Сверены
docs/USER-GUIDE.ru.md:157-169(таблица «Права пользователей», §2) иdocs/USER-GUIDE.ru.md:2173-2183(§20 «Где лежат данные») — текущая таблица §2 буквально документирует старый баг («Только администраторы — выключено» → «Все вошедшие пользователи» получают право редактирования), это ровно то место, которое АС6/«Затрагиваемые файлы» обязывает поправить. Согласованность найдена, а не предположена. - Сверен
docs/ARCHITECTURE.md:1230-1247(таблица WS-команд) — описание ответовplans/list/trail/get/assets/listне противоречит ТЗ и тоже требует правки (АС6 верно называет этот файл). - Проверены
docs/CONFIG-COMPATIBILITY.mdиdocs/TOUCH-SUPPORT.mdна применимость: задача не меняет ни одно персистентное поле конфигурации (реестрconfig-field-registry.mjsне затрагивается) и не трогает View/touch-рендер (только серверная авторизация и проекция ответа) — ТЗ верно не ссылается на эти документы, и явные «нет» в разделах «Модель данных» и «UX» корректны.
Гейты не гонялись: этап spec, продуктового кода к этой правке ещё нет —
диапазон ревью это текст ТЗ в теле issue, а не диапазон коммитов веток. Это
ожидаемо для ревью ТЗ, а не пропуск.
Находки
Low-1 (снято ревьюером, без правки). Раздел «что человек увидит»
называет внутреннее имя поля API вместо пользовательского языка
Место: тело issue #626, раздел «Пользовательский сценарий и результат»:
«После изменения config/get.can_write=false, редакторы скрыты, а сервер
независимо отклоняет запись.»
Что не так. §7.1 требует, чтобы это предложение читалось «без терминов
реализации» — а config/get.can_write это буквальное имя протокольного
поля, не то, что видит пользователь. По существу описание верное и
однозначное (read-only пользователь теряет доступ к редакторам, сервер
дублирует проверку), просто сформулировано языком протокола, а не
интерфейса.
Почему снимаю, а не отправляю на доработку текста. Формулировка не создаёт двусмысленности и не меняет поведение AC — переформулировка чисто косметическая, а Low-находки процесс разрешает снимать решением ревьюера с записью (PROCESS.md §2.4). Блокировать полный цикл ревью ради одной фразы было бы дороже, чем сама находка.
Что проверено и корректно
- Обязательные разделы §7.1 присутствуют все и в правильном порядке: сценарий → что человек увидит → проблема → скоуп/не-скоуп → контракт поведения (ACL-таблица) → UX → модель данных и совместимость → i18n → AC1–AC7 с доказательством → план автотестов → риски → откат → release-артефакты → «принято предположительно».
- Все три продуктовых решения владельца (Q1–Q3) корректно и без искажений перенесены в контракт, скоуп и не-скоуп — сверено построчно (см. «Как проверялось», п.4).
- Каждый факт из раздела «Факты» подтверждён чтением кода как есть, а не
принят на слово:
may_write(auth.py:15-31), незащищённыйplans/list(websocket_api.py:1052-1096), незащищённый и непроецируемыйtrail/get(websocket_api.py:2391-2394, trails.py:104-127), уже writer-onlyassets/list(websocket_api.py:1191-1193), уже единый источникcan_write(websocket_api.py:1469, src/config-adoption.ts:381). - ACL-таблица внутренне непротиворечива: колонки
admin_only=true/falseдляplans/list/assets/listв точности следуют из определенияmay_write, а не вводят отдельный, второй класс прав;system-read-onlyединая колонка обоснована тем, что результат для неё не зависит отadmin_only(АС1 требуетfalseпри обоих значениях). - Каждый AC называет способ доказательства; для защитных AC (АС1, АС3,
АС4, АС5) явно назван и механизм «чем краснеет»: АС1 — негативные
user/group case; АС3 — spy на сканирование каталога, не выполняемое при
отказе; АС4 — сравнение
storageдо/после проекции, гарантирующее отсутствие мутации; АС5 — «удаление group check должно сделать негативный тест красным», плюс отдельный пункт плана автотестов (6) требует внести эти защиты в mutant registry. Пустой третьей колонки нет ни у одного защитного AC. - Тестовая инфраструктура, на которую опирается план автотестов
(
hass_read_only_access_token), реально существует и уже используется в соседних тестах — план не описывает вымышленный механизм. - Риски раздела покрывают все дефекты из «Фактов» (регресс обычных
домочадцев, fail-closed при неполном user, немутирующая проекция trail,
дрейф UI/backend через единственный
can_write, регрессия View) — пробелов не найдено. - Откат — «откатить продуктовый коммит целиком», без миграции данных, корректно исключает частичный откат документации/кода порознь (именно так и рассинхронизировалась бы модель).
- DoR-пункты §2.5 закрыты явными «нет»/значением там, где применимо:
миграции нет (совместимость подтверждена — задача не трогает
config-field-registry.mjs), touch-влияния нет (backend-only правка, View-рендер не меняется), i18n — новых строк нет, release-артефакты (changelog RU+EN, USER-GUIDE, SCOPE, ARCHITECTURE) названы явно. User-Visible: yesподразумевается («публичное изменение отражается в обоих changelog») и корректно требует правки обоих changelog в одном коммите (AGENTS.md).- Открытых продуктовых вопросов владельцу нет: комментарий-аналитика поставил ровно три продуктовых вопроса с умолчаниями, второй комментарий владельца принял умолчания — процесс §7.1 (вопрос с готовым дефолтом) соблюдён, а не обойдён.
- Не-скоуп сформулирован явно (entity-filtering
config/get, координаты трейла/marker ID,virtual_light/toggle, ACL радаров, новая роль/ настройка) и не пересекается с контрактом.
Чего не проверял
- Гейты (
tsc,npm test,npm run build,check-docs.mjs, backend pytest, инварианты) — не прогонял: этапspec, кода к задаче ещё нет (веткаissue/626-acl-policyупомянута в занятии, но не является материалом ревью текста). Это предмет код-ревью после реализации. - Не проверял вручную поведение реального HA-инстанса (группы
system-read-only,hass_read_only_access_token) — ревью ТЗ не предполагает исполнения; grounding сделан чтением кода и существующих тестов, а не запуском. - Не оценивал перформанс на реальных объёмах (число групп пользователя, размер trail-payload) — ТЗ явно называет сложность O(n) и заявляет отсутствие новых запросов/подписок; количественной проверки на этом этапе не требуется и не проводилась.
- Не проверял построчно весь
docs/ARCHITECTURE.mdи весьdocs/USER-GUIDE.ru.md— только участки, прямо относящиеся кplans/list,trail/get,config/get.can_writeи таблице прав (§2, §20, ACL-таблица команд). Полная сверка этих документов — работа АС6 при код-ревью, когда правки уже будут внесены.
Вердикт
Найдено 0 High и 0 Medium. Одна Low-находка (формулировка «что человек увидит» использует имя API-поля вместо пользовательского языка) снята решением ревьюера без возврата автору — она не влияет ни на один AC и не создаёт двусмысленности. ТЗ грамотно замкнуто на трёх продуктовых умолчаниях владельца, каждый факт о текущем баге подтверждён чтением кода, каждый AC — включая четыре защитных — называет и доказательство, и мутацию, на которой тест обязан покраснеть. Задача готова к разработке.
Вердикт: зелёный · заход r1 · блокирующих циклов 0/4 · High: 0 · Medium: 0 → в задаче
Материал раунда
- Ветка:
issue/626-acl-policy, коммитe52afe63495b— ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет. - Дерево материала:
0977af5c3a98f43e9e74c633f2817e204e35f673git log --all --format='%H %T' | grep 0977af5c3a98 - Тело issue:
83f287ba63d6a04d1ffa6f1205382587c2342cbc04bfaebcde04219d841fa564 - Вердикт конвейера:
green· High 0