19 KiB
SPEC-REVIEW-385-r1
- Issue: https://github.com/Matysh/houseplan-card/issues/385
- Этап: ТЗ на ревью (PROCESS.md §2.4)
- Артефакт под ревью:
docs/specs/385-audit-lows.md, ревизия 1 (2026-08-30), коммитc28b2f2d("docs: specify #385 audit-lows batch") - Заход: r1 · блокирующих циклов израсходовано 0 из 4 (до этого вердикта)
- Ревьюер получил issue и ТЗ без устных пояснений автора (§2.4)
Скоуп
Четыре точечные правки из adversarial-аудита v1.69.0:
- (а)
houseplan-editor-runtime.ts— клик по уже выбранному binding-кандидату безусловно сбрасываетvalue_source/value_badge; - (б)
devices.ts—rewriteMarkerControlReferencesпишет фантомный ключvalue_source: undefined; - (в)
scripts/process-gate.mjs—releaseSourceViolationsOfсчитается для каждого коммита диапазона, а не только для релизных; - (г)
custom_components/houseplan/import_export.py— асимметрия форматов обезвреживания внешних ссылок при экспорте (полями vs удалением ключа).
Полный трек обоснован корректно: аналитика (комментарий владельца от 2026-08-30) явно называет непройденный критерий §5 — «одна поверхность» (четыре несвязанные поверхности: фронт-UI, фронт-модель, инфраструктурный скрипт, бэкенд), со ссылкой на прецедент SPEC-REVIEW-376-r1 H1 / #369. Это соответствует правилу AGENTS.md: «обычный трек» без названного критерия не основание, а здесь критерий назван.
Как проверялось
Первый заход — разбор полный, дельты нет. По каждому пункту (а)–(г) код прочитан напрямую и сверен построчно с утверждениями ТЗ (без исполнения — кода фичи ещё нет, это стадия спецификации):
src/houseplan-editor-runtime.ts:11961(_valueBadgeForBinding),:12252-12312(радио-веткаvirtualи клик по кандидатуcands.map) — подтверждён безусловный сбросvalueSource: null, valueSourceTouched: trueв обоих местах, оба вызывают_valueBadgeForBinding, который также безусловноvalueBadgeTouched: true. Утверждение (а) точное.src/devices.ts:868-892(rewriteMarkerControlReferences) — прослежена логика: для маркера безvalue_sourceпеременнаяvalueSourceостаётсяundefined, и если хотя быcontrols/value_badgeизменились, возвращаемый объект собирается через{ ...marker, controls, value_badge: valueBadge, value_source: valueSource }— ключvalue_sourceявно проставляется со значениемundefined. Утверждение (б) точное.scripts/process-gate.mjs:108-155(makeCommit,parseRecords) и:650-670(вызов из CLI) — подтверждено:releaseSourceViolationsOf(sha, files)вparseRecordsвызывается для каждого коммита без проверки релизности;isReleaseвычисляется отдельно внутриmakeCommit. Утверждение «сегодня вызывается для каждого коммита» точное — но см. находку M1 ниже про точность описания самого предиката релизности.custom_components/houseplan/import_export.py:504-519— подтверждена асимметрия форматов (badge["enabled"] = False; badge["source"] = Noneпротивmarker.pop("value_source", None)), оба пути инкрементируютdropped_marker_links. Утверждение (г) точное.docs/specs/378-value-face-source.md:85— подтверждена ссылка «при явной смене HA binding в диалоге старыйvalue_sourceсбрасывается»: контракт (а) действительно возвращает поведение к букве спеки #378, а не изобретает новое.- Существующие тесты сверены на предмет реалистичности плана автотестов:
test/devices.test.mjs:1900-1917(rewriteMarkerControlReferences) — готовая база для AC3;test/process-gate.test.mjs(parseRecords,isRelease) — готовая база для AC4;tests_backend/test_ha_import_export.py:1549-1589(badge-only и value_source-only кейсы отдельно, но не оба сразу на одном маркере) — подтверждает, что AC5 — новый, не задвоенный сценарий. - Гейты: код продукта не менялся (стадия — только ТЗ), поэтому typecheck/test/
build не гоняю — исполнять нечего, это не пропуск, а неприменимость гейта на
этой стадии.
node scripts/check-docs.mjsне запускался —src/**не тронут (изменился толькоdocs/specs/385-audit-lows.md, класс C).
Находки
Medium (в скоупе задачи — чинится в этом ТЗ, без него вердикт был бы зелёным)
M1. Контракт (в) описывает предикат isRelease неполно, что рискует
разойтись с AC4.
docs/specs/385-audit-lows.md, раздел «Проблема / Контракты по пунктам», (в):
потребляется она только стабильными релизными (
/^Release v\d/без beta/candidate, :134)
Фактический код, scripts/process-gate.mjs:133-135:
isRelease:
(/^Release v\d/.test(subject) && !/-(beta|rc|alpha)\.|candidate/i.test(subject))
|| Boolean(one('Release')),
Это ИЛИ из двух условий, а в тексте назван только первый дизъюнкт. Второй —
«есть трейлер Release: вообще» — делает isRelease: true для ЛЮБОГО
коммита с этим трейлером, включая бета-коммиты приёмки golden-эталонов: по
AGENTS.md («A commit touching demo/golden/baselines/** additionally
requires: Release: v1.62.0-beta.9») это штатная, частая категория.
Воспроизведение:
git show -s --format=%B 5f6ee65768788502ffb540c0df350d8f66fe7ce2
# Release: v1.69.0-beta.5 <- трейлер есть
# subject: "test: accept explicit value source golden" <- НЕ матчит /^Release v\d/
sed -n '133,135p' scripts/process-gate.mjs
# isRelease = (…regex…) || Boolean(one('Release')) → true для этого коммита
Почему это не «мелочь оформления»: AC4 требует «классификация релизности — та
же функция, что в гейте», и если реализатор напишет НОВУЮ функцию по описанию
из текста ТЗ (только первый дизъюнкт), а не буквально вынесет существующее
выражение целиком — гейт получит расхождение. Сценарий поломки: коммит,
легитимно несущий одновременно Issue:, Release: vX.Y.Z-betaN,
Baseline-Reviewed: И правки src/** (например, фикс + принятие эталона в
одном коммите — комбинация, которую AGENTS.md не запрещает). При суженном
предикате parseRecords для такого коммита НЕ вызовет
releaseSourceViolationsOf, оставив releaseSourceViolations: null; но
c.isRelease (вычисленный внутри makeCommit по полному, немодифицированному
выражению) останется true. В evaluateCommit (:196-203) сработает ветка
!Array.isArray(c.releaseSourceViolations) → violations = sources, то есть
ВСЕ тронутые src/**-файлы будут ошибочно засчитаны как «не-версионное
изменение продукта» — ложный отказ pre-push/CI на легитимном коммите.
AC4 сам по себе (юнит на идентичность/переиспользование предиката) должен бы поймать такую реализацию при код-ревью, поэтому до продакшена дефект скорее всего не дойдёт — но именно от того, что текст ТЗ читается раньше AC и описывает предикат неточно, растёт риск, что реализатор напишет «упрощённую», а не «вынесенную» версию, и код-ревью придётся ловить это постфактум вместо того, чтобы ТЗ прямо предотвратило ошибку.
Требуется на этом раунде: заменить фразу «потребляется она только
стабильными релизными (/^Release v\d/ без beta/candidate, :134)» на точную:
предикат — это ВСЁ булево выражение isRelease (оба дизъюнкта, включая
Boolean(one('Release'))), и контракт (в) требует буквально вынести именно
это выражение в общую функцию, а не переформулировать его по памяти.
Low (правится или снимается решением ревьюера — не блокирует)
L1. «План автотестов» ссылается на несуществующий тестовый паттерн.
Текст: «паттерн существующих тестов _valueBadgeForBinding». Проверено:
grep -rn "_valueBadgeForBinding" test/ — ноль совпадений; метод не
покрыт ни одним существующим тестом (используется только в
src/houseplan-editor-runtime.ts и src/houseplan-card.ts). Технический
вопрос (стратегия теста — домен автора, PROCESS §7.1), снимаю без возврата:
реализатору предстоит написать harness с нуля, а не переиспользовать образец.
Полезно поправить текст, но не блокирует переход в S5-ready.
L2. DoR-примечание не называет явно влияние на производительность.
PROCESS.md §2.5 требует пункт «влияние на производительность и бюджеты
названо (или явно «нет»)» отдельно от touch/миграции. Текущая строка
DoR-примечания: миграция/compatibility — нет; touch — не влияет эту графу
пропускает, хотя пункт (в) сам по себе — перф-правка гейта (снижает число
git show на нерелизных диапазонах). Достаточно одной строки в этом же
разделе; не блокирует, чиню на усмотрение автора.
L3. Формулировка «(б)–(г) — «мелкие уточнения»» в Release-артефактах
двусмысленна. Раздел UX того же документа прямо говорит: «Пункты (б)–(г)
видимого поведения не меняют» — по конвенции AGENTS.md это кандидаты на
User-Visible: no, для которых правка changelog не требуется вовсе. Фраза
«мелкие уточнения» читается как обещание отдельных строк в пользовательском
changelog для невидимых правок, что противоречит соседнему же разделу.
Не блокирует (это описание коммит-стратегии, домен автора), но стоит явно
решить: одна строка про (а) в едином коммите (если а–г идут одним коммитом
User-Visible: yes), либо явное «(б)–(г) отдельными User-Visible: no
коммитами без changelog».
Что проверено и корректно
- Все четыре описания дефектов (а)–(г) в разделе «Проблема / Контракты» сверены построчно с текущим кодом и точны (см. «Как проверялось»).
- Обязательные разделы §7.1 присутствуют: сценарий, что человек увидит до/после, проблема+контракт, скоуп/не-скоуп, UX, модель данных/миграция, i18n, AC1–AC6 с указанием способа доказательства, план автотестов, риски, откат, release-артефакты.
- Ни одно утверждение о поведении не выдано за факт без опоры: контракт (а)
прямо ссылается на букву спеки #378 §1.6 и это подтверждено чтением
docs/specs/378-value-face-source.md:85; контракт (г) явно и осознанно выбирает НЕ менять формат хранения («Формат хранения менять нельзя — это ломало бы round-trip»), с обоснованием, а не как невысказанное предположение. - Открытых продуктовых вопросов нет — и это оправдано: (а) не вводит новый UX, а восстанавливает уже описанное поведение #378 §1.6; (б)–(г) не видны пользователю вообще. Ни один вопрос из вынесенных в тело ТЗ не относится к «что человек видит/делает» — правильно, что автор не эскалировал ничего владельцу.
- AC1–AC6 пронумерованы, у каждого указан способ доказательства
(unit/smoke/pytest), формулировки однозначны (например, AC3 буквально даёт
проверяемое выражение
'value_source' in marker === false), для AC1/AC4 описаны и позитивная, и регрессная ветки — тест «умеет упасть» показан явно через раздел «Мутанты». - AC5 — не задвоенный сценарий: существующие pytest-тесты покрывают
badge-only (
test_issue_90...) и value_source-only (test_issue_378...) по отдельности, но не комбинацию на одном маркере — подтвержденоgrep. - Откат простой и честный (
git revert, нет флагов/миграций/персистентных данных) — соответствует малому риску задачи. - i18n корректно помечен как незадетый — ни один пункт не добавляет строк интерфейса (проверено — п.(а) в UX-разделе прямо говорит «Новых строк нет», и ни один из четырёх контрактов не описывает новый текст).
Чего не проверял
- Код продукта не существует на этой стадии (спека, не реализация) — гейты
typecheck/test/build/check-docs/invariants/смоки/golden:verifyнеприменимы и не запускались; это нормально для этапа ТЗ, а не пропуск. - Не проверял, действительно ли
_boolInput/остальной рендер диалога маркера не имеет ТРЕТЬЕГО пути выбора binding помимо названных :12260 и :12308 — сам автор в разделе «Риски» просит ревьюера перепроверитьgrep'ом; я прогналgrep -n "_valueBadgeForBinding" src/houseplan-editor-runtime.tsи получил ровно два вызова внутри шаблона (:12260, :12308) плюс определение (:11961) — третьего пути нет. Это подтверждает риск закрытым, но не гарантирует отсутствие иного, не через_valueBadgeForBinding, места записиvalue_source/value_badge— за пределами grep по одному имени я не искал. - Не оценивал сравнительную стоимость 2×
git showна коммит в (в) в реальных цифрах (сколько коммитов в типичном диапазоне) — контракт и AC4 не требуют числового бюджета, только факта отсутствия лишних вызовов, этого достаточно для проверки.
Вердикт
Жёлтый: одна находка Medium в скоупе задачи (M1), High нет. Возврат автору на правку текста ТЗ (раздел «Проблема / Контракты», пункт в) — код не пишется, правки текстовые. После правки — повторный заход по дельте (PROCESS.md §2.10): достаточно перечитать изменённый абзац (в) и подтвердить, что предикат назван полностью и AC4 остаётся согласован.