Files
houseplan-card/docs/reviews/SPEC-REVIEW-385-r1.md
T
2026-08-30 07:41:48 +00:00

19 KiB
Raw Blame History

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