docs: specify #385 audit-lows batch

User-Visible: no
Issue: #385
This commit is contained in:
Codex
2026-08-30 10:32:43 +03:00
parent 9c162c0926
commit c28b2f2d41
+130
View File
@@ -0,0 +1,130 @@
# ТЗ #385 — Пачка Low аудита v1.69.0 (а–г)
- Issue: https://github.com/Matysh/houseplan-card/issues/385
- Приоритет: P3, polish; полный трек (четыре несвязанные поверхности — фронт-UI,
фронт-модель, инфраструктурный скрипт, бэкенд; критерий «одна поверхность» §5
не выполняется — прецеденты #369, SPEC-REVIEW-376-r1 H1)
- Ревизия: 1 (2026-08-30)
## Сценарий
Четыре шероховатости, найденные 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).
Контракт: проверка вычисляется только для коммитов, чей subject проходит тот
же релизный предикат (вынести предикат в общую константу/функцию, чтобы гейт
и вычислитель не разошлись). Семантика гейта не меняется: для нерелизных
`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 — не влияет.
## Release-артефакты
- CHANGELOG + CHANGELOG.ru: user-visible запись про (а) («повторный клик по
выбранной привязке больше не сбрасывает источник значения»), (б)–(г) —
«мелкие уточнения»; ссылка #385.
- Доки: не задеты.