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

222 lines
19 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 остаётся согласован.