mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-30 11:49:16 +00:00
145 lines
11 KiB
Markdown
145 lines
11 KiB
Markdown
# ТЗ #385 — Пачка Low аудита v1.69.0 (а–г)
|
||
|
||
- Issue: https://github.com/Matysh/houseplan-card/issues/385
|
||
- Приоритет: P3, polish; полный трек (четыре несвязанные поверхности — фронт-UI,
|
||
фронт-модель, инфраструктурный скрипт, бэкенд; критерий «одна поверхность» §5
|
||
не выполняется — прецеденты #369, SPEC-REVIEW-376-r1 H1)
|
||
- Ревизия: 2 (2026-08-30) — по SPEC-REVIEW-385-r1 (M1 + Low)
|
||
|
||
## Сценарий
|
||
|
||
Четыре шероховатости, найденные adversarial-аудитом стабильной v1.69.0 — каждая
|
||
мала, вместе — гигиена свежего кода #378/#379. Пользователь видит одно: (а)
|
||
случайный повторный клик по уже выбранному binding в диалоге маркера больше не
|
||
стирает молча настроенный источник значения (и «Значение + состояние», и
|
||
бейджа). Остальное — невидимая честность модели, гейта и экспорта.
|
||
|
||
## Что человек увидит до и после
|
||
|
||
**До**: повторный клик по своему же binding сбрасывает выбранный источник
|
||
значения на «авто» без предупреждения. **После**: клик по уже выбранному
|
||
кандидату — no-op; сброс происходит только при явной смене binding (буква
|
||
спеки #378 §1.6). Пункты (б)–(г) видимого поведения не меняют.
|
||
|
||
## Проблема / Контракты по пунктам
|
||
|
||
**(а) Сброс value_source и value_badge при клике по тому же binding.**
|
||
`houseplan-editor-runtime.ts:12299-12310`: обработчик кандидата безусловно
|
||
пишет `valueSource: null, valueSourceTouched: true` и зовёт
|
||
`_valueBadgeForBinding` (:11961), который так же безусловно touched. Контракт:
|
||
если `c.value === d.binding` — обработчик выходит без изменений драфта (кроме
|
||
закрытия списка `bindingOpen: false`); при реальной смене — поведение прежнее.
|
||
Чинится в обоих местах выбора кандидата (:12308 и виртуальная ветка :12260 —
|
||
для неё «тот же binding» = `d.binding === 'virtual'`).
|
||
|
||
**(б) `rewriteMarkerControlReferences` пишет ключ `value_source: undefined`.**
|
||
`src/devices.ts:886-892`: при переписывании ссылок маркера в объект попадает
|
||
ключ со значением `undefined` (JSON его отбрасывает — до бэкенда не доходит,
|
||
но hasOwnProperty-семантика полей отличается от исходного маркера). Контракт:
|
||
ключ не пишется вовсе, если значения нет (условный спред).
|
||
|
||
**(в) `releaseSourceViolationsOf` для каждого коммита диапазона.**
|
||
`scripts/process-gate.mjs:139-155`: `parseRecords` зовёт дорогую проверку
|
||
(2×`git show` на каждый src-файл) для КАЖДОГО коммита, хотя потребляется она
|
||
только для стабильных релизных (`/^Release v\d/` без beta/candidate, :134).
|
||
Контракт: проверка вычисляется только для коммитов, для которых `isRelease`
|
||
истинно, и предикат — это **буквально вынесенное целиком выражение из
|
||
`process-gate.mjs:133-135`, оба дизъюнкта**:
|
||
`(/^Release v\d/.test(subject) && !/-(beta|rc|alpha)\.|candidate/i.test(subject)) || Boolean(one('Release'))` —
|
||
НЕ пересказ по памяти и НЕ только первый дизъюнкт. Второй дизъюнкт
|
||
(`Release:`-трейлер) уже сегодня делает `isRelease` истинным для штатных
|
||
бета-коммитов приёмки эталонов (пример: `5f6ee657`), и суженный предикат дал
|
||
бы им `releaseSourceViolations: null` при `isRelease: true` → `evaluateCommit`
|
||
(:196-203) счёл бы ВСЕ их src-файлы нарушением → ложный отказ pre-push/CI.
|
||
Реализация: выражение выносится в одну функцию (например,
|
||
`isReleaseSubject(subject, one)`), которую используют И `makeCommit`, И гейт
|
||
вычисления в `parseRecords`; AC4 доказывает переиспользование. Семантика
|
||
гейта не меняется: для нерелизных `releaseSourceViolations` остаётся null,
|
||
как и сегодня при `() => null`; для релизных (по ПОЛНОМУ предикату) —
|
||
вычисляется, как сегодня.
|
||
|
||
**(г) Асимметрия обезвреживания внешних ссылок при экспорте.**
|
||
`custom_components/houseplan/import_export.py:504-519`: внешний
|
||
`value_badge`-ref нейтрализуется полями (`enabled=False, source=None`), а
|
||
`value_source` — удалением ключа (`marker.pop`). Контракт: `value_source`
|
||
приводится к формату бейджа по смыслу — внешняя ссылка заменяется на
|
||
отсутствие источника ТЕМ ЖЕ способом, каким это представляет фронт: ключ
|
||
`value_source` удаляется у ОБОИХ? Нет — решение: оставить существующие
|
||
форматы (у бейджа поле enabled — часть его модели, у value_source отсутствие
|
||
ключа = auto — часть его модели), но зафиксировать симметрию НАМЕРЕНИЯ юнитом:
|
||
оба пути дают `dropped_marker_links` +1 и валидный по схеме маркер, и оба
|
||
документируются одним комментарием-парой в коде. (Формат хранения менять
|
||
нельзя — это ломало бы round-trip уже существующих экспортов.)
|
||
|
||
## Скоуп / Не-скоуп
|
||
|
||
- Скоуп: четыре правки выше + тесты + ченджлог.
|
||
- Не-скоуп: изменение формата хранения `value_badge`/`value_source` (г);
|
||
любой новый UI; изменение семантики гейта (в).
|
||
|
||
## UX
|
||
|
||
Видимых изменений нет, кроме (а): исчезает молчаливый сброс. Новых строк нет.
|
||
|
||
## Модель данных и миграция
|
||
|
||
Нет. (б) убирает фантомный ключ до записи; (г) форматы хранения не меняются.
|
||
|
||
## i18n
|
||
|
||
Не задето.
|
||
|
||
## Критерии приёмки
|
||
|
||
- **AC1** (а, юнит/смок): в открытом диалоге маркера с выбранным
|
||
`value_source` клик по УЖЕ выбранному binding-кандидату не меняет
|
||
`valueSource`/`valueSourceTouched` и настройки бейджа; клик по ДРУГОМУ
|
||
кандидату сбрасывает, как раньше (регресс-ветка).
|
||
- **AC2** (а, зеркало): то же для value_badge: повторный клик не трогает
|
||
`valueBadge*`-поля драфта.
|
||
- **AC3** (б, юнит): результат `rewriteMarkerControlReferences` для маркера
|
||
без `value_source` не содержит ключа `value_source`
|
||
(`'value_source' in marker === false`); с внешним `value_source` — ключ
|
||
переписан корректно (существующее поведение).
|
||
- **AC4** (в, юнит): `parseRecords` на диапазоне из N нерелизных коммитов не
|
||
вызывает вычислитель ни разу (счётчик-шпион); релизный коммит — вызывает
|
||
ровно один раз; классификация релизности — та же функция, что в гейте
|
||
(юнит сверяет идентичность/переиспользование предиката).
|
||
- **AC5** (г, pytest): экспорт пространства с внешними `value_badge`-ref И
|
||
`value_source`-ref даёт `dropped_marker_links == 2`, оба маркера валидны по
|
||
схеме, `value_badge.enabled is False`, `"value_source" not in marker`.
|
||
- **AC6**: полный гейт зелёный; бюджет ≈ 0.
|
||
|
||
## План автотестов
|
||
|
||
- node --test: AC1/AC2 (диалоговый драфт; паттерн существующих тестов
|
||
`_valueBadgeForBinding`), AC3 (devices), AC4 (process-gate — тесты гейта уже
|
||
существуют в test/, добавить кейс со шпионом).
|
||
- pytest: AC5 в существующем файле экспорта.
|
||
- Мутанты (mutation-gate): м1 — вернуть безусловный сброс в (а) → красный AC1;
|
||
м2 — вернуть безусловный вызов в (в) → красный AC4.
|
||
|
||
## Риски
|
||
|
||
- (а): единственная поведенческая правка; риск — пропустить третий путь выбора
|
||
binding (поиск по `_valueBadgeForBinding` даёт два вызова: :12260, :12308 —
|
||
оба в скоупе; ревьюеру — перепроверить grep'ом).
|
||
- (в): риск рассинхронизации предиката релизности — гасится AC4 (общая функция).
|
||
|
||
## Откат
|
||
|
||
Обычный `git revert`: флагов, миграций, персистентных данных нет. Откат (а)
|
||
возвращает молчаливый сброс, остальное — косметика.
|
||
|
||
**DoR-примечания:** миграция/compatibility — нет; touch — не влияет;
|
||
производительность — (в) сокращает работу pre-push/CI-гейта на длинных
|
||
диапазонах (только выигрыш, бенч не требуется), продуктовый рантайм не задет.
|
||
|
||
## Release-артефакты
|
||
|
||
- CHANGELOG + CHANGELOG.ru: user-visible запись только про (а) («повторный
|
||
клик по выбранной привязке больше не сбрасывает источник значения»),
|
||
ссылка #385. (б)–(г) видимого поведения не меняют и в changelog НЕ входят
|
||
(согласовано с разделом UX; коммит с ними — User-Visible: no).
|
||
- Доки: не задеты.
|