docs: review document for #301

Issue: #301
User-Visible: no
This commit is contained in:
claude[bot]
2026-08-25 08:44:48 +00:00
parent 6d094ca01b
commit fd4fc801ef
+177 -158
View File
@@ -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()` |
Это не заменяет полный разбор ниже — оно объясняет, почему ряд очевидных вопросов
в тексте уже закрыт и не всплывает как новая находка.
## Скоуп
Заменить нативный `<select class="areasel">` (локальный хелпер `opt()`,
`houseplan-card.ts:19463`) в диалоге проёма для двух полей — контакта и замка —
на существующий визуальный паттерн `dropbtn`/`droppanel`/`candlist` с текстовым
полем поиска, без изменения набора кандидатов, их базового порядка (кроме как
под фильтром) и формата конфига (`opening.contact`/`opening.lock`).
Диалог проёма (дверь/окно/ворота) — два `<select>`-селектора (контакт, замок) —
переводятся на существующий паттерн поиска `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) — визуально идентичны, строка
кандидата везде рендерится как `<span class="cl">label</span><span
class="cs">sub</span>`;
- `_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). Вердикт — жёлтый.