mirror of
https://github.com/Matysh/houseplan-card
synced 2026-10-04 05:41:34 +00:00
96 lines
18 KiB
Markdown
96 lines
18 KiB
Markdown
# SPEC-REVIEW-301-r1
|
||
|
||
- Issue: [#301](https://github.com/Matysh/houseplan-card/issues/301) — Add search/filter to entity selector for doors, windows and other openings
|
||
- Трек: `small` (лёгкий) — ТЗ живёт в теле/комментарии issue, отдельного файла в `docs/specs/` нет
|
||
- Этап: ТЗ на ревью (PROCESS.md §2.4)
|
||
- Заход: r1 · блокирующих циклов израсходовано (на входе) 0 из 2
|
||
- Ревьюер: свежая сессия, без контекста автора
|
||
- Материал: финальная редакция ТЗ в комментарии issue `IC_kwDOTOcLQM8AAAABQjQZeg` (после всех правок `accept`), код на `dev`@`fd4fc801` (HEAD на момент ревью)
|
||
|
||
## Скоуп ревью
|
||
|
||
ТЗ описывает замену нативного `<select>` в диалоге проёма (сенсор контакта и замок) на существующий UI-паттерн `dropbtn`/`droppanel`/`candlist` с текстовым поиском по `friendly_name` и `entity_id`. Изменение класса A, без изменения формата конфига и модели данных. Персона — home admin (`docs/SCOPE.md`), поверхность — Редактор плана, задача закрывает J4/J6 («editable icon rules», «keep the plan true as the home evolves» — оба про качество и скорость настройки проёмов). Конфликта со `SCOPE.md` нет: изменение чисто UI/UX, не трогает View, не добавляет поверхность актуации (замок по-прежнему только привязывается, а не переключается из этого диалога) — инвариант блокировки (`SCOPE.md`, «The lock invariant») не затронут.
|
||
|
||
Замечание по контексту захода: в комментариях issue уже есть **четыре** более ранних вердикта «жёлтый · заход r1», каждый со своими Medium-находками и последующим `accept`-исправлением автора (владелец явно отметил, что первый прогон конвейера не переставил метку, и последующие — по всей видимости, повторы того же сбоя, раз каждый снова маркирован «заход r1»). Поскольку эти прогоны не считаются циклами (метка не переходила), я не наследую их выводы на слово — ниже приведена независимая сверка того, что все находки этих прогонов закрыты в тексте, который сейчас фактически лежит в issue.
|
||
|
||
## Как проверялось
|
||
|
||
Инструмент проверки — чтение, не исполнение (стадия ТЗ, кода ещё нет). Каждая фактическая ссылка ТЗ на код сверена построчно с `src/houseplan-card.ts` на `dev@fd4fc801`:
|
||
|
||
| Ссылка ТЗ | Файл:строка | Проверено |
|
||
|---|---|---|
|
||
| `opt()` — нативный `<select class="areasel">` без фильтра | `src/houseplan-card.ts:19463-19467` | ✅ совпадает дословно |
|
||
| `_contactCandidates()` — binary_sensor + «дверные» cover, дверные `device_class` первыми, тип `{value,label}` без `sub` | `:12871-12886` | ✅ подтверждено; сортировка `a[2]-b[2] || label.localeCompare` — дверные (`doorish?0:1`) действительно первые |
|
||
| `_lockCandidates()` — `lock.*`, алфавитная сортировка, тип `{value,label}` без `sub` | `:12888-12893` | ✅ совпадает |
|
||
| `_runCandidates()` — фильтруется на рендере (`:20981-20983`) без повторной сортировки списка кандидатов; кап на рендере — **40**, не 200 (`:20996`) | `:13815-13829`, `:20980-21002` | ✅ форма фильтрации подтверждена; кап 40 подтверждён — ТЗ корректно исключает это число из переиспользования (п.4) |
|
||
| `_bindingCandidates()` — безусловная пересортировка (`filtered.sort(...)` выполняется даже при пустом `f`), кап `slice(0,200)` | `:13832-13907`, сортировка `:13905`, срез `:13906` | ✅ подтверждено: сортировка **не** зависит от наличия фильтра — ТЗ право, что этот паттерн «не подходит буквально» |
|
||
| `_roomSrcCandidates()` — тот же дефект (безусловная сортировка), тот же кап 200 | `:18703-18724`, сортировка `:18722`, срез `:18723` | ✅ подтверждено |
|
||
| Паттерн `dropbtn`/`droppanel`/`candlist`, поля `.cl`/`.cs` для label/sub, отсутствие `autofocus` во всех трёх существующих `droppanel` (binding, run, roomSrc) | `:20868-20899` (binding), `:20977-21003` (run), `:21705-21727` (roomSrc) | ✅ подтверждено; ни один инстанс не использует `autofocus` — решение ТЗ снять автофокус согласуется с реальным прецедентом, а не выдумано |
|
||
| `_saveOpening()` существует, пишет `opening.contact`/`opening.lock` без изменения формата | `:12751` и далее | ✅ подтверждено (сигнатура и начало функции совпадают) |
|
||
| `passage` не имеет селектора контакта; замок — только door/gate | `:19512-19531` | ✅ подтверждено построчно: `d.type !== 'passage'` для контакта, `d.type === 'door' \|\| d.type === 'gate'` для замка |
|
||
| `opening.none`, `marker.nothing_found`, `marker.search_ph` (стиль «Поиск: …») — переиспользуемые строки | `src/i18n/en.json:142,178,177`; `ru.json` те же ключи | ✅ ключи существуют, значения соответствуют цитируемым |
|
||
| `opening.search_ph` — новый ключ | `src/i18n/{en,ru}.json` | ✅ ключа пока нет ни в одном файле — ожидаемо для стадии ТЗ, будет добавлен в реализации |
|
||
|
||
Отдельно проверено (не на слово автора) закрытие находок предыдущих (незасчитанных) прогонов — таблица ниже.
|
||
|
||
### Закрытие находок предыдущих (незасчитанных) прогонов
|
||
|
||
| Находка прежнего прогона | Чем закрыта в текущем тексте ТЗ | Где видно |
|
||
|---|---|---|
|
||
| M1 (1-й прогон): AC6 противоречит образцу `_bindingCandidates()` (та безусловно пересортировывает) | П.2 явно исключает `_bindingCandidates()`/`_roomSrcCandidates()` как образец именно из-за пересортировки и называет `_runCandidates()` образцом сохранения порядка; п.4 повторяет это тем же текстом | П.2, абзац 2; п.4, предложение 2 |
|
||
| M2 (1-й прогон): AC1–AC8 без пометки способа доказательства | Каждый AC маркирован `[unit]`/`[unit + smoke]`/`[smoke]` | Раздел 6, каждая строка |
|
||
| Low (1-й прогон): нет отдельной фразы «что человек увидит после» | Отдельное предложение сразу после сценария: «Что человек увидит после: …» | Раздел 1, абзац 2 |
|
||
| M1 (2-й прогон): строка кандидата не решено, виден ли `entity_id` в `candlist` | П.4.2, последнее предложение: «Каждая строка результата показывает friendly_name как основную подпись и полный entity_id (c.value) как вторичную» | П.4.2 |
|
||
| M2 (2-й прогон): нет заявления о производительности | Раздел 9, абзац 1 | Раздел 9 |
|
||
| Low (2-й прогон): `_roomSrcCandidates()` не назван вторым «неподходящим буквально» образцом | П.4, предложение 2: «`_bindingCandidates()`/`_roomSrcCandidates()`» (оба названы) | П.4 |
|
||
| M1 (3-й прогон): кап 200 заимствован из образца, который ТЗ же отвергло; принятый `_runCandidates()` капает на 40, а не 200 | П.4, предложения 3–4: лимит 200 зафиксирован как «отдельное явное правило этого селектора», заимствован только как «безопасная верхняя граница»; лимит 40 явно исключён | П.4 |
|
||
| M2 (3-й прогон): автофокус ничем не обоснован, не подтверждён ни одним прецедентом | П.4.2: «Поле получает фокус только после отдельного клика/тапа пользователя: открытие панели само не вызывает экранную клавиатуру» — автофокус снят | П.4.2 |
|
||
|
||
Все восемь находок закрыты текстом, а не заявлением — сверено построчно с итоговой редакцией комментария и с кодом (таблица выше). Расхождений не найдено.
|
||
|
||
## Проверка Acceptance Criteria (AC1–AC8)
|
||
|
||
Каждый AC однозначен, привязан к способу доказательства и опирается на реально существующий код (не на выдуманное поведение):
|
||
|
||
- **AC1–AC3, AC6** (фильтрация, регистр/пробелы, кап, сохранение порядка) — воспроизводимы как чистая функция; форма `_runCandidates()`, на которую они ссылаются, подтверждена построчно.
|
||
- **AC4, AC8** (пункт «нет», пустой результат) — используют реальные строки `opening.none`/`marker.nothing_found`, подтверждено.
|
||
- **AC5** — сформулирован через отсылку («ведёт себя так же, как контакт») вместо перечисления; в контексте `small`-трека и одного смока `demo/smoke_opening_entity_search.mjs`, который явно называет проверку и контакта, и замка в разделе 7, это не создаёт двусмысленности при написании теста.
|
||
- **AC7** — явно разграничивает «значение меняется» от «формат данных не меняется», что и есть предмет проверки.
|
||
|
||
Ни один AC не выдаёт догадку за решение: каждый либо констатирует наблюдаемое поведение, либо явно ссылается на переиспользуемый паттерн, который я сверил с кодом.
|
||
|
||
## Находки
|
||
|
||
### Low (не блокирует, снимаю с записью)
|
||
|
||
**L1. Контракт п.4.1/4.2 требует показывать `entity_id` вторичной подписью в двух местах (закрытая кнопка `dropbtn` и каждая строка `candlist`), но ни один AC и ни один пункт плана тестов (раздел 7) явно не проверяет именно факт отображения — только факт того, что поиск *находит* по `entity_id` (AC2).**
|
||
|
||
- Файл: тело ТЗ (комментарий issue), разделы 4 и 6.
|
||
- Сценарий отказа: реализация корректно фильтрует список по `entity_id` (AC2 зелёный), но по недосмотру не пробрасывает `sub: c.value` в шаблон `candlist` или в подпись `dropbtn` — пользователь по-прежнему не видит, почему найденная строка совпала, хотя именно это было явной причиной правки M1 второго прогона. Ни один автотест такую регрессию не поймает.
|
||
- Почему не блокирую: это утверждение о видимом поведении, но не отдельный AC, поэтому его всё ещё можно закрыть на код-ревью через «проверено чтением, не исполнением» (`PROCESS.md` §2.7) — DoR требует пометки доказательства только у существующих AC, а не превращения каждого предложения контракта в отдельный AC. Свойство простое (одна строка кода в шаблоне на каждое место), а не поведенческая развилка.
|
||
- Рекомендация код-ревьюеру следующего этапа: явно свериться чтением, что `sub: c.value` (или эквивалент) реально попадает в оба места рендера — закрытую кнопку и `candlist` — а не только используется в фильтрации.
|
||
|
||
## Что проверено и корректно
|
||
|
||
- Все фактические ссылки ТЗ на код (строки, функции, сигнатуры типов, i18n-ключи) — точны на `dev@fd4fc801`.
|
||
- Выбор образца для фильтрации (`_runCandidates()` вместо `_bindingCandidates()`/`_roomSrcCandidates()`) обоснован верно: только `_runCandidates()` не пересортировывает список повторно.
|
||
- Кап 200 и отказ от автофокуса — не голословные утверждения, обе оговорки явно зафиксированы как самостоятельные решения ТЗ (не «взято по умолчанию» из отвергнутого образца).
|
||
- Не-скоуп (раздел 5) исключает изменение правил отбора кандидатов, других селекторов, `ha-entity-picker`, формата данных — не сужает и не расширяет задачу relative к issue.
|
||
- DoR (`PROCESS.md` §2.5) выполнен по всем пунктам: AC пронумерованы с доказательством, файлы/модули названы по существу (пусть не единым списком — точечными ссылками на функции), i18n-ключи перечислены с текстом на обоих языках, миграции нет и это явно сказано, влияние на производительность и на touch названо, release-артефакты и откат описаны, открытых продуктовых вопросов к владельцу нет.
|
||
- Терминология: пользовательские строки (`opening.contact_label` = «Датчик открытия», формат плейсхолдера «Поиск: …») согласуются с `docs/USER-GUIDE.ru.md` (раздел 9) и с существующим прецедентом `marker.run_search_ph` = «Поиск: автоматизация, скрипт или сцена…» — новый ключ не изобретает стиль.
|
||
- Инвариант блокировки (`docs/SCOPE.md`, «The lock invariant») не затрагивается: изменение — только представление выбора привязки замка, не добавляет путь актуации.
|
||
- Проверил закрытие всех 8 находок предыдущих (незасчитанных) прогонов по коду и тексту, а не на слово автора — таблица выше.
|
||
|
||
## Чего не проверял
|
||
|
||
- Код ещё не написан (стадия ТЗ) — гейты `typecheck`/`test`/`build`, смоки, golden, инварианты модели, мутационные гварды не запускались и запускаться на этой стадии не должны.
|
||
- Не проверял реальную производительность (нет кода) — оценка в разделе 9 ТЗ принята как правдоподобная по аналогии с уже работающими `_bindingCandidates()`/`_roomSrcCandidates()` на том же порядке величин сущностей.
|
||
- Не проверял `docs/USER-GUIDE.ru.md` на предмет точной будущей формулировки — ТЗ (light track) не обязано фиксировать точный текст документации, только факт правки.
|
||
- Не проверял мутационные имена (`opening-search-*`) на синтаксическую валидность в `scripts/mutation-gate.mjs` — это тестовая инфраструктура, решается на этапе реализации, не на этапе ТЗ.
|
||
|
||
## Вердикт
|
||
|
||
High: 0, Medium: 0, Low: 1 (снят с записью, см. L1). AC полны, однозначны, привязаны к доказательствам и опираются на подтверждённый код. DoR удовлетворён по всем пунктам. Продуктового конфликта со SCOPE.md, TOUCH-SUPPORT.md или инвариантом блокировки нет.
|
||
|
||
**Зелёный.**
|