# 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`: ```js 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 остаётся согласован.