diff --git a/docs/specs/398-sysmodules-guard-scope.md b/docs/specs/398-sysmodules-guard-scope.md index 601f33a7..91787426 100755 --- a/docs/specs/398-sysmodules-guard-scope.md +++ b/docs/specs/398-sysmodules-guard-scope.md @@ -3,7 +3,7 @@ - Issue: https://github.com/Matysh/houseplan-card/issues/398 - Приоритет: P2, infra/tests; полный трек — класс B (тесты и гейт), меняется правило, а не продуктовый код -- Ревизия: 1 (2026-08-31) +- Ревизия: 2 (2026-08-31) — по SPEC-REVIEW-398-r1 (Medium: путь 2 несовместим с AC4) ## Сценарий @@ -68,20 +68,23 @@ custom_components.houseplan.coordinate_canonicalization статически — тогда гвард обязан отказывать (fail-closed): «не могу доказать, что ключ не из `custom_components`» — это отказ, а не пропуск. -**Что делать с `load_pure`.** Два допустимых исхода, выбор — за реализацией -после замера: +**Что делать с `load_pure` — один путь: снимать за собой.** Ревизия 1 +допускала второй исход (объявить `pure_imports.py` вторым легальным +исключением), и ревьюер справедливо показал, что он несовместим с AC4: +регистрация тогда остаётся, и требование «после прогона нет лишних ключей» +краснеет по построению. Держать в ТЗ путь, ведущий в заведомо красный +критерий, нельзя — вариант снят. -1. **Снимать за собой.** `load_pure` восстанавливает `sys.modules` к состоянию - до вызова (образец — обратимая подмена в `scripts/dump-config-schema.py` - после #389). Плюс: класс закрыт полностью. Минус: относительные импорты - внутри загруженного модуля должны успеть отработать до восстановления — - проверить, что `exec_module` завершается раньше. -2. **Объявить единственным легальным исключением** рядом с conftest, с - комментарием, почему именно ему можно, и с тестом, фиксирующим, что список - исключений состоит ровно из двух файлов и не растёт. +`load_pure` восстанавливает `sys.modules` к состоянию до вызова (образец — +обратимая подмена в `scripts/dump-config-schema.py` после #389). Существенная +деталь, выясненная замером: снимать только собственное имя **недостаточно**. +Относительные импорты внутри загружаемого модуля подтягивают соседей, и после +загрузки `junction_limits` в `sys.modules` остаются ещё `wall_segment_model` и +`coordinate_canonicalization`. Снимать нужно всё, что появилось под префиксом +`custom_components` за время `exec_module`, — тогда после вызова остаются ровно +пустышки conftest. Проверено исполнением, включая повторный вызов подряд. -Предпочтителен (1): исключение, которое нельзя объяснить в одну строку, -завтра станет прецедентом. +Единственное исключение остаётся одно — `tests_backend/conftest.py`. ## Скоуп / не-скоуп @@ -114,9 +117,8 @@ custom_components.houseplan.coordinate_canonicalization `sys.modules.setdefault(...)`/`update(...)` тоже отклоняются либо приводят к явному отказу «не могу доказать безопасность». Доказательство: по одному контракту на форму. -- **AC3**. `tests_backend/conftest.py` остаётся разрешённым, и это единственное - исключение (либо два файла, если выбран путь 2 — тогда список - зафиксирован тестом и его рост краснеет). +- **AC3**. `tests_backend/conftest.py` остаётся разрешённым и остаётся + единственным исключением; список зафиксирован тестом, его рост краснеет. - **AC4**. После прогона всего `tests_backend/` в `sys.modules` нет ключей `custom_components*` сверх тех, что положил conftest. Доказательство: исполняемая проверка, а не чтение исходников. @@ -134,8 +136,7 @@ custom_components.houseplan.coordinate_canonicalization 1. Синтетический файл с записью через переменную → гвард краснеет (AC1). 2. По контракту на каждую форму из AC2. 3. `conftest.py` со своей условной подменой → зелено (AC3). -4. Попытка добавить третий файл в список исключений (если выбран путь 2) → - красный. +4. Попытка добавить второй файл в список исключений → красный. **Backend** (`tests_backend/test_backend_quality.py` или новый файл): @@ -155,10 +156,15 @@ custom_components.houseplan.coordinate_canonicalization строковых литералах; смягчение: тест-контракты на «похожий, но безопасный» код (например, `# sys.modules[...]` в комментарии) должны оставаться зелёными. -- **Снятие регистрации ломает относительные импорты.** Если выбран путь (1) и - восстановление происходит слишком рано, `from .x import y` внутри модуля - упадёт. Смягчение: восстановление строго после `exec_module`, AC5 покрывает - оба существующих места использования. +- **Снятие регистрации ломает относительные импорты.** Восстановление слишком + рано уронит `from .x import y` внутри модуля. Смягчение: восстановление + строго после `exec_module` (замер: модуль полностью рабочий после очистки), + AC5 покрывает оба существующих места использования. +- **Соседи, подтянутые относительными импортами.** Снятие одного лишь + собственного имени оставляет `wall_segment_model` и + `coordinate_canonicalization` — замер это показал. Смягчение: снимать + разницу по префиксу `custom_components`, а не одно имя; AC4 проверяет + исполнением итог, а не намерение. - **Скрытая зависимость от повторного импорта.** Модуль, загруженный дважды, даёт два объекта классов; тесты, сравнивающие типы, могли на этом молча держаться. Смягчение: AC5 гоняет весь набор, а не отдельный файл.