docs: review document for #301

Issue: #301
User-Visible: no
This commit is contained in:
claude[bot]
2026-08-25 08:55:50 +00:00
parent fd4fc801ef
commit 19332a915d
+74 -187
View File
@@ -1,208 +1,95 @@
# 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 (комментарий автора, `edited: true`), файла в `docs/specs/` нет и не должно быть.
**Этап:** ТЗ на ревью (PROCESS.md §2.4), лёгкий трек.
**Заход:** r1 · блокирующих циклов израсходовано 0 из 2 (лимит лёгкого трека — 2, PROCESS.md §4).
**ТЗ проверялось против кода на:** `dev`@`6d094ca0` (текущий `HEAD`/`origin/dev` на момент ревью).
**Вердикт:** жёлтый · High: 0 · Medium: 2 (оба в скоупе, возвращаются автору).
- 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 на момент ревью)
## Замечание к нумерации захода
## Скоуп ревью
В истории issue уже есть два опубликованных вердикта «жёлтый · заход r1» (первый —
после сбоя автоматизации, по словам владельца, метка не переставилась; второй —
«переисполнение», названное автором «первым засчитанным циклом»), и оба раза
автор вносил правки в текст ТЗ. Тем не менее конвейер передал этот запуск как
`r1 · 0/2` — то есть с точки зрения системы состояния предыдущие проходы не
засчитаны (ни один не сдвинул метку `S4-spec-review`). Я делаю ставку на эту
информацию, а не на прочитанные комментарии, и провожу **полный** разбор, как
предписано для r1, а не разбор по дельте (§2.10 применяется только со второго
цикла). Ниже я всё же сверяю, закрыты ли находки предыдущих (незасчитанных)
проходов по существу — не потому что это обязательно на r1, а потому что это
дёшево и предотвращает регресс уже обсуждённых мест.
ТЗ описывает замену нативного `<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») не затронут.
### Что стало с находками предыдущих (незасчитанных) проходов
| Находка прежних проходов | Закрыта? | Где видно |
|---|---|---|
| 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>`-селектора (контакт, замок) —
переводятся на существующий паттерн поиска `dropbtn`/`droppanel`/`candlist`, уже
использованный для привязки маркера, `run`-цели и источника измерения комнаты.
Формат конфига и набор кандидатов не меняются. По `docs/SCOPE.md` это улучшение
J4/J6 (editor ergonomics), не новая работа продукта — конфликта с рамкой нет.
Замечание по контексту захода: в комментариях issue уже есть **четыре** более ранних вердикта «жёлтый · заход r1», каждый со своими Medium-находками и последующим `accept`-исправлением автора (владелец явно отметил, что первый прогон конвейера не переставил метку, и последующие — по всей видимости, повторы того же сбоя, раз каждый снова маркирован «заход r1»). Поскольку эти прогоны не считаются циклами (метка не переходила), я не наследую их выводы на слово — ниже приведена независимая сверка того, что все находки этих прогонов закрыты в тексте, который сейчас фактически лежит в issue.
## Как проверялось
- Прочитаны `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 (проёмы) — обещанная правка документации не
противоречит существующей терминологии раздела.
Инструмент проверки — чтение, не исполнение (стадия ТЗ, кода ещё нет). Каждая фактическая ссылка ТЗ на код сверена построчно с `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 не выдаёт догадку за решение: каждый либо констатирует наблюдаемое поведение, либо явно ссылается на переиспользуемый паттерн, который я сверил с кодом.
## Находки
### M1 — Кап списка кандидатов назван по образцу, который ТЗ само же исключило
### Low (не блокирует, снимаю с записью)
**Где:** §4 п.4 и §7 (план тестов) текста ТЗ.
**L1. Контракт п.4.1/4.2 требует показывать `entity_id` вторичной подписью в двух местах (закрытая кнопка `dropbtn` и каждая строка `candlist`), но ни один AC и ни один пункт плана тестов (раздел 7) явно не проверяет именно факт отображения — только факт того, что поиск *находит* по `entity_id` (AC2).**
П.2 ТЗ явно решает, какую форму фильтрации переиспользовать, и явно отвергает
`_bindingCandidates()`/`_roomSrcCandidates()` как образец — потому что они
безусловно пересортировывают результат (подтверждено чтением, см. выше).
Выбранный образец — `_runCandidates()` вместе с его рендер-паттерном (строки
13815, 20981–20996): фильтрация без ресорта, а кап на количество видимых строк
применяется **на рендере**, `cands.slice(0, 40)` (строка 20996) — не внутри
самой функции построения кандидатов.
При этом п.4.4 и раздел «План тестов» требуют: «список ограничивается тем же
капом 200, что и поисковые селекторы карточки». Число 200 в проекте
действительно есть — но это `slice(0, 200)` именно у `_bindingCandidates()` и
`_roomSrcCandidates()` (строки 13906, 18723) — у тех самых образцов, которые ТЗ
только что признало неподходящими. Единственный сохранённый образец
(`_runCandidates()`) использует **40**, а не 200, и капает список на рендере, а
не внутри candidate-функции.
Это не стилистическая мелочь: у карточки нет единого «того же капа» — их два
разных, и ТЗ ссылается на число от отвергнутого образца, а не от принятого. Из
текущей формулировки реализатору не следует однозначно ни (а) какое число
использовать — 40 или 200, ни (б) где резать список — внутри
`_contactCandidates()`/`_lockCandidates()` (по образцу `_bindingCandidates()`,
которую ТЗ отвергло как образец сортировки, но, видимо, не как образец капа) или
на рендере (по образцу `_runCandidates()`, который ТЗ выбрало явно). При сотнях
`binary_sensor`/`cover` в реальной инсталляции (ровно сценарий issue) разница
между «до 40» и «до 200» видимых строк при пустом запросе — заметное отличие
первого экрана панели, а не деталь реализации.
**Как чинится:** одно предложение в п.4.4, фиксирующее и число, и место среза —
согласованно с тем, что реально принято образцом `_runCandidates()` (40, на
рендере), либо явное решение поднять его до 200 с объяснением, почему в этом
случае берётся число от другого паттерна.
### M2 — Автофокус на поле поиска не имеет опоры ни в одном образце и не помечен как решение
**Где:** §4 п.4.2: «текстовое поле поиска с **автофокусом**».
Проверены все три существующих экземпляра `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` не упоминают ни
автофокус, ни поведение полей поиска при открытии панели.
Заголовок ТЗ утверждает «Touch editor: supported — диалог один и тот же на любом
вводе», подразумевая отсутствие нового touch-контракта. Но автофокус — это
новое, ранее не существовавшее в проекте поведение именно для сенсорного ввода:
он немедленно вызывает системную клавиатуру в момент открытия панели, чего не
делает ни один из трёх образцов, включая тот, чью форму фильтрации ТЗ
переиспользует буквально. Это не подкреплено ссылкой ни на один документ и не
помечено как предположение (§7.1 PROCESS.md: непомеченная догадка о поведении —
находка). Решение может быть любым — оставить автофокус, убрать его или сделать
условным по типу указателя, — но его нужно явно принять с одним предложением
обоснования, а не постулировать как факт совместимости с уже принятым паттерном,
которому оно не соответствует.
**Как чинится:** одно предложение в п.4.2 — либо снять автофокус (тогда
поведение будет буквально идентично трём существующим панелям), либо явно
принять его с обоснованием и отметкой, что это осознанное расхождение с
образцом.
### Low — не найдено
Оба Low из предыдущих (незасчитанных) проходов уже закрыты (см. таблицу выше);
новых Low в этом полном разборе не нашёл.
- Файл: тело ТЗ (комментарий issue), разделы 4 и 6.
- Сценарий отказа: реализация корректно фильтрует список по `entity_id` (AC2 зелёный), но по недосмотру не пробрасывает `sub: c.value` в шаблон `candlist` или в подпись `dropbtn` — пользователь по-прежнему не видит, почему найденная строка совпала, хотя именно это было явной причиной правки M1 второго прогона. Ни один автотест такую регрессию не поймает.
- Почему не блокирую: это утверждение о видимом поведении, но не отдельный AC, поэтому его всё ещё можно закрыть на код-ревью через «проверено чтением, не исполнением» (`PROCESS.md` §2.7) — DoR требует пометки доказательства только у существующих AC, а не превращения каждого предложения контракта в отдельный AC. Свойство простое (одна строка кода в шаблоне на каждое место), а не поведенческая развилка.
- Рекомендация код-ревьюеру следующего этапа: явно свериться чтением, что `sub: c.value` (или эквивалент) реально попадает в оба места рендера — закрытую кнопку и `candlist` — а не только используется в фильтрации.
## Что проверено и корректно
- Все фактические ссылки ТЗ на существующий код (номера строк, имена функций,
сортировки, 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 — если автофокус
будет снят или обоснован, критерий трека не нарушается).
- Все фактические ссылки ТЗ на код (строки, функции, сигнатуры типов, 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 находок предыдущих (незасчитанных) прогонов по коду и тексту, а не на слово автора — таблица выше.
## Чего не проверял
- Реальное поведение в браузере (панель, автофокус, виртуальная клавиатура на
тачскрине) — на этапе ТЗ ручного/браузерного тестирования нет и не должно
быть; это работа код-ревью после реализации.
- Не прогонял никакие гейты (`tsc`, `test`, `build`, smoke, invariants) — на
этом этапе продуктовый код не менялся, гейты нечего проверять; они относятся к
этапу код-ревью (PROCESS.md §2.7).
- Не оценивал предложенные id мутантов (`opening-search-*`) на полноту — это
план тестов будущей реализации, его состоятельность проверяется на код-ревью,
когда мутанты реально появятся в `scripts/mutation-gate.mjs`.
- Не проверял, действительно ли предыдущие два (незасчитанных) вердикта были
учтены конвейером верно — это вопрос к автоматизации, а не к содержанию ТЗ;
привожу таблицу закрытия только для прозрачности, не как часть обязательного
для r1 раздела «Унаследовано» (он относится к r2+, §2.10).
- Код ещё не написан (стадия ТЗ) — гейты `typecheck`/`test`/`build`, смоки, golden, инварианты модели, мутационные гварды не запускались и запускаться на этой стадии не должны.
- Не проверял реальную производительность (нет кода) — оценка в разделе 9 ТЗ принята как правдоподобная по аналогии с уже работающими `_bindingCandidates()`/`_roomSrcCandidates()` на том же порядке величин сущностей.
- Не проверял `docs/USER-GUIDE.ru.md` на предмет точной будущей формулировки — ТЗ (light track) не обязано фиксировать точный текст документации, только факт правки.
- Не проверял мутационные имена (`opening-search-*`) на синтаксическую валидность в `scripts/mutation-gate.mjs` — это тестовая инфраструктура, решается на этапе реализации, не на этапе ТЗ.
## Итог
## Вердикт
0 High, 2 Medium — оба в скоупе задачи, чинятся правкой текста ТЗ в issue, без
нового issue (владельческое решение 2026-08-19, #202). Вердикт — жёлтый.
High: 0, Medium: 0, Low: 1 (снят с записью, см. L1). AC полны, однозначны, привязаны к доказательствам и опираются на подтверждённый код. DoR удовлетворён по всем пунктам. Продуктового конфликта со SCOPE.md, TOUCH-SUPPORT.md или инвариантом блокировки нет.
**Зелёный.**