# 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 на момент ревью) ## Скоуп ревью ТЗ описывает замену нативного `` без фильтра | `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 или инвариантом блокировки нет. **Зелёный.**