42 KiB
CODE-REVIEW-43-r1
- Issue: #43 — Диалог помощи и обратной связи с обезличенным support report
- Ветка:
issue/43-help-feedback - SHA ревью:
1ba5363038055b3f3442b62a761161d05c6d03c7(mergeorigin/devв issue-ветку поверх1e2e0fa6— продуктовая реализация) - Заход: r1 (первый код-ревью; ТЗ прошло 5 раундов ревью и зелёное,
S5-ready→S7-code-review) - Вердикт: жёлтый · блокирующих циклов 1/4 · High: 0 · Medium: 3 → в задаче
1. Скоуп
Диапазон origin/dev...HEAD: 71 файл, +7023/−411. Реализация ТЗ
docs/specs/043-private-support-report.md (ревизия 2, зелёное ревью r5):
- фронтенд: кнопка «Помощь и обратная связь» в шапке, новый диалог (перенос
«О карточке», языковая ссылка на USER-GUIDE, форма отчёта, preview
диагностического пакета) —
src/houseplan-card.ts,src/houseplan-editor-runtime.ts,src/support-feedback.ts,src/hp-dialog.ts,src/styles/*.ts,src/i18n/*.json; - backend: три websocket-команды
houseplan/support/{preview,preview/discard,submit}, allowlist-проекцияsupport_package.py, транспортsupport_transport.py, константы вconst.py; - отдельный class-B сервис
scripts/support-relay/**(написан и развёрнут на проектном стенде ещё во время цикла ТЗ, при закрытых DoR-зависимостях §17; это уже было предметом пяти раундов ревью ТЗ, включая живой пентест владельца на трассировкеX-Forwarded-For); - документация:
USER-GUIDE.{md,ru.md},ARCHITECTURE.md, новыйSUPPORT-PRIVACY.md,TESTING.md, обаCHANGELOG.
Это первый код-ревью задачи — раунд полный, дельты по §2.10 нет.
2. Как проверялось
Ревью кода отвечает на вопрос «оно вообще работает» вместо ручного тестирования (§2.7). Ниже — что выполнено лично, а не заявлено.
2.1 Дешёвые гейты (прогнаны лично, HEAD 1ba53630)
| Гейт | Команда | Результат |
|---|---|---|
| Типы | npx tsc --noEmit |
зелёный, 0 ошибок, 6.2 с |
| Юнит/интеграционные (frontend) | npm test |
1724 теста: 1723 passed, 1 skipped, 0 failed, 28.2 с |
| Сборка + сверка бандла | npm run build && cmp dist/houseplan-card.js custom_components/houseplan/frontend/houseplan-card.js |
идентичны; npm run bundle:sync не меняет рабочее дерево (git status чист) — все три копии (dist, custom_components/.../frontend, demo/srv/assets) уже синхронны в коммите |
Новый any |
node scripts/no-new-any.mjs --base origin/dev --head HEAD |
красный — 4 новых явных any без // any-ok: (находка Medium №3 ниже) |
| Docs-контракт | node scripts/check-docs.mjs |
красный — screenshot source fingerprint is stale (находка Medium №1 ниже) |
| process-gate | node scripts/process-gate.mjs --range origin/dev..HEAD --issues |
«гейт пройден, предупреждений 0», 15 коммитов, включая проверку статуса issue через gh |
check-docs обязателен, потому что диапазон меняет src/**
(houseplan-card.ts, houseplan-editor-runtime.ts, hp-dialog.ts, оба
styles/*.ts, все 4 i18n/*.json) — правило §8 не оставляет выбора.
Проверено, что стойкость не унаследована из dev: тот же скрипт на чистом
origin/dev (отдельный worktree, 319b25c2) даёт «Documentation checks passed
(7 files, 10 external links)» — значит, устаревание отпечатка порождено именно
этим диффом, а не фоновой работой #410.
2.2 Бэкенд Python (доступность харнесса)
Локальный образ ревьюера без .venv-backend (как и описанный в AGENTS.md
случай «Local Windows checkout»). Установил недостающие пакеты вручную, чтобы
не оставлять этот участок полностью непроверенным:
pip install pytest voluptuous aiohttp pytest-asyncio homeassistant(без пина) →tests_backend/test_support_package.py: 10 passed (модульsupport_package.pyнамеренно не импортирует HA, тест не нуждается в харнессе);tests_backend/test_ha_support_transport.py: 7 passed (этому файлу тоже хватает гологоhomeassistant.helpers.aiohttp_client, харнессpytest-homeassistant-custom-componentне нужен).pip install pytest-homeassistant-custom-component(снова без пина; CI пинуетhomeassistant==2026.8.3+phcc==0.13.357под Python 3.14, здесь только 3.12) → резолвер подобрал несовместимую пару,python -m pytest tests_backend -qдал 96 failed / 387 passed по всемуtest_ha_*.py, включая файлы, никак не связанные с #43 (test_ha_upload.py,test_ha_virtual_lights.py). Это диагностика рассинхронизации версий харнесса, а не регрессия диффа — результат не используется как доказательство ни в одну сторону. Четыре support-специфичных теста вtest_ha_websocket.py(test_support_preview_is_authorized_exact_and_consumed_only_after_success,test_support_preview_replacement_and_discard_are_draft_local,test_support_text_only_submit_carries_safe_versions_without_plan_data,test_support_commands_reject_read_only_user_before_build_or_transport,test_support_preview_schema_does_not_coerce_client_facts[...]) упали той же генерическойassert False, что и заведомо исправные несвязанные тесты — подтверждает, что причина в среде, не в тесте.- Итог: полный HA-харнесс в этом ревью не поднят (нет Python 3.14 в песочнице,
тянуть его ради одного прогона — непропорциональная трата времени). Авторский
хендофф в issue называет точные числа с зелёными прогонами на его машине
(
privacy/backend targeted — 15 passed,support relay — 36 passed) — доверяю числу для support_package.py/support_transport.py (сам перепроверил, совпадает — 10+7=17, близко к заявленным 15 при другом подсчёте узкого таргета), но дляtest_ha_websocket.pyавторские числа не перепроверены исполнением. Соответствующие AC (AC9, AC10) закрыты ниже пометкой «проверено чтением, не исполнением», не автотестом с моей стороны.
2.3 Relay (scripts/support-relay/**)
python3 -m unittest discover -s scripts/support-relay/tests -q → 36
passed (тот же набор, что и в последнем зелёном спек-ревью r5; код hp_relay/**
этим диапазоном не менялся — сверено git diff origin/dev...HEAD -- scripts/support-relay/hp_relay вместе с диапазоном коммитов истории: relay был
написан и вычитан во время цикла ТЗ пятью раундами ревью, включая живой пентест
владельца против подмены X-Forwarded-For). Прочитал код заново (не как
унаследованное, а как часть этого код-ревью — предмет здесь другой гейт,
код-ревью, а не спек-ревью):
hp_relay/app.py:147-158— источник rate-limit берётся из последнего элементаX-Forwarded-Forтолько приtrusted_proxy=True(default), иначе — адрес TCP-соединения; совпадает сscripts/support-relay/deploy/Caddyfile.fragment(header_up X-Forwarded-For {remote_host}— заголовок перезаписывается целиком на обоих продовых сайтах).hp_relay/delivery.py— оба канала (TelegramDelivery,HaWebhookDelivery) не используютparse_mode, то есть Telegram показывает текст пользователя буквально; ответ провайдера не отражается наружу и не попадает в HTTP-ответ клиенту (app.pyмапит статус на закрытый список кодов).hp_relay/app.py— единственный лог-метод (log_message) переопределён и не печатает адрес источника, только команду и путь.
Отдельно нашёл несостыковку единиц измерения в конфигурации прокси (находка Low №4 ниже) — не блокирует, эффекта на легитимный трафик не имеет по расчёту.
2.4 Смоки и golden
node scripts/smoke-select.mjs --base origin/dev --head HEAD печатает ~40
существующих смоков — все совпадают по диффу через уже существующие символы
(_config, _editorRuntime, _infoCard, _markerDialog), ни один не создан и
не изменён этим диффом. ls demo/smoke_*.mjs | grep -iE "support|help|feedback"
находит smoke_feedback_v2.mjs и smoke_help_affordance.mjs — оба
существовали в origin/dev до этой ветки и относятся к issue #68 (контекстная
помощь hp-help/.rlgearbtn), никак не к диалогу #43. Ни один существующий,
ни один новый браузерный смок не касается нового диалога, кнопки или формы.
git diff --stat origin/dev...HEAD -- demo/ — пусто; -- demo/golden — пусто.
Причина находки Medium №2 ниже.
2.5 Инварианты модели
Не прогонял npm run invariants -- --config <...>. Диапазон не меняет ни
validation.py, ни модуль геометрии/толщины стен, ни схему layout (сверено
git diff --stat — custom_components/houseplan/{diagnostics,import_export,validation}.py
не тронуты). support_package.py только читает уже провалидированные
config/layout под тем же write_lock и строит отдельный allowlist-объект;
ничего не пишет обратно в хранимую модель. Инварианты о ключах записи толщины и
разрешимости ссылок относятся к хранимой модели, а не к экспортному снапшоту —
гейт не по предмету этого диффа, не «пропущен», а не применим.
2.6 Одно число — один источник
Проследил цепочку preview: backend строит bytes/sha256/size один раз в
ws_support_preview (websocket_api.py:2153-2221), кладёт в
rt.support_previews[token] и больше не пересчитывает — submit
(websocket_api.py:2260-2312) берёт preview.get("bytes")/preview.get("sha256")
из того же токена, никогда не перестраивая пакет. На фронтенде
_buildSupportPreview (houseplan-editor-runtime.ts:9193-9250) сверяет
response.size с независимо посчитанным TextEncoder().encode(text).byteLength
и падает в support_rejected при расхождении, затем кладёт результат в один
SupportPreview-объект (support-feedback.ts:6-16), который читают бейдж
размера, строка SHA-256, <textarea> с сырым JSON и кнопка Download
(_downloadSupportPreview) — везде один и тот же объект, ни одного второго
независимого вычисления не найдено. Единственное число, которое пользователь
видит дважды (preview vs отправленное), доказуемо имеет один источник по обе
стороны границы.
3. Разбор по AC (§13 ТЗ)
Обозначения: PASS/тест — автотест, который умеет падать (проверил сам или логикой теста); PASS/чтение — проверено чтением кода, не исполнением; UNVERIFIED — ни то, ни другое в этом ревью.
| AC | Итог | Свидетельство |
|---|---|---|
| AC1 | PASS/чтение | houseplan-card.ts — кнопка Help делит тот же _norm && _canEdit блок и kiosk-класс, что и General Settings; порядок в DOM (zoom → settings → help) совпадает с test/support-feedback.test.mjs. Открытие — только newSupportDialogState(), ни один mode/zoom/selection не трогается. Браузерного смока с реальным View/тремя редакторами/kiosk-негативом, который спек называет доказательством, нет — см. находку Medium №2 |
| AC2 | PASS/тест+чтение | gs.about_group/about_version/github/telegram перенесены ровно один раз (test/support-feedback.test.mjs, счётчик вхождений); маршрутизация RU→RU/остальное→EN подтверждена чтением (houseplan-editor-runtime.ts:9360-9363) и юнит-тестом |
| AC3 | PASS/тест | support-feedback.ts — supportDraftError, code-point-точная длина (тест на эмодзи), чекбокс всегда false при новом открытии, не сохраняется |
| AC4 | PASS/чтение | Текст предупреждения называет точную геометрию явно; открытие диалога не делает сетевых вызовов; включение чекбокса вызывает только внутренний houseplan/support/preview (это и есть контракт §6.4/§9.3 — «preview не трогает внешний relay»), внешний relay достижим только из submit |
| AC5 | PASS/тест | Backend никогда не перестраивает пакет на submit (preview.get("bytes")); test_support_package.py доказывает детерминизм байт/хеша; фронтенд сверяет присланный size с независимым подсчётом байт текста |
| AC6 | PASS/тест+чтение | Каждая _project_*-функция в support_package.py строит новый dict по allowlist полей, ни разу не мутируя исходный config/layout (grep по файлу не находит config[...] =/.pop); test_geometry_and_references_survive_with_package_local_pseudonyms проверяет разрешимость ссылок после ремаппинга |
| AC7 | PASS/тест (с оговоркой) | test_privacy_projection_never_contains_raw_or_encoded_forbidden_values сеет sentinel в каждое запрещённое поле, включая неизвестные вложенные ключи, и проверяет отсутствие verbatim/JSON-escaped/base64. Путь _support_repairs() (реальное чтение HA issue registry, websocket_api.py:244-253) этим тестом не задет — тест кормит build_support_package уже готовым списком repairs, минуя чтение реестра. Прочитал функцию: она физически не может вернуть ничего, кроме {"code": "broken_plan", "count": N}} (raw issue_id/space_id нигде не покидают функцию) — оцениваю PASS по чтению для этого конкретного пути, но замечаю расхождение с §14.1, который явно требует sentinel-фикстуру именно для broken_plan_<spaceId> |
| AC8 | PASS/тест | Тот же sentinel-тест сеет неизвестные top-level и вложенные ключи — allowlist «строит новое», а не «копирует и чистит», поэтому регрессия к redaction-after-copy была бы поймана |
| AC9 | PASS/чтение (частично тест) | _check_write/may_write на всех трёх командах, тест на read-only пользователя (test_support_commands_reject_read_only_user_before_build_or_transport) зелёный по чтению кода (харнесс не поднят — см. §2.2). Проверка владения токеном (preview.get("owner") != _connection_user_id(...)) в discard/submit простая и однозначная по чтению; межпользовательского теста (два can_write-пользователя, один не видит чужой токен) в кодовой базе нет, хотя у соседней фичи (import-preview) есть точно такой паттерн теста. Не блокирую отдельно — логика идентична уже проверенному паттерну соседней фичи, но фиксирую пробел |
| AC10 | PASS/чтение | Submit никогда не перечитывает config/layout — структурная гарантия, а не только тестовая. Замена/discard токена — тест зелёный (по чтению, харнесс не поднят). TTL 10 минут ни один тест не проверяет fake-clock (для аналогичной import-preview фичи такой тест есть: runtime.import_previews[token]["expires"] = 0), хотя АС в ТЗ явно обещает «Fake-clock backend tests». Код (preview["expires"] <= time.monotonic()) простой, оцениваю PASS по чтению |
| AC11 | PASS/тест | support_transport.py — HTTPS/фиксированный хост/без редиректов/таймауты 5/20с, статус мапится на закрытый список кодов до чтения тела (413 отрабатывает корректно независимо от того, кто вернул этот статус — сам relay или проксирующий Caddy), ответ ограничен 4097 байт и не отражается. test_transport_uses_only_fixed_https_host_without_redirects, test_transport_maps_remote_status_without_reflecting_response, test_transport_rejects_unbounded_or_invalid_receipt — все зелёные (запустил сам, §2.2). Отсутствие логирования message/contact подтверждено grep-ом по файлу (0 вызовов логгера) — чтением, не caplog-тестом |
| AC12 / AC12a | PASS/тест | 36/36 в scripts/support-relay/tests, включая test_client_cannot_pick_its_own_rate_bucket и test_direct_node_ignores_the_forwarded_header — оба уже мутационно доказаны в спек-ревью r3-r5, код с тех пор не менялся |
| AC13 | PASS/чтение | Успех не закрывает диалог, показывает id + Copy; ошибка сохраняет черновик, предлагает Retry/Download/ссылки. Браузерного смока success/429/timeout, который спек называет доказательством, нет — см. Medium №2 |
| AC14 | PASS/чтение, UNVERIFIED по заявленному способу доказательства | 44×44 CSS px, media-запрос на 320px, aria-describedby, управление фокусом — всё присутствует в источнике. Спек требует «Reviewed desktop + phone + tablet goldens and touch smoke» — этого артефакта нет вообще (см. Medium №2). Реальный рендер в браузере (фактические вычисленные размеры, фокус в настоящем DOM, реакция на нажатие Tab) статическим чтением TS не доказывается |
| AC15 | PASS/чтение | diagnostics.py, import_export.py, validation.py — 0 изменений в диапазоне (git diff --stat) |
| AC16 | PASS/чтение+прод-эксплуатация | Секрета в custom_components/** нет, только SUPPORT_RELAY_URL-константа; issue-хендоффы фиксируют реальный health-check прод/staging, ретеншн-таймеры на стенде |
| AC17 | PASS/чтение (frontend), UNVERIFIED (backend perf) | Фронтенд: SupportDialogState-тип импортируется как type-only, реальный код лежит в существующем lazy-чанке редактора — нового eager-импорта нет. Backend perf-бюджет (≤750 мс/≤24 MiB на максимальный конфиг) не имеет ни одного бенчмарк-теста — спек называет «Backend benchmark/limit test» явно, в кодовой базе такого теста нет вовсе. MAX_SUPPORT_ATTACHMENT_BYTES (8 MiB) проверен по коду (support_package.py:494), но не тестом на реальном превышении размера |
4. Находки
Medium (в скоупе #43, чинится в этой же задаче)
M1. check-docs красный на этом SHA — устаревший отпечаток скриншотов документации.
node scripts/check-docs.mjs → ERROR screenshot source fingerprint is stale; run npm run build && node demo/docs/capture.mjs. Подтверждено, что это не
фоновый шум: тот же скрипт на чистом origin/dev (319b25c2, отдельный
worktree) даёт «Documentation checks passed». Причина механическая и полностью
предсказуемая по AGENTS.md/PROCESS.md §8: docs/images/screenshots.json
хранит sourceFingerprint: 7faa6c2e…, а актуальный visualFingerprint(src/**)
на HEAD — d300613d…; диапазон меняет houseplan-card.ts,
houseplan-editor-runtime.ts, оба styles/*.ts — любая из этих правок делает
отпечаток устаревшим, «выбирать тут нечего». Job docs в validate.yml
запускает ровно эту же команду (--external) — CI на этом SHA покраснеет
именно там, повторяя сценарий #230/#234/#237 (там же dev простоял с красным
docs до следующей задачи). Фикс — пересъёмка (npm run build && node demo/docs/capture.mjs), либо, если требуется байт-в-байт воспроизводимость
между окружениями (docs:accept --reviewed --from=<CI-артефакт>), прогон job
Docs screenshots (workflow_dispatch) с последующей приёмкой. Ни то, ни
другое ревьюер делать не вправе (не правит продуктовый код/сгенерированное).
M2. Ни одного браузерного теста или golden-сцены для всего диалога Help & Feedback.
git diff --stat origin/dev...HEAD -- demo/ и -- demo/golden — пусто.
node scripts/smoke-select.mjs --base origin/dev --head HEAD называет ~40
смоков, отобранных по совпадению с уже существующими символами
(_config, _editorRuntime, _infoCard, _markerDialog) — ни один не
упоминает новую кнопку, диалог или форму, потому что символы _openSupportDialog,
_submitSupport, _renderSupportDialog не встречаются ни в одном смоке
репозитория. Единственные тематически похожие файлы,
demo/smoke_feedback_v2.mjs и demo/smoke_help_affordance.mjs, существовали в
origin/dev до этой ветки и относятся к неродственной фиче #68 (контекстная
подсказка hp-help, кнопка .rlgearbtn) — совпадение имён случайное.
Это не абстрактная придирка к процессу, а прямое расхождение с самим ТЗ, которое прошло пять раундов ревью именно ради точности доказательств:
- §13 AC1: «Browser smoke in View + three editors + kiosk/unauthorized negatives»;
- §13 AC13: «Browser smoke across success/429/timeout/unknown command»;
- §13 AC14: «Reviewed desktop + phone + tablet goldens and touch smoke»;
- §14.2 явно перечисляет golden-сцены («desktop no attachment, desktop preview, phone validation error, phone success, relay error/manual recovery, light and dark themes»);
- §14.4 (обязательные мутации): «success shown on timeout → browser smoke red» — без единого браузерного теста эта мутация физически некому ловить.
Прочитанный код (см. §3 таблицы AC1/AC13/AC14) выглядит корректным, и я принимаю его как «проверено чтением» для логики. Но именно то, что чтением принципиально не доказывается — реальный вычисленный размер тач-таргета в браузере, фактическое поведение фокуса/ARIA при настоящем Tab/Escape, реальная раскладка при ширине 320px, экранные состояния success/429/timeout — эта фича вводит новую диалоговую поверхность с уникально строгим touch-контрактом (единственное явное исключение из «редакторы desktop-first» во всём проекте) и не имеет вообще никакого доказательства на этом уровне. Ручного тестирования в процессе нет по конструкции — это тем более причина не оставлять единственный уровень, который ловит браузерные дефекты, полностью пустым для новой поверхности.
M3. Новый явный any без обоснования — 4 вхождения, гейт no-new-any красный.
node scripts/no-new-any.mjs --base origin/dev --head HEAD:
src/houseplan-editor-runtime.ts:9199 — const response: any = await this.host.hass.callWS({...}) (preview)
src/houseplan-editor-runtime.ts:9317 — const response: any = await this.host.hass.callWS({...}) (submit)
src/support-feedback.ts:124 — options.language as any
src/support-feedback.ts:141 — (value as any).code
Ни на одной строке нет // any-ok: <причина>, как того явно требует §8. Все
четыре — не случай «тип недоступен»: формы WS-ответов полностью описаны в §8.1
и §8.3 ТЗ ({token, expires_in, size, sha256, spaces, format, version, text}
и {report_id}), их можно типизировать интерфейсом; options.language as any
используется только для проверки членства в readonly ['en','ru','de','fr'] —
идиоматично решается через (LANGS as readonly string[]).includes(...) без
приведения аргумента к any; (value as any).code — тривиально
(value as Record<string, unknown>).code. Гейт создан именно для этого класса
случаев (issue #342): новый код не должен по умолчанию расширять долг в 1034
уже существующих any.
Low (снимается с записью, не блокирует)
L1. scripts/support-relay/deploy/Caddyfile.fragment: request_body { max_size 8.7MB } — Caddy парсит MB как десятичные байты (8 700 000), а
собственный лимит relay MAX_REQUEST_BYTES = 8 * 1024*1024 + 512*1024 — это
8.5 MiB (8 912 896 байт). То есть внешний прокси-лимит на ~213 КБ меньше
внутреннего, хотя по комментарию в config.py («вложение ≤ 8 MiB, весь запрос
≤ 8.5 MiB») задумывался запас, а не сужение. Эффекта на легитимный трафик нет:
support_package.py отклоняет пакет ещё в HA до отправки, если он больше ровно
8 MiB (MAX_SUPPORT_ATTACHMENT_BYTES), так что реальный исходящий запрос —
максимум ~8.0-8.1 MiB, с запасом ниже обеих границ; и даже гипотетический
прямой запрос к публичному эндпоинту, упёршийся в Caddy раньше, чем в
app.py, получит тот же HTTP 413, который support_transport.py мапит на
support_package_too_large по коду статуса, а не по телу ответа. Стоит
поправить директиву на явно бо́льшее значение (например, 9MB или
9437184) при следующей правке деплоя — путаница decimal/binary единиц имеет
свойство накапливаться.
L2. custom_components/houseplan/websocket_api.py, ws_support_preview:
дорогая сборка снимка (await hass.async_add_executor_job(_build_snapshot),
бюджет до 750 мс/24 MiB) выполняется до проверки
MAX_SUPPORT_PREVIEWS_PER_USER/_TOTAL (строки 2180 → 2194-2196). Уже
авторизованный (can_write) пользователь с 3 живыми превью может повторно
дёргать houseplan/support/preview, каждый раз оплачивая полную стоимость
сборки, и только после этого получать support_rate_limited — лимит защищает
только хранимое состояние, не CPU. Блокирующим не считаю: действующее лицо уже
прошло авторизацию записи (доверенная роль admin/can_write, не анонимный
интернет), итоговый инвариант «не больше 3 сохранённых превью» не нарушается.
L3. _support_repairs() (websocket_api.py:244-253, новая функция) не
покрыта ни одним тестом — §14.1 явно называет «sentinel fixture including
broken_plan_<spaceId> Repair normalization» в плане тестов, а
test_support_package.py подаёт repairs=[...] в build_support_package
напрямую, минуя чтение реестра. Прочитал функцию — она физически не может
вернуть ничего, кроме {"code": "broken_plan", "count": N} (raw issue_id
нигде не покидает функцию, кроме как через .startswith()-проверку), поэтому
не блокирую, но фиксирую расхождение с собственным тест-планом ТЗ.
L4. test/support-feedback.test.mjs и весь diff добавляют
backup.error.support_invalid_message/support_package_too_large/
support_preview_expired/support_rate_limited/support_rejected/
support_unavailable в namespace backup.error.* во всех 4 локалях
(src/i18n/en.json:1132-1137 и параллельно в ru/de/fr.json), но ни разу их
не читает — реальные ошибки диалога идут через support.error.*
(_supportErrorText, houseplan-editor-runtime.ts:9345-9351). Функционально
безвредно (просто неиспользуемые переводы), но может ввести в заблуждение
следующего читателя, решившего, что backup-flow умеет показывать
support-ошибки. Можно удалить точечной правкой.
5. Что проверено и корректно (без замечаний)
- Allowlist-проекция
support_package.py: каждая_project_*-функция строит новыйdictполем за полем, ни разу не сериализуя и не мутируя исходныйconfig/layout— структурная гарантия §7.3 выполнена буквально, не только по духу. - Авторизация: все три WS-команды используют тот же
_check_write/may_write, что и остальные write-команды интеграции; владение токеном проверяется в discard и submit. - Пседонимизация: пространство имён случайное на каждый preview
(
secrets.token_hex(4)), не хранится и не возвращается клиенту; неизвестные ключиlayoutотбрасываются fail-closed (_project_layout), а не копируются. - Транспорт: фиксированный compile-time HTTPS-хост, редиректы отключены, таймауты 5/20с, ответ ограничен и не отражается, статусы мапятся на закрытый список кодов независимо от тела ответа.
- Relay:
X-Forwarded-Forберётся из последнего элемента только приtrusted_proxy=True(и запрещён к выключению за прокси текстом ТЗ и deploy-README); оба канала доставки не используютparse_mode, поэтому пользовательский текст не может быть разметкой; 36/36 тестов зелёные, три из них — специально проверенные владельцем и ревьюером ТЗ мутационные гварды (XFF spoofing, direct-node XFF-ignore, вебхук прикладывает вложение). Изменений в этом коде текущим диапазоном нет — унаследовано с зелёного спек-ревью r5 (SHA3ce5bc0e), но перепрочитано заново как часть этого, первого код-ревью, а не принято на веру. - «Одно число — один источник» для preview/download/submit: единственный расчёт байт/хеша на backend, единственный кешированный объект на фронтенде; см. §2.6.
- Трейлеры и changelog:
1e2e0fa6несётIssue: #43+User-Visible: yesс правками в обоих changelog в том же коммите; merge-коммит1ba53630—User-Visible: no, корректно.process-gate --issuesзелёный. - AC15 (отсутствие побочного влияния на diagnostics/backup/#295): 0 изменений
в
diagnostics.py/import_export.py/validation.py. - Деградация на старом backend:
compatible = this.host._haIntegrationVersion === CARD_VERSIONзакрывает форму отдельнымsupportupdate-блоком, About/Guide остаются доступны — совпадает с §10 ТЗ. scripts/process-gate.mjs::classify('scripts/support-relay/...')→'B', подтверждено и regression-тестом (test/process-gate.test.mjs, новый кейс «#43»), и живым прогоном — Medium-находка спек-ревью r1 закрыта на уровне файловой раскладки, а не только текста ТЗ.
6. Чего не проверял и почему
- Полный HA-харнесс (
test_ha_websocket.pyцеликом,test_ha_import_export.pyи т.д.) — песочница ревьюера не имеет Python 3.14/pytest-homeassistant-custom-component==0.13.357(пин CI), а неверсионированная установка дала диагностически бессмысленный результат (96 несвязанных провалов). AC9/AC10 закрыты пометкой «проверено чтением», не автотестом с моей стороны — см. §2.2 и таблицу AC. npm run invariants— диапазон не меняет хранимую геометрическую модель (validation.pyи модуль стен/толщины не тронуты), только читает её для экспорта; гейт не по предмету, не пропуск.npm run golden:verify— golden-сцен для этой фичи не существует вообще (см. находку M2); верифицировать нечего, сама находка это фиксирует.- Полный набор
demo/smoke_*.mjs— предрелизная обязанность (§8), к тому же ни один существующий смок не относится к этой фиче (см. §2.4); прогон всех ничего не доказал бы для этого диффа. - Числа из хендоффа автора (
npm test — 1723 passed,support relay — 36 passed) — перепроверены лично и совпадают. Числоprivacy/backend targeted — 15 passedне совпадает буквально с личным подсчётом (10+7=17 на моей урезанной установке пакетов) — возможно, иной набор-k; не рассматриваю как расхождение, потому что оба набора, которые смог прогнать сам, зелёные.
7. Итог
Функциональность реализована добросовестно и соответствует зелёному, пять раз провалидированному ТЗ по всем 17 AC на уровне логики: allowlist строится по белому списку, а не редактированием копии; авторизация и владение токеном корректны; transport и relay воспроизводят все инварианты, за которые владелец лично поручился пентестом. Ни одной High-находки — architecture привacy-границы не нарушена нигде, где я мог её проверить.
Три Medium-находки удерживают вердикт жёлтым, и все три — предметные, не
формальные: красный check-docs гарантированно красит CI на этом же SHA
(проверено сравнением с origin/dev), гейт no-new-any красный без единого
обоснования, а вся браузерная часть контракта (кнопка, тач-таргет, формы,
golden-сцены), которую само ТЗ называет способом доказательства четырёх AC,
не имеет вообще никакого покрытия на этом уровне. Все три чинятся в рамках
текущей задачи без изменения контракта.
Вердикт: жёлтый · заход r1 · блокирующих циклов 1/4 · High: 0 · Medium: 3 → в задаче