18 KiB
SPEC-REVIEW-498-r2
- Issue: https://github.com/Matysh/houseplan-card/issues/498
- ТЗ:
docs/specs/498-backend-hardening-quota-palette-svg-refs.md - Материал: ветка
issue/498-backend-hardening-quota-palette-svg-refs, SHA2ed7bbe679ec06532359da87a3fa7dac9b898a7a(единственный коммит поверх r1:docs: spec #498 r2 — numbered AC, longest-chain reference limit, concurrent quota semantics) - Этап: ТЗ на ревью (PROCESS.md §2.4), заход r2, блокирующих циклов израсходовано 1 из 4
- Трек: полный (не изменился)
- Предыдущий раунд:
docs/reviews/SPEC-REVIEW-498-r1.md, вердикт жёлтый на SHA95493de2fcdbf4e27b91038de92ccebab42719ec(High: 0, Medium: 2 в скоупе)
Скоуп раунда
Разбор идёт по дельте (PROCESS.md §2.9/§2.10): дельта локальна — единственный коммит правит только docs/specs/498-backend-hardening-quota-palette-svg-refs.md (git diff 95493de2..2ed7bbe6 — 31 добавление / 10 удалений в одном файле), тело issue не менялось (последний человеческий комментарий — постановка от 2026-09-09, S2-аналитика; после вердикта r1 новых комментариев от владельца нет). Продуктовая рамка, персона и не-скоуп не затронуты — переносятся из r1 без повторной проверки. Полный разбор не требуется: рамка не изменилась, новая подсистема не появилась, объём дельты — точечное закрытие двух Medium предыдущего раунда, а не пересмотр задачи.
Дельта правит ровно две вещи:
- Добавляет раздел «## 8. Критерии приёмки» (AC1–AC8, пронумерованные, с доказательством) и делает
## 8. Совместимость и откат→## 8.0. - Переписывает §4.2 (семантика параллельных загрузок) и §6 (алгоритм обхода графа ссылок SVG) + добавляет тест на недоброжелательный порядок
idв §7.3, шестой мутант в §7.4.
Как проверялось
Стадия spec-review — гейты (typecheck/test/build) не запускались, продуктовый код не менялся. Каждое утверждение дельты проверено:
- AC-раздел (§8): сверил каждый AC1–AC8 построчно с соответствующим текстом §2/§4/§5/§6/§7 — противоречий нет, имена тестов в AC совпадают 1:1 с именами в §7 (
grep -n "test_issue_498"по файлу — все шесть имён встречаются ровно дважды: один раз в §7, один раз в привязанном AC, без расхождений). AC8 явно закрывает «нет» по перфу/touch, как требовал r1. - Алгоритм §6 (мемоизированный longest-path вместо булева
visited): прогнал руками адверсариальный пример из r1 (лимит=2, графa→d, b→c, c→a,sorted(ids)=[a,b,c,d], что при старом алгоритме проходило все проверки, хотя истинный путьb→c→a→dдлиной 4 вдвое больше лимита). Трасса нового алгоритма:_visit(a)→longest[d]=1,longest[a]=max(1, longest[d]+1)=2(не >2, принято, путьa→dдействительно длины 2 — верно);_visit(b)→cне вlongest, спуск, ссылкаc→a:aуже вlongest→chain(c)=max(1, longest[a]+1)=3;cисчерпан →longest[c]=3 > 2→too_large. Мемоизированная длина корректно всплывает через уже обработанный узел независимо от порядка обхода — дыра r1 закрыта, не декларативно, а по факту пересчёта. - Эндпойнт в AC6/§7.3 (
/api/houseplan/assets/upload): в r1-версии ТЗ (и в исходном S2-тексте) фигурировал несуществующий путь/api/houseplan/decor/assets; в r2 путь исправлен. Сверил с кодом:custom_components/houseplan/http_api.py:221—class HouseplanDecorAssetUploadView,url = "/api/houseplan/assets/upload"— совпадает буква в букву с AC6/§7.3 r2. Незамеченная r1 неточность устранена попутно. - Семантика параллельных загрузок §4.2 (
консервативно отклоняются обе, если обе проверки идут, пока оба файла staged): сверил с реальным путём исполненияHouseplanUploadView.post(http_api.py:352-460) —check_quotaвызывается черезawait hass.async_add_executor_job(...)после того, как тело файла уже полностью дописано в.upload-*через отдельные executor-джобы; между стадией «staged» и вызовомcheck_quotaесть await-точка, так что при двух конкурентных запросах, стартовавших почти одновременно, к моменту вызоваcheck_quotaоба временных файла уже реально лежат на диске — сценарий «оба видят чужой staged-файл» не гипотетический, а прямое следствие текущей структуры кода. Тест 7.1 (не [200, 200]) корректно допускает оба исхода (один успех/один отказ и оба отказа), не переобещая детерминизм, которого код не даёт. - Все шесть мутантов §7.4: два новых (
svg-reference-depth-per-start-not-per-chain, дублирующий обходsvg-reference-walk-recursive-againне менялся) имеют явных свидетелей в §7.3; убедился, что новый тест на недоброжелательный порядокid(n0065→n0064→…→n0001, 65 узлов) — единственный тест-свидетель первого нового мутанта, и что «естественный» тест (g0…g2499, по возрастанию) его не покрывает (совпадает с рекомендацией r1). - Release-артефакты и DoR-опоры, не тронутые дельтой (release-артефакты §9, откат §8.0, риски §8.2, i18n §8.1, не-скоуп §3): сверены на факт присутствия и внутренней непротиворечивости, детальная построчная проверка кода под ними наследуется из r1 (код не менялся).
- Существование
docs/CHANGELOG.md,docs/CHANGELOG.ru.md,docs/SUPPORT-PRIVACY.md(фраза «safe display settings», на которую ссылается §9) подтверждено чтением файлов. - Issue #498: перечитал тело и оба комментария (
gh issue view 498) — после вердикта r1 (2026-09-09T06:42) новых комментариев от владельца нет, тело issue не редактировалось. Открытых продуктовых вопросов не появилось.
Закрытие раунда r1
| Находка r1 | Чем закрыта | Где это видно |
|---|---|---|
| Medium 1 — нет раздела «AC1…ACn» с доказательством, нет явного «нет» по перф/touch (DoR, §2.5/§7.1) | Добавлен раздел «## 8. Критерии приёмки», AC1–AC8, у каждого назван тест-доказательство; AC8 — явная строка «Производительность и touch не затронуты» | docs/specs/498-...md:127-136 (AC1–AC8), AC8 = строка 136 |
Medium 2 — алгоритм обхода графа ссылок (§6) не гарантирует предел 64 при недоброжелательном порядке id (глубина стека вместо длины самой длинной цепочки через узел) |
§6 переписан: мемоизированная longest[node] (длина самой длинной цепочки от узла вниз) вместо булева visited; узел из longest даёт chain = max(chain, longest[ref]+1) вместо простого пропуска; в §7.3 добавлен тест с id n0065→…→n0001 (sorted стартует с хвоста); в §7.4 — шестой мутант svg-reference-depth-per-start-not-per-chain |
docs/specs/498-...md:84-93 (алгоритм), :113 (тест недоброжелательного порядка), :122 (мутант) — вручную проверено трассировкой на адверсариальном примере r1, см. «Как проверялось» |
| Low — §1.2 «После» смешивает пользовательский язык с HTTP-кодами/именами ошибок | Переписано на язык результата без кодов/статусов | docs/specs/498-...md:25 — «слишком большой», «отклоняется как некорректная», без too_large (413)/invalid_image (400) |
Все находки r1 закрыты в этом раунде, включая необязательный Low.
Унаследовано из r1
Без повторной проверки принято (код не менялся, дельта этих участков не касалась):
- Построчное подтверждение трёх дефектов B5/B6/B7 в текущем коде (
plans.py:201-239,http_api.py:352-491,http_api.py:230-263,decor_assets.py:200-289) — SPEC-REVIEW-498-r1, раздел «Как проверялось», SHA95493de2. - Совпадение
SUPPORT_FILL_COLOR_KEYS(§5) сDEFAULT_FILL_COLORSизsrc/logic.ts:1412-1424(11 из 11) — SPEC-REVIEW-498-r1, там же. - Совместимость сигнатур
excludeкак keyword-only сNoneпо умолчанию — не ломает три существующих вызоваdir_usage/check_quota— SPEC-REVIEW-498-r1, там же. - Персона и сценарий (§1.1) внутри
docs/SCOPE.md(Home admin, поверхности upload/support/decor, job J4/J6) — SPEC-REVIEW-498-r1, раздел «Скоуп»/«Что проверено». - Не-скоуп (§3): ослабление лимитов, сужение схемы
fill_colors, автоочистка чужих.upload-*, перенос staging, квота планов — всё обоснованно исключено — SPEC-REVIEW-498-r1. - Существование и корректность существующих тестов-регрессий (
test_rich_plan_projection_preserves_safe_structure_and_drops_unknown_values,test_svg_rejects_the_whole_unsafe_document,test_svg_preserves_safe_local_gradient_clip_mask_and_transparency) — SPEC-REVIEW-498-r1.
Находки
Ни одной High или Medium в дельте r2 не найдено.
Low — нумерация разделов «## 8» / «## 8.0» после вставки AC-раздела
Дельта вставила «## 8. Критерии приёмки» перед прежним «## 8. Совместимость и откат», и вместо сдвига нумерации (9, 9.1, 9.2, 10, 11, 12) переименовала старый раздел в «## 8.0», оставив «## 8.1»/«## 8.2» как были. Раздел «8.0» после «8» — не соответствует обычной десятичной нумерации (стандартно 8.0 предшествовал бы 8.1, а не следовал за целым 8). Не блокирует: содержание всех разделов присутствует и однозначно, PROCESS.md §7.1 требует наличия перечисленных разделов, а не конкретного числового формата заголовков. Снимаю решением ревьюера без возврата на цикл — техническая правка нумерации тривиальна и не требует отдельного прохода; на усмотрение автора при следующей правке файла.
Что проверено и корректно
- Оба Medium из r1 закрыты по существу, не декларативно: алгоритм проверен вручную на исходном контрпримере r1 и даёт правильный результат независимо от порядка обхода; AC-раздел — не косметическая обёртка, а точное сведение уже написанного контракта в проверяемый список с именами тестов, совпадающими с §7.
- Попутно исправлена унаследованная из S2/r1 неточность — несуществующий путь эндпойнта
/api/houseplan/decor/assetsзаменён на реальный/api/houseplan/assets/upload(сверено с кодом). - Новая формулировка семантики конкурентных загрузок (§4.2, AC2) — не догадка: подтверждена структурой реального кода (await-точка между стадией staging и вызовом
check_quotaделает сценарий «оба видят staged-файл друг друга» реальным, а не гипотетическим), и AC2/тест 7.1 сформулированы так, что не переобещают детерминизм, которого код не может дать. - Открытых продуктовых вопросов нет; владелец не оставлял новых комментариев после вердикта r1, тело issue не редактировалось.
- Все шесть мутантов §7.4 имеют явного тест-свидетеля; новый мутант
svg-reference-depth-per-start-not-per-chainловится именно новым тестом на недоброжелательный порядок id — «естественный» тест (по возрастанию) его не поймал бы, как и предсказывал r1.
Чего не проверял
- Гейты
typecheck/test/build/pytest— не требуются на стадии spec-review, продуктовый код ещё не написан. - Построчную повторную сверку кода B5/B6/B7 — не менялась дельтой, код на дереве не менялся с r1, наследуется (см. «Унаследовано из r1»).
- Флейкость/детерминизм самого
asyncio.gather-теста для §7.1 (барьер на два вызоваcheck_quota) — вопрос дизайна теста, а не ТЗ; вернётся на код-ревью, если тест окажется нестабильным. - Реальный прогон предложенных тестов — их ещё не существует (стадия spec), только проверка на непротиворечивость текста и совпадение имён между §7 и §8.
Материал раунда
- Ветка:
issue/498-backend-hardening-quota-palette-svg-refs - SHA:
2ed7bbe679ec06532359da87a3fa7dac9b898a7a - ТЗ:
docs/specs/498-backend-hardening-quota-palette-svg-refs.md, blob11929110b55c3a7ac3c1ba41469b82de3eeb88b8 - Предыдущий раунд:
docs/reviews/SPEC-REVIEW-498-r1.md, SHA95493de2fcdbf4e27b91038de92ccebab42719ec
Вердикт
Зелёный. Оба Medium из r1 закрыты по существу (проверено пересчётом алгоритма на контрпримере и сверкой AC с тестами), High-находок нет. Найден один Low (нумерация «8»/«8.0»), снят решением ревьюера без возврата на цикл. ТЗ готово к переходу в «Готово к разработке» (S5).
Материал раунда
- Ветка:
issue/498-backend-hardening-quota-palette-svg-refs, коммит2ed7bbe679ec— ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет. - Дерево материала:
52ce02cf46032fac5a67d7fd8cb290af16a70503git log --all --format='%H %T' | grep 52ce02cf4603 - ТЗ
docs/specs/498-backend-hardening-quota-palette-svg-refs.md, блоб11929110b55c3a7ac3c1ba41469b82de3eeb88b8git log --all --find-object=11929110b55c3a7ac3c1ba41469b82de3eeb88b8 -- docs/specs/498-backend-hardening-quota-palette-svg-refs.md - Вердикт конвейера:
green· High 0