# SPEC-REVIEW-398-r1 - Issue: https://github.com/Matysh/houseplan-card/issues/398 - Этап: ТЗ на ревью (PROCESS.md §2.4) - Материал: `docs/specs/398-sysmodules-guard-scope.md`, коммит `69dd09a7` (тело issue #398 без изменений) - Заход: r1 · блокирующих циклов израсходовано до этого вердикта: 0 из 4 - Комментариев в issue к моменту ревью: 0 (первый заход, полный разбор) ## Скоуп проверки Issue #398 просит расширить гвард `test/backend-test-hygiene.test.mjs`, который сегодня ловит запись стаба интеграции в `sys.modules` только по строковому литералу и поэтому не видит `tests_backend/pure_imports.py:31` (`sys.modules[name] = module`, запись через переменную). Изменение полностью внутри класса B (`test/**`, `tests_backend/**`), не трогает `src/**` и `custom_components/**/*.py`, `User-Visible: no`. ТЗ оформлено полным треком — `docs/specs/398-sysmodules-guard-scope.md`, файл на месте, ссылка issue↔ТЗ двусторонняя. ## Как проверялось 1. Прочитаны `docs/SCOPE.md`, `AGENTS.md`, `PROCESS.md` целиком. 2. Прочитано тело issue #398 и лента меток (`gh api .../timeline`) — задача ушла `S1-new → S3-spec → S4-spec-review` без публичного комментария аналитики (см. «Наблюдения», п.2). 3. Прочитан ТЗ `docs/specs/398-sysmodules-guard-scope.md` целиком. 4. Сверены с реальным кодом все цитаты из ТЗ: `test/backend-test-hygiene.test.mjs` (регэксп на строке 39), `tests_backend/pure_imports.py` (строка 31), `tests_backend/conftest.py` (условная подмена под `if not HAS_HA:`), `tests_backend/test_junction_limits.py` (безусловный вызов `load_pure` с каноническим именем `custom_components.houseplan.junction_limits`), `tests_backend/test_validation.py` (собственный `_load_pure`, пишет в `sys.modules` под префиксом `hp_pure.*`, вне описанного скоупа гварда). 5. Канонические документы подсистем (`SUN.md`, `LIGHT.md`, `CANVAS.md`, `WALL-THICKNESS.md`, `UX-MODES.md`, `CONFIG-COMPATIBILITY.md`, `TOUCH-SUPPORT.md`) — не применимы, задача не меняет видимое поведение продукта. 6. `docs/USER-GUIDE.ru.md` — не применим, `User-Visible: no`, персона — разработчик, а не пользователь продукта. 7. Гейты кода не гонялись: на этапе ревью ТЗ продуктового/тестового кода ещё нет, есть только новый markdown-файл — `typecheck`/`test`/`build` к нему неприменимы. ## Находки ### Medium (в скоупе задачи) — AC3 разрешает «путь 2», AC4 делает его невозможным `docs/specs/398-sysmodules-guard-scope.md:73-84` называет два одинаково допустимых исхода для `load_pure`: 1. снимать регистрацию за собой после `exec_module` (самоочистка); 2. объявить `pure_imports.py` вторым легальным исключением рядом с `conftest.py`, с тестом, фиксирующим список ровно из двух файлов. Текст явно оставляет выбор реализации: «Предпочтителен (1)… — но завтра станет прецедентом» (строка 83) описывает предпочтение, не запрет второго пути, и **AC3** (строки 117-119) прямо предусматривает путь 2: «единственное исключение (**либо два файла, если выбран путь 2** — тогда список зафиксирован тестом и его рост краснеет)». Но **AC4** (строки 120-122) требует: «После прогона всего `tests_backend/` в `sys.modules` нет ключей `custom_components*` **сверх тех, что положил conftest**». Это не совместимо с путём 2 по конструкции: если `pure_imports.py` объявлен вторым легальным исключением и не убирает за собой запись, то после прогона `tests_backend/` в `sys.modules` останутся ключи `custom_components.houseplan.junction_limits`, `custom_components.houseplan.wall_segment_model`, `custom_components.houseplan.coordinate_canonicalization` — ровно то, что issue проверил исполнением как текущее наблюдаемое поведение — и они не «положены conftest», а положены вторым разрешённым файлом. AC4 в буквальной формулировке тогда красный при полностью корректной, явно допустимой реализации. **Почему это находка ТЗ, а не мелочь для код-ревью.** DoR требует «пронумерованные проверяемые критерии приёмки» — набор AC должен быть одновременно выполним. Сейчас AC3 и AC4 совместно выполнимы только при выборе пути (1); реализатор, прочитавший ТЗ и выбравший путь (2) как более простой (сам документ признаёт его допустимым и даже кладёт под него отдельный тест-пункт «план автотестов», п.4), гарантированно упрётся в красный AC4 при исполняемой проверке — и потратит цикл код-ревью на то, что было видно уже на этапе ТЗ. **Как править (для автора, не предписание):** либо явно исключить путь 2 из допустимых исходов (тогда «выбор — за реализацией» в контракте лишний — есть только путь 1), либо переформулировать AC4 так, чтобы он допускал оба исхода: «…нет ключей `custom_components*` сверх положенных `conftest.py` **и**, если выбран путь 2, кроме предъявленных явным списком исключений в `pure_imports.py`». **Воспроизведение** (не гипотеза — уже исполнено issue-автором и переподтверждено при разборе): `cd tests_backend && python3 -c "import conftest, test_junction_limits, sys; print([k for k in sys.modules if k.startswith('custom_components')])"` печатает три ключа `junction_limits`/`wall_segment_model`/`coordinate_canonicalization` сверх пустышек `custom_components`/`custom_components.houseplan`, поставленных `conftest.py`. При выборе пути 2 эти три ключа остаются легальными по AC3, но проваливают AC4 буквально. ## Наблюдения (не блокируют, не находки к ТЗ) 1. **Контракт vs риски о способе разбора.** Раздел «Проблема и контракт» говорит «Форма проверки меняется с регулярного выражения на разбор синтаксиса Python» (что совпадает с просьбой issue: «разбор AST вместо регулярки»), а раздел «Риски» тут же ограничивает: «Полноценный парсер тянуть не нужно и нельзя: достаточно узкого разбора строк… с fail-closed на всё непонятое». Формулировки не запрещают друг друга буквально (узкий разбор конкретно конструкции присваивания — тоже «разбор синтаксиса», не обязательно generic-грамматика), но провисает связка: если ключевая гарантия ТЗ — «отказ на всё недоказуемое» — реализована самим fail-closed по умолчанию, то выбор между вызовом Python `ast` через subprocess (0 новых зависимостей, репозиторий и так тянет Python 3.13) и узким текстовым разбором в Node не влияет на корректность настолько, насколько намекает контраст формулировок. Технический вопрос — не продуктовый, решается автором/ревьюером по существу, не эскалируется владельцу; не блокирую, но рекомендую снять формулировочное противоречие в следующей редакции. 2. **Маршрутизация issue мимо инфраструктурного пути.** Диапазон правки — исключительно класс B (`test/**`, `tests_backend/**`), ни одного файла класса A. По `AGENTS.md` («Infrastructure-only work runs outside this flow… The test… is mechanical: not a single class A file») и `PROCESS.md` §1 такая задача идёт «без ТЗ, ревью ТЗ, код-ревью и без прохода по статусам». Лента меток issue (`gh api .../timeline`) показывает, что все переходы `S1-new → S3-spec → S4-spec-review` проставил лично владелец (`Matysh`) без публичного комментария аналитики S2 — то есть это не сбой автоматики, а его собственное решение вести задачу полным треком. Это не дефект ТЗ и не встаёт в оценку документа: владелец вправе выбрать больше строгости для третьего по счёту случая одного класса дефектов. Фиксирую как наблюдение для гигиены процесса, не как находку ревью. ## Что проверено и корректно - Оба обязательных продуктовых раздела на месте: «Сценарий» называет персону верно (разработчик, добавляющий backend-тест — не пользователь продукта по `docs/SCOPE.md`, что для чисто инфраструктурной задачи ожидаемо) и поверхность (backend test harness, `tests_backend/`); «Что человек увидит до и после» отвечает без терминов реализации. - Все обязательные разделы §7.1 присутствуют: проблема, скоуп/не-скоуп, контракт, UX (Н/П), модель данных и миграция (Н/П), i18n (Н/П), AC1…AC6 с указанием доказательства, план автотестов, риски, откат, release-артефакты. - Цитаты кода в ТЗ (регэксп `test/backend-test-hygiene.test.mjs:39`, запись `pure_imports.py:31`) дословно совпадают с текущим содержимым файлов — не придуманы задним числом. - AC1, AC2, AC5, AC6 однозначны и проверяемы: у каждого назван способ доказательства (тест-контракт по форме записи, backend-прогон, мутант). AC2 корректно перечисляет конкретные проверяемые формы (f-строка, конкатенация, `setdefault`/`update`) вместо общего «и другие похожие». - Скоуп/не-скоуп разграничены точно: `tests_backend/conftest.py` (контракт #394) и правило про `sys.path` (#393) явно исключены из скоупа этой задачи — граница, названная в issue #394, не ослабляется этим ТЗ. - `docs/SCOPE.md` не нарушается: задача не претендует ни на одну строку Core user jobs, это ожидаемо для чисто инфраструктурного изменения, и ТЗ не делает вид, что решает продуктовую задачу. - Риски названы по существу (разбор Python из Node, снятие регистрации ломает относительные импорты, скрытая зависимость от повторного импорта) со смягчениями, а не общими словами. - Откат дешёвый и конкретный: «два коммита назад, продуктовый код не затронут» — соответствует факту (диапазон правки — исключительно class B). - Ни одна догадка о поведении не выдана за факт: утверждения о текущем дефекте («переживает свой тест», перечень трёх модулей) сопровождаются пометкой «проверено исполнением» и совпадают с тем, что показал сам issue. ## Чего не проверял - Не гонял `npx tsc --noEmit` / `npm test` / `npm run build` — на этапе ревью ТЗ нет ни строки продуктового или тестового кода, есть только новый `docs/specs/*.md`; эти гейты неприменимы к чтению документа. - Не проверял технической реализуемости «разбора синтаксиса Python из Node» экспериментом (не писал прототип) — вопрос вынесен в наблюдение 1 как формулировочный, а не как проверенный тупик. - Не проверял `tests_backend/test_validation.py` построчно за пределами найденного вызова `_load_pure`/`sys.modules[f"hp_pure...`] — этого было достаточно, чтобы подтвердить корректность утверждения ТЗ «при необходимости» про этот файл (префикс `hp_pure` вне заявленного скоупа гварда `custom_components*`); полный разбор файла оставляю код-ревью, если реализация всё же тронет этот файл. ## Вердикт Жёлтый. Единственная блокирующая находка (Medium, в скоупе задачи) — несовместимость AC3 и AC4 для явно допущенного документом «пути 2»: описанный как разрешённый исход не может пройти собственный исполняемый критерий приёмки. Правится в тексте ТЗ этим же автором, без выхода за рамки текущей задачи.