From fd4fc801efe849ac1c40b1dfd325e249aa6efb08 Mon Sep 17 00:00:00 2001 From: "claude[bot]" <209825114+claude[bot]@users.noreply.github.com> Date: Tue, 25 Aug 2026 08:44:48 +0000 Subject: [PATCH] docs: review document for #301 Issue: #301 User-Visible: no --- docs/reviews/SPEC-REVIEW-301-r1.md | 335 +++++++++++++++-------------- 1 file changed, 177 insertions(+), 158 deletions(-) diff --git a/docs/reviews/SPEC-REVIEW-301-r1.md b/docs/reviews/SPEC-REVIEW-301-r1.md index ace064a1..f7f743fd 100644 --- a/docs/reviews/SPEC-REVIEW-301-r1.md +++ b/docs/reviews/SPEC-REVIEW-301-r1.md @@ -1,189 +1,208 @@ -# SPEC-REVIEW — issue #301, заход r1 +# SPEC-REVIEW-301-r1 -**Тема:** поиск/фильтр в селекторах контакта и замка диалога проёма (дверь/окно/ворота). -**Трек:** `small` — ТЗ в теле issue, ревью комментарием (этот документ — файл ревьюера; в issue уходит краткий комментарий). -**Заход:** r1 · блокирующих циклов израсходовано 0 из 2 (лимит лёгкого трека — 2). -**Материал:** тело issue #301 + комментарий-ТЗ (Codex, редактирован 2026-08-25, текущая версия на момент этого ревью), код на `dev` @ `65e55872`. +**Issue:** [#301](https://github.com/Matysh/houseplan-card/issues/301) — Add search/filter to entity selector for doors, windows and other openings +**Трек:** `small` — ТЗ живёт в теле issue (комментарий автора, `edited: true`), файла в `docs/specs/` нет и не должно быть. +**Этап:** ТЗ на ревью (PROCESS.md §2.4), лёгкий трек. +**Заход:** r1 · блокирующих циклов израсходовано 0 из 2 (лимит лёгкого трека — 2, PROCESS.md §4). +**ТЗ проверялось против кода на:** `dev`@`6d094ca0` (текущий `HEAD`/`origin/dev` на момент ревью). +**Вердикт:** жёлтый · High: 0 · Medium: 2 (оба в скоупе, возвращаются автору). -## Контекст: почему это снова r1, а не r2 +## Замечание к нумерации захода -Автоматическое ревью уже отработало один раз (комментарий от `claude`, -2026-08-25T08:09:14Z, документ `docs/reviews/SPEC-REVIEW-301-r1.md`, коммит -`68498e52`, вердикт жёлтый, Medium: 2). Владелец зафиксировал сбой конвейера -*после* публикации вердикта: статусная метка не переставилась -(issue-комментарий 2026-08-25T05:14:19Z, прогон -`actions/runs/32811973025`). По правилу «после прогона ревью метка всегда -меняется; если не сменилась — упал сам прогон» (`AGENTS.md`) этот заход -считается не состоявшимся механически, и настоящий прогон переисполняет его -как r1, а не как r2 — с полным разбором, не по дельте (§2.10 применяется -начиная со второго *засчитанного* цикла). +В истории issue уже есть два опубликованных вердикта «жёлтый · заход r1» (первый — +после сбоя автоматизации, по словам владельца, метка не переставилась; второй — +«переисполнение», названное автором «первым засчитанным циклом»), и оба раза +автор вносил правки в текст ТЗ. Тем не менее конвейер передал этот запуск как +`r1 · 0/2` — то есть с точки зрения системы состояния предыдущие проходы не +засчитаны (ни один не сдвинул метку `S4-spec-review`). Я делаю ставку на эту +информацию, а не на прочитанные комментарии, и провожу **полный** разбор, как +предписано для r1, а не разбор по дельте (§2.10 применяется только со второго +цикла). Ниже я всё же сверяю, закрыты ли находки предыдущих (незасчитанных) +проходов по существу — не потому что это обязательно на r1, а потому что это +дёшево и предотвращает регресс уже обсуждённых мест. -Автор уже ответил на находки того захода правкой текста ТЗ (комментарий -2026-08-25T08:23:38Z: «M1 — accept, M2 — accept, Low — accept»). Ниже — -самостоятельная проверка текущего (уже отредактированного) ТЗ с нуля, а не -доверие к заявлению автора о зачёте. Совпадение с прежними находками там, где -оно есть, отмечено явно. +### Что стало с находками предыдущих (незасчитанных) проходов + +| Находка прежних проходов | Закрыта? | Где видно | +|---|---|---| +| M1 (проход 1): форма фильтрации не выбрана явно, есть риск скопировать `_bindingCandidates()` с его безусловной пересортировкой | Да | §2 ТЗ прямо называет `_runCandidates()` образцом и объясняет, почему `_bindingCandidates()`/`_roomSrcCandidates()` не подходят буквально | +| M2 (проход 1): AC1–AC8 без пометки способа доказательства | Да | Каждый AC в §6 несёт `[unit]`/`[unit + smoke]`/`[smoke]` | +| Low (проход 1): нет отдельной фразы «что человек увидит после» без терминов реализации | Да | §1, последний абзац: «Что человек увидит после: …» | +| M1 (проход 2): не решено, виден ли `entity_id` в каждой строке `candlist` | Да | §4.2: «Каждая строка результата показывает `friendly_name`… и полный `entity_id` (`c.value`) как вторичную…» | +| M2 (проход 2): нет заявления о производительности | Да | §9: явный расчёт O(n), кап, «debounce не требуется» | +| Low (проход 2): только один пример неподходящего виджета назван | Да | §2 теперь называет оба: `_bindingCandidates()` и `_roomSrcCandidates()` | + +Это не заменяет полный разбор ниже — оно объясняет, почему ряд очевидных вопросов +в тексте уже закрыт и не всплывает как новая находка. ## Скоуп -Заменить нативный ``-селектора (контакт, замок) — +переводятся на существующий паттерн поиска `dropbtn`/`droppanel`/`candlist`, уже +использованный для привязки маркера, `run`-цели и источника измерения комнаты. +Формат конфига и набор кандидатов не меняются. По `docs/SCOPE.md` это улучшение +J4/J6 (editor ergonomics), не новая работа продукта — конфликта с рамкой нет. ## Как проверялось -Кода нет — есть только текст ТЗ, поэтому это не код-ревью. Задача — проверить, -что описанное реализуемо, не противоречит себе и что каждая фактическая ссылка -на код/паттерн/строку i18n соответствует действительности (иначе автор строит -решение на несуществующей опоре). Прочитаны: `docs/SCOPE.md`, `AGENTS.md`, -`PROCESS.md` §2.4/§2.5/§2.10/§7.1/§7.2/§5, `docs/TOUCH-SUPPORT.md`, -`docs/USER-GUIDE.ru.md` (раздел «Настройки проёма»). Каждая фактическая ссылка -ТЗ сверена с текущим `src/houseplan-card.ts` на `dev`@`65e55872`: - -| Ссылка ТЗ | Проверка | Результат | -|---|---|---| -| `opt()` — локальный select-хелпер, `:19463` | чтение | подтверждено (строка 19463, ровно тот блок) | -| `_contactCandidates()` `:12871`, дверные `device_class` первыми | чтение (12871–12886) | подтверждено — сортировка `doorish ? 0 : 1` перед алфавитом | -| `_lockCandidates()` `:12888` | чтение (12888–12892) | подтверждено — только `lock.*`, алфавитная сортировка | -| Контактный селектор скрыт для `passage` | чтение (guard `d.type !== 'passage'`, `:19519`) | подтверждено — весь блок контакта/замка под условием | -| Замковый селектор только для `door`/`gate` | чтение (guard `:19528`) | подтверждено | -| Паттерн `dropbtn`/`droppanel`/`candlist` существует | `grep dropbtn/droppanel` | подтверждено — **два** живых экземпляра: `_bindingCandidates()` (рендер 20868–20903) и `_roomSrcCandidates()` (рендер 21705–21728), не один, как можно понять из текста ТЗ | -| `_bindingCandidates()` пересортировывает безусловно даже без запроса | чтение (`:13901`, `filtered.sort(...)` вне ветки `f ?`) | подтверждено — ровно то, что называет ТЗ как причину отказа от этого образца | -| `_runCandidates()` не пересортировывает при фильтрации | чтение (сборка/сортировка один раз `:13815–13827`, фильтр без ресорта на рендере `:20981`) | подтверждено | -| Кап 200 «как у поисковых селекторов карточки» | чтение (`_bindingCandidates` `:13905`, `_roomSrcCandidates` `:18722`) | подтверждено — оба капают на 200. `_runCandidates()`, чей алгоритм фильтрации ТЗ просит взять за образец, сам капает иначе (`.slice(0, 40)`, `:20981`) — ТЗ берёт кап числом от одного образца, а форму фильтрации от другого; явных противоречий в тексте это не создаёт, но стоило назвать оба источника | -| `opening.none`, `opening.contact_label`, `opening.lock_label`, `opening.invert`, `marker.nothing_found` | `python -m json` по `src/i18n/{en,ru}.json` | все пять ключей существуют в обоих языках с ожидаемым смыслом | -| `opening.search_ph` — новый ключ | тот же разбор | подтверждено отсутствие — ТЗ корректно называет его новым | -| `_saveOpening` | чтение (`:12751`, вызов из футера диалога `:19557`) | подтверждено | -| `scripts/mutation-gate.mjs`, `scripts/fix-test-build.mjs` | `ls` | оба существуют | -| Именование смока `demo/smoke_opening_entity_search.mjs` | `ls demo/smoke_opening*.mjs` | согласуется с существующим рядом (`smoke_opening_binding.mjs`, `smoke_opening_measure.mjs`, `smoke_opening_preview.mjs`, …) | -| Раздел `docs/USER-GUIDE.ru.md` «Настройки проёма» существует и описывает контакт без поиска | чтение (строки 611–629) | подтверждено — там же явно сказано, из чего выбирается контакт; корректное место для правки release-артефакта | - -Не проверялось (и не должно на этом этапе): кода нет, поэтому -`typecheck`/`test`/`build`/смоки/`check-docs`/инварианты модели не прогонялись — -предмета для них нет. Это граница этапа ТЗ, а не пропущенный гейт. +- Прочитаны `docs/SCOPE.md`, `AGENTS.md`, `PROCESS.md` (целиком, включая §2.4, + §2.5, §2.10, §4, §5, §7.1, §7.2, §8). +- Прочитано тело issue #301 и вся цепочка комментариев (два предыдущих вердикта + и правки автора между ними). +- Каждая фактическая ссылка ТЗ на код сверена чтением `src/houseplan-card.ts` на + `dev`@`6d094ca0`: + - `opt()` — строка 19463 (совпадает); + - `_contactCandidates()` — строка 12871, `_lockCandidates()` — строка 12888 + (совпадают, включая сортировку `doorish ? 0 : 1` и алфавит внутри группы); + - `_runCandidates()` — строка 13815, вызов на рендере — строки 20981/20984, + кап `slice(0, 40)` — строка 20996; + - `_bindingCandidates()` — строка 13832, безусловная `filtered.sort(...)` и + `slice(0, 200)` — строка ~13901–13906 (пересортировка подтверждена: `sort` + вызывается независимо от того, пуст ли `bindingFilter`); + - `_roomSrcCandidates()` — строка 18703, тоже безусловный `list.sort(...)` + + `slice(0, 200)` (тот же дефект, что у `_bindingCandidates()` — второй проход + справедливо это заметил); + - паттерн `dropbtn`/`droppanel`/`candlist` — строки 20868–20900 (binding), + 20981–21000 (run), 21705–21721 (roomSrc) — визуально идентичны, строка + кандидата везде рендерится как `labelsub`; + - `_saveOpening()` — строка 12751, кнопка `Save` — строка 19557; + - guard `passage` без контакта — строка 19519 (`d.type !== 'passage'`), guard + замка только для `door`/`gate` — строка 19527; + - i18n: `opening.search_ph` в `src/i18n/en.json` и `ru.json` **отсутствует** + (проверено `python3 -c "json.load(...)"` по обоим файлам) — заявка на новый + ключ обоснована, это не переизобретение существующего; `marker.search_ph` + существует, но с другим текстом («Search device / group…») — использование + отдельного ключа для проёма оправдано контекстом; + - `demo/smoke_opening_binding.mjs` уже существует, но проверяет доступность + сущностей после tombstone через прямой вызов `_contactCandidates()`/ + `_lockCandidates()`, а не UI выбора — пересечения с предлагаемым + `smoke_opening_entity_search.mjs` нет; + - `scripts/mutation-gate.mjs` — реестр существует, предложенные id + (`opening-search-*`) в нём отсутствуют, что ожидаемо: это будущие мутанты, + ТЗ не выдаёт их за уже существующие. +- Прочитан `docs/TOUCH-SUPPORT.md` целиком и `docs/UX-MODES.md` (полнотекстовый + поиск по «autofocus», «поиск», «фокус» — совпадений нет) — в проекте нет + канонической нормы, ожидающей или запрещающей автофокус в такой панели. +- Прочитан `docs/USER-GUIDE.ru.md` §9 (проёмы) — обещанная правка документации не + противоречит существующей терминологии раздела. ## Находки -### [Medium, в скоупе] Контракт не решает, что показывает строка кандидата в открытой панели +### M1 — Кап списка кандидатов назван по образцу, который ТЗ само же исключило -П.4.1 фиксирует, что в **закрытом** состоянии кнопка `dropbtn` показывает -`friendly_name` **и** `entity_id` подписью. П.4.2 описывает открытую панель как -«поле поиска + список кандидатов», но не говорит, что видно в каждой строке -`candlist` — только `label`, или `label` плюс `entity_id` как подпись. +**Где:** §4 п.4 и §7 (план тестов) текста ТЗ. -Это не мелочь ради красоты: у двух реально существующих `candlist`-виджетов -в проекте поведение расходится. `_bindingCandidates()` показывает под именем -`sub` — модель устройства (`sub: dev.model || …`), а `_roomSrcCandidates()` -показывает под именем ровно `entity_id` (`sub: eid`, `houseplan-card.ts:18722`). -Тип, который возвращают `_contactCandidates()`/`_lockCandidates()` -(`{ value: string; label: string }[]`), вообще не содержит поля `sub` — -значит, без явного решения реализация с равной вероятностью повторит любой -из двух образцов или не покажет `entity_id` в строке вовсе. +П.2 ТЗ явно решает, какую форму фильтрации переиспользовать, и явно отвергает +`_bindingCandidates()`/`_roomSrcCandidates()` как образец — потому что они +безусловно пересортировывают результат (подтверждено чтением, см. выше). +Выбранный образец — `_runCandidates()` вместе с его рендер-паттерном (строки +13815, 20981–20996): фильтрация без ресорта, а кап на количество видимых строк +применяется **на рендере**, `cands.slice(0, 40)` (строка 20996) — не внутри +самой функции построения кандидатов. -Это напрямую задевает AC2 («фильтр находит и по `entity_id`: запрос -`binary_sensor.win` находит сущность, чьё имя не содержит `win`») по духу, а -не только по факту: AC2 останется технически проверяемым смоком без этого -решения (можно кликнуть единственный оставшийся кандидат и проверить -сохранённое значение), но пользователь, ради которого A2 существует, увидит -находку без объяснения, почему она совпала — ровно то отличие, которое делает -`entity_id` в подписи полезным, а не декоративным. Учитывая, что `value` -контакта/замка уже и есть исходный `entity_id` (в отличие от `_bindingCandidates`, -где `value` — это `device:id`/`entity:id` с префиксом), техническое решение -дешёвое: `sub: c.value`, без изменения сигнатур `_contactCandidates()`/ -`_lockCandidates()`. +При этом п.4.4 и раздел «План тестов» требуют: «список ограничивается тем же +капом 200, что и поисковые селекторы карточки». Число 200 в проекте +действительно есть — но это `slice(0, 200)` именно у `_bindingCandidates()` и +`_roomSrcCandidates()` (строки 13906, 18723) — у тех самых образцов, которые ТЗ +только что признало неподходящими. Единственный сохранённый образец +(`_runCandidates()`) использует **40**, а не 200, и капает список на рендере, а +не внутри candidate-функции. -**Что нужно поправить:** одно предложение в п.4.2 — какая из двух форм строки -берётся (рекомендация: подпись `entity_id` под именем, по образцу -`_roomSrcCandidates()`, поскольку именно `entity_id`, а не модель устройства, -и есть второй канал поиска по AC2). +Это не стилистическая мелочь: у карточки нет единого «того же капа» — их два +разных, и ТЗ ссылается на число от отвергнутого образца, а не от принятого. Из +текущей формулировки реализатору не следует однозначно ни (а) какое число +использовать — 40 или 200, ни (б) где резать список — внутри +`_contactCandidates()`/`_lockCandidates()` (по образцу `_bindingCandidates()`, +которую ТЗ отвергло как образец сортировки, но, видимо, не как образец капа) или +на рендере (по образцу `_runCandidates()`, который ТЗ выбрало явно). При сотнях +`binary_sensor`/`cover` в реальной инсталляции (ровно сценарий issue) разница +между «до 40» и «до 200» видимых строк при пустом запросе — заметное отличие +первого экрана панели, а не деталь реализации. -**Как воспроизвести неоднозначность:** взять текст ТЗ буквально и попросить -двух разных исполнителей написать рендер строки кандидата — получится два -разных, оба «по образцу из ТЗ». +**Как чинится:** одно предложение в п.4.4, фиксирующее и число, и место среза — +согласованно с тем, что реально принято образцом `_runCandidates()` (40, на +рендере), либо явное решение поднять его до 200 с объяснением, почему в этом +случае берётся число от другого паттерна. -### [Medium, в скоупе] DoR требует явного заявления о влиянии на производительность — в ТЗ его нет +### M2 — Автофокус на поле поиска не имеет опоры ни в одном образце и не помечен как решение -`PROCESS.md` §2.5 перечисляет обязательные пункты «Готово к разработке», -включая «влияние на производительность и бюджеты названо (или явно «нет»)» — -и это пункт из категории «все обязательны», трек `small` упрощает форму ТЗ -(§5: «проблема · контракт · AC1…ACn с доказательством · откат»), но не снимает -требований DoR при выходе из ревью в `S5-ready`. +**Где:** §4 п.4.2: «текстовое поле поиска с **автофокусом**». -В тексте раздела 9 («Риски и откат») и раздела 8 («Release-артефакты») нет ни -слова о производительности — ни явного «нет», ни оценки. Мотивирующий сценарий -issue («сотни `binary_sensor` и `cover`») — это ровно тот случай, где -«очевидно, что дешёво» стоило написать явно: фильтрация подстрокой на каждое -нажатие клавиши без debounce по массиву в несколько сотен записей — тот же -порядок величины, что уже фильтруют `_bindingCandidates()`/`_roomSrcCandidates()` -сегодня без debounce, поэтому риска здесь по факту нет, но раздел, где это -следовало сказать, пуст. +Проверены все три существующих экземпляра `droppanel` с полем поиска в проекте +— `_bindingCandidates()` (строки 20877–20885), `run`-таргет (строки 20981–20990) +и `_roomSrcCandidates()` (строки 21713–21721): ни один инпут поиска не несёт +атрибут `autofocus`. `autofocus` в файле вообще используется только в двух +других контекстах — текстовое поле маркера типа Text (строка 10904) и кнопка +отмены в диалогах экспорта/импорта (15816, 15906), никак не связанных с +`droppanel`. `docs/TOUCH-SUPPORT.md` и `docs/UX-MODES.md` не упоминают ни +автофокус, ни поведение полей поиска при открытии панели. -**Что нужно поправить:** одна строка в разделе 9, например: «Производительность: -фильтрация подстрокой по тому же порядку кандидатов (десятки–сотни записей), -что уже обрабатывают `_bindingCandidates()`/`_roomSrcCandidates()` без debounce -— нового узкого места не создаёт». +Заголовок ТЗ утверждает «Touch editor: supported — диалог один и тот же на любом +вводе», подразумевая отсутствие нового touch-контракта. Но автофокус — это +новое, ранее не существовавшее в проекте поведение именно для сенсорного ввода: +он немедленно вызывает системную клавиатуру в момент открытия панели, чего не +делает ни один из трёх образцов, включая тот, чью форму фильтрации ТЗ +переиспользует буквально. Это не подкреплено ссылкой ни на один документ и не +помечено как предположение (§7.1 PROCESS.md: непомеченная догадка о поведении — +находка). Решение может быть любым — оставить автофокус, убрать его или сделать +условным по типу указателя, — но его нужно явно принять с одним предложением +обоснования, а не постулировать как факт совместимости с уже принятым паттерном, +которому оно не соответствует. -### [Low, к сведению] Второй живой образец `dropbtn`/`droppanel` не назван +**Как чинится:** одно предложение в п.4.2 — либо снять автофокус (тогда +поведение будет буквально идентично трём существующим панелям), либо явно +принять его с обоснованием и отметкой, что это осознанное расхождение с +образцом. -Текст ТЗ (раздел 2) называет ровно один образец, который «не подходит -буквально» — `_bindingCandidates()`. Но безусловно пересортировывает список и -второй существующий `candlist`-виджет с идентичной разметкой, -`_roomSrcCandidates()` (`houseplan-card.ts:13901` там нет, ресорт на -`:18722`, вне ветки `q ?`, — то есть тот же дефект «сортирует, даже если запрос -пуст»). Вывод ТЗ (взять форму фильтрации `_runCandidates()`) от этого не -меняется — он верен для обоих контрпримеров, — но перечисление только одного -из двух даёт читателю неполную картину «почему». Не блокирует, снимаю -записью: автор может поправить текст или оставить как есть — вывод не -меняется независимо от того, назван ли один контрпример или оба. +### Low — не найдено -## Проверено и корректно +Оба Low из предыдущих (незасчитанных) проходов уже закрыты (см. таблицу выше); +новых Low в этом полном разборе не нашёл. -- Все фактические ссылки ТЗ на код и i18n подтверждены (таблица выше) — - решение не построено на несуществующей опоре. -- **M1 предыдущего (сбойного) захода закрыт по существу.** ТЗ теперь явно - называет форму фильтрации без пересортировки (`_runCandidates()`) и явно - объясняет, почему буквальное копирование `_bindingCandidates()` сломало бы - AC6 (безусловный `.sort()`). Я перепроверил это заявление по коду независимо - (см. таблицу) — оно точное, а не пересказ автора. -- **M2 предыдущего захода закрыт.** Каждый AC1…AC8 несёт пометку `unit` - и/или `smoke`; раздел 7 (план тестов) соответствует этим пометкам по - содержанию (чистая функция фильтрации для unit, реальный диалог для smoke). -- **Low предыдущего захода закрыт.** Раздел 1 содержит отдельное предложение - «Что человек увидит после» без терминов реализации. -- Скоуп укладывается в `docs/SCOPE.md` (J4/J6 — GUI-настройка проёмов, - поддержание актуальности плана на больших инсталляциях), без конфликта с - «View mode is the product»: правка целиком внутри редактора плана, ничего - не протекает в Просмотр. -- Формат конфига не меняется (`opening.contact`/`opening.lock` — те же поля, - раздел 5 явно исключает изменение модели), поэтому `CONFIG-COMPATIBILITY.md` - не затрагивается — миграция не нужна, и ТЗ это не придумывает, а выводит из - раздела «Не входит». -- Touch: диалог общий для всех вводов, новый виджет — воспроизведение уже - работающего на touch паттерна (`_bindingCandidates`/`_roomSrcCandidates` - используют тот же `dropbtn`/`droppanel` без специальной touch-деградации) — - утверждение «поддерживается» не противоречит `docs/TOUCH-SUPPORT.md` - (best-effort редактора, без нового регресса). -- AC1–AC8 однозначны и проверяемы по отдельности; ни один не выдаёт догадку за - факт — там, где текст опирается на конкретный код (образец фильтрации, - наличие i18n-ключей), это подтверждено чтением, а не заявлено на веру. -- Release-артефакты названы верно: `docs/USER-GUIDE.ru.md` действительно имеет - подходящий раздел («Настройки проёма», строки 611–629) с описанием текущего - (безпоискового) выбора контакта — естественное место для правки. +## Что проверено и корректно + +- Все фактические ссылки ТЗ на существующий код (номера строк, имена функций, + сортировки, guard'ы `passage`/`door`/`gate`, путь сохранения через + `_saveOpening`) подтверждены чтением — ТЗ не строит контракт на + несуществующей опоре. +- Выбор образца фильтрации (`_runCandidates()`, без повторной сортировки) + корректен и решает именно ту проблему, которую сам же и называет: сохранение + приоритета дверных `device_class`. +- AC1–AC8 пронумерованы, каждый несёт способ доказательства (`unit`/`smoke`), + сформулированы как проверяемые утверждения, не пересекаются по содержанию. +- Раздел «Не входит» корректно исключает смежные, но посторонние поверхности + (`ha-entity-picker`, отбор кандидатов, формат конфига). +- i18n: единственный новый ключ (`opening.search_ph`) действительно отсутствует + в обоих файлах — заявка не дублирует существующую строку. +- Заявление о производительности (§9) соответствует фактическому порядку + величины операций в коде (линейный фильтр по уже сформированному массиву, без + debounce, как и в двух реально существующих аналогах). +- Откат («один revert») адекватен масштабу изменения — чистая замена + представления, конфиг не трогается. +- Скоуп не расширяется за пределы диалога проёма; сравнение с `docs/SCOPE.md` + (J4/J6) не выявило конфликта с продуктовой рамкой. +- Трек `small` подтверждён: одна поверхность (диалог проёма), нет миграции + конфига, нет нового touch-контракта (за вычетом находки M2 — если автофокус + будет снят или обоснован, критерий трека не нарушается). ## Чего не проверял -- Реальную реализацию — её нет, это стадия ТЗ. -- `typecheck`/`test`/`build`/`check-docs`/смоки/инварианты модели — неприменимо - без кода; это не пропуск гейта, а корректная граница этапа. -- Не сверял поведение `_openingEntityAvailable()` (фильтр доступности сущностей - внутри `_contactCandidates`/`_lockCandidates`) построчно — оно не меняется - этим ТЗ и не упомянуто как предмет изменения, вне скоупа проверки ссылок. +- Реальное поведение в браузере (панель, автофокус, виртуальная клавиатура на + тачскрине) — на этапе ТЗ ручного/браузерного тестирования нет и не должно + быть; это работа код-ревью после реализации. +- Не прогонял никакие гейты (`tsc`, `test`, `build`, smoke, invariants) — на + этом этапе продуктовый код не менялся, гейты нечего проверять; они относятся к + этапу код-ревью (PROCESS.md §2.7). +- Не оценивал предложенные id мутантов (`opening-search-*`) на полноту — это + план тестов будущей реализации, его состоятельность проверяется на код-ревью, + когда мутанты реально появятся в `scripts/mutation-gate.mjs`. +- Не проверял, действительно ли предыдущие два (незасчитанных) вердикта были + учтены конвейером верно — это вопрос к автоматизации, а не к содержанию ТЗ; + привожу таблицу закрытия только для прозрачности, не как часть обязательного + для r1 раздела «Унаследовано» (он относится к r2+, §2.10). -## Вердикт +## Итог -Жёлтый. High: 0, Medium: 2 (оба в скоупе — правятся текстом ТЗ, без нового -issue), Low: 1 (к сведению, не блокирует). Заход r1 (переисполнение сбойного -автоматического прогона), блокирующих циклов израсходовано 0 из 2 до этого -вердикта. +0 High, 2 Medium — оба в скоупе задачи, чинятся правкой текста ТЗ в issue, без +нового issue (владельческое решение 2026-08-19, #202). Вердикт — жёлтый.