mirror of
https://github.com/Matysh/houseplan-card
synced 2026-10-03 05:08:53 +00:00
222 lines
19 KiB
Markdown
222 lines
19 KiB
Markdown
# 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 остаётся согласован.
|