mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-29 03:09:36 +00:00
+177
-158
@@ -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). Вердикт — жёлтый.
|
||||
|
||||
Reference in New Issue
Block a user