20 KiB
SPEC-REVIEW-398-r2
- Issue: https://github.com/Matysh/houseplan-card/issues/398
- Этап: ТЗ на ревью (PROCESS.md §2.4)
- Материал:
docs/specs/398-sysmodules-guard-scope.md, коммит480d202f(«#398 spec revision 2 per SPEC-REVIEW-398-r1») - Заход: r2 · блокирующих циклов израсходовано до этого вердикта: 1 из 4
- Предыдущий вердикт:
docs/reviews/SPEC-REVIEW-398-r1.md, жёлтый, на коммите69dd09a7(ревизия 1 ТЗ). SHA в вердикте r1 не назван явно текстом — найден сопоставлением:git show a4b686a7 --stat(коммит с самим документом r1) датирован позже69dd09a7и раньше480d202f, а «Материал» в r1 называет файл без ревизии — правки после которой ещё не было, то есть r1 читал ровно содержимое69dd09a7.
Разбор по дельте (PROCESS.md §2.9, #214)
Дельта — git diff 69dd09a7..480d202f -- docs/specs/398-sysmodules-guard-scope.md
(28 добавленных / 22 удалённых строк). Правка не переезжает на другую подсистему,
не меняет продуктовый контракт (задача остаётся User-Visible: no, класс B) и по
объёму меньше исходной задачи — полный повторный разбор не требуется. Проверены
только: (а) закрытие находки r1 по цитате нового текста и (б) те AC, чьё
доказательство дельта задевает — это AC3, AC4 и, как показано ниже, обнаружилось,
что дельта задевает ещё и совместимость AC1/AC2 с AC3 через код pure_imports.py,
явно оставленный «в скоупе» правки. Разделы, которых дельта не касалась (Сценарий,
UX, i18n, AC5, AC6, откат, release-артефакты), унаследованы из r1 — см. раздел
ниже.
Закрытие раунда r1
| Находка r1 | Чем закрыта | Где это видно |
|---|---|---|
Medium: AC3 явно допускал «путь 2» (объявить pure_imports.py вторым легальным исключением), а AC4 требовал отсутствия лишних ключей после прогона — путь 2 конструктивно не мог пройти AC4 |
«Путь 2» полностью удалён из текста; раздел «Что делать с load_pure» переписан как «один путь: снимать за собой», с явной ссылкой на находку ревьюера |
docs/specs/398-sysmodules-guard-scope.md:71-87 («Ревизия 1 допускала второй исход… вариант снят»); AC3 (строки 120-121) больше не содержит альтернативы «либо два файла»; план автотестов, п.4 (строка 139) убрал условность «если выбран путь 2» |
Текстовое закрытие корректно и однозначно: альтернативный путь убран, а не спрятан («не рекомендуется», «как правило» и т.п. отсутствуют — использована формулировка «вариант снят»).
Новая находка r2
Medium (в скоупе задачи) — устранение «пути 2» открывает новый конфликт: AC1/AC2 не совместимы с AC3 для tests_backend/pure_imports.py, оставленного «в скоупе»
Ревизия 2 фиксирует ровно одно исключение из-под гварда —
tests_backend/conftest.py (строка 87: «Единственное исключение остаётся одно»;
AC3, строки 120-121: «остаётся единственным исключением»; план автотестов,
п.4, строка 139: «попытка добавить второй файл в список исключений →
красный» — то есть механизм исключения зафиксирован именно как список файлов
мощности 1, а не как распознавание безопасного паттерна «запись + восстановление»).
Одновременно раздел «Скоуп/не-скоуп» (строка 91) оставляет
tests_backend/pure_imports.py в скоупе как файл, который правка изменяет (а не
удаляет), и требует (через «Что делать с load_pure», строки 78-87, и AC5,
строки 125-127), чтобы после правки load_pure продолжал резолвить
относительные импорты для существующих вызовов — в частности,
tests_backend/test_junction_limits.py:28-30, который передаёт канонический
литерал "custom_components.houseplan.junction_limits" в качестве аргумента:
sys.modules[name] = module # tests_backend/pure_imports.py:31
Это ровно образец, который AC1 называет напрямую: «Файл, записывающий
sys.modules[<переменная>] = …, где переменная может содержать имя пакета
интеграции, отклоняется гвардом» (строки 112-114). Значение name внутри
pure_imports.py — параметр функции, из текста самого файла статически не
определимо, что оно не начинается с custom_components; собственный
fail-closed принцип ТЗ («значение ключа может быть неизвестно статически —
тогда гвард обязан отказывать», строки 67-69) требует отклонить именно этот
файл. Проверка в test/backend-test-hygiene.test.mjs статическая, идёт
файл-за-файлом без исполнения (комментарий в коде, строки 11-14: «Проверка
статическая — по исходникам, без запуска») — она не может увидеть, что
load_pure восстанавливает sys.modules после exec_module; восстановление
— рантайм-факт, а гвард текст не исполняет.
Итог: pure_imports.py, оставленный в скоупе и обязанный по AC5 продолжать
работать для test_junction_limits.py, структурно не может одновременно (а)
не быть исключением (по AC3, ровно один файл-исключение — conftest.py) и (б)
избежать отклонения по AC1 (поскольку пишет в sys.modules по переменной,
которая в вызовах из скоупа действительно принимает значения вида
custom_components.houseplan.*). Здесь нет обходного варианта на уровне формы
записи: регистрация модуля в sys.modules под точным каноническим именем —
обязательное условие резолва относительных импортов в Python
(importlib.util.module_from_spec + exec_module), других способов сделать
from .wall_segment_model import … рабочим для этого файла ТЗ не описывает и
предложить не может без изменения самого механизма загрузки.
Почему это находка ТЗ, а не код-ревью. Это тот же класс дефекта, что и
закрытая находка r1: набор AC совместно невыполним для сценария, который сам
документ называет обязательным (пункт «в скоупе» + AC5). Разработчик,
реализующий ТЗ буквально — упрощённая по AC1/AC2 регулярка/AST-проверка плюс
очистка sys.modules в load_pure — получит перманентно красный
test/backend-test-hygiene.test.mjs на собственном pure_imports.py, то есть
красный npm test сразу после реализации, и упрётся в противоречие, которое
было видно уже на этапе чтения ТЗ.
Как править (для автора, не предписание). Разрешить pure_imports.py
вторым явным исключением в AC3 (список из двух конкретных, поимённо
зафиксированных файлов — conftest.py и pure_imports.py, рост списка сверх
этого по-прежнему красный) — это НЕ реинкарнация снятого «пути 2»: там
исключение освобождало от очистки и потому ломало AC4; здесь очистка (уже
специфицированная в ревизии 2 как «снимать всё, что появилось под префиксом
custom_components за время exec_module») остаётся обязательной сама по
себе и закрывает AC4 независимо от статуса файла в списке исключений
статического гварда. Иными словами: исключение из AC1-проверки (статический
факт «этому файлу разрешено писать») и обязанность очистки по AC4
(рантайм-факт «после прогона лишнего не осталось») — два независимых
требования, и revision 2 верно оставила второе, но по инерции убрала и первое,
хотя первое было не источником проблемы. Альтернатива — доказать в тексте, что
name в pure_imports.py статически ограничен паттерном, не совпадающим с
custom_components* (не просматривается, как это сделать без потери гибкости
вызова с каноническим именем).
Воспроизведение (разбор по коду, не исполнено как эксперимент — гварда,
описанного ТЗ, ещё не существует в коде): применить контракт AC1 к
tests_backend/pure_imports.py:31 буквально даёт «отклонить», а вызов из
tests_backend/test_junction_limits.py:28 требует, чтобы файл продолжал
работать. Оба требования одновременно предъявлены этой же ревизией ТЗ (AC1 +
скоуп + AC5).
Унаследовано из r1
Без повторной проверки в этом раунде — дельта не касалась этих участков текста и они не входят в цепочку доказательства новой находки:
- Обязательные продуктовые разделы («Сценарий», «Что человек увидит до и
после») и корректность персоны —
docs/reviews/SPEC-REVIEW-398-r1.md, раздел «Что проверено и корректно», п.1, на SHA69dd09a7(текст этих разделов дельтой не тронут). Как проверить самостоятельно, что раздел не менялся: сравнитьgit diff 69dd09a7..480d202f -- docs/specs/398-sysmodules-guard-scope.md— «Сценарий» и «Что человек увидит» отсутствуют среди затронутых hunks. - Полнота обязательных разделов §7.1 (проблема, скоуп/не-скоуп, UX Н/П, модель данных и миграция Н/П, i18n Н/П, план автотестов, откат, release-артефакты) — там же, п.2. Структура не менялась, менялось только содержимое трёх блоков (проблема/контракт, AC3, план автотестов п.4), уже разобранных выше.
- Соответствие
docs/SCOPE.md: задача не претендует на строку Core user jobs, чисто инфраструктурная — там же, п.5. Не переоценивалось: дельта не меняет ни продуктовую рамку, ниUser-Visible. - AC2, AC5, AC6 — однозначны и проверяемы каждый по отдельности — там же, п.4 (частично; см. ниже уточнение по AC1). Текст этих критериев дельтой не тронут (см. диф выше — правки только в AC3 и в тексте перед AC-блоком). Уточнение к унаследованному: r1 также назвал AC1 «однозначным» — это верно в изоляции (формулировка ясна), но r1 не проверял совместность AC1 с AC3 и скоупом; это ровно то, что находка r2 закрывает как пробел прошлого раунда, а не противоречие ему.
- Риски «разбор Python из Node» и «скрытая зависимость от повторного импорта» — назывались по существу уже в r1, текст не менялся (третий риск, «снятие регистрации ломает относительные импорты», получил только уточнение «замер: модуль полностью рабочий после очистки» — редакционное, не меняет сути) — там же, п.6. Новый риск ревизии 2 («соседи, подтянутые относительными импортами») проверен заново в этом раунде, см. «Что проверено и корректно» ниже.
- Откат и release-артефакты — не изменены дельтой, наследуются из п.7-8 того же документа.
- Наблюдение 1 (напряжение формулировок «контракт vs риски» про способ разбора) и наблюдение 2 (маршрутизация issue мимо инфраструктурного пути) — не блокировали в r1 и не затронуты дельтой; остаются наблюдениями, не находками.
Что проверено и корректно (в этом раунде)
- Закрытие находки r1 текстуально полное: «путь 2» не просто ослаблен формулировкой, а вычеркнут вместе со всеми его следами в AC3 и плане автотестов (сверено построчно по диффу выше).
- Новый риск «Соседи, подтянутые относительными импортами» (строки 163-167)
назван конкретно, с перечислением реальных имён модулей
(
wall_segment_model,coordinate_canonicalization) и с механизмом смягчения (диф по префиксу вокругexec_module, а не снятие одного имени) — проверено сопоставлением с фактическим поведением, описанным в разделе «Проблема и контракт» (те же три модуля названы там как реально оставшиеся послеimport test_junction_limits). docs/SCOPE.mdпо-прежнему не нарушается: правка не расширяет заявленную поверхность, остаётся полностью инфраструктурной.
Чего не проверял
- Не гонял
npx tsc --noEmit/npm test/npm run build— на этапе ревью ТЗ нет продуктового или тестового кода, есть только правкаdocs/specs/*.md; эти гейты неприменимы. (То же основание, что и в r1.) - Не проверял эксперимента «разбор синтаксиса Python из Node» — вопрос остаётся открытым наблюдением из r1, не заново поднятым в этом раунде.
- Не перечитывал
tests_backend/test_validation.pyцеликом повторно — его собственный_load_pureиспользует префиксhp_pure.*, статически не совпадающий сcustom_components*(литеральная часть f-строки/конкатенации проверяема по AC2), и это уже подтверждено в r1; для новой находки этот файл не релевантен, она проpure_imports.py. - Не проверял, можно ли реализовать распознавание паттерна «запись сразу с последующим восстановлением» как альтернативу пофайловому списку исключений — это один из вариантов правки, adресованных автору, а не факт, который нужно доказывать ревьюеру; сам документ фиксирует именно файловый список (AC3 + план автотестов п.4), и находка оценивает именно этот, явно выбранный автором механизм.
Вердикт
Жёлтый. Единственная блокирующая находка (Medium, в скоупе задачи) — устраняя
находку r1 удалением «пути 2», ревизия 2 разорвала совместность AC1/AC2 (гвард
обязан отклонить запись pure_imports.py:31 по правилу fail-closed) и AC3
(единственное разрешённое исключение — только conftest.py) при том, что
pure_imports.py явно оставлен в скоупе и обязан по AC5 продолжать работать
для test_junction_limits.py. Правится в тексте ТЗ тем же автором, без выхода
за рамки текущей задачи — вероятный минимальный фикс: вернуть
pure_imports.py в список статических исключений (не путать со снятым «путём
2» — обязанность очистки sys.modules по AC4 остаётся в силе независимо от
статуса файла в списке исключений гварда).