# CODE-REVIEW-252-r2 Вердикт: зелёный · заход r2 · блокирующих циклов израсходовано 0 из 4 · High: 0 · Medium: 0 ## Скоуп раунда: почему это переиздание r2, а не новый разбор с нуля Этот запуск — повторное проведение того же раунда r2, а не заход r3. Round r2 уже был проведён и опубликован (комментарий issue от 2026-08-23T07:56:33Z, документ `docs/reviews/CODE-REVIEW-252-r2.md`, зелёный вердикт, High:0/Medium:0), но пайплайн не довёл дело до конца: автор сообщил (2026-08-23T07:56:45Z), что автоматический прогон [упал](https://github.com/Matysh/houseplan-card/actions/runs/32625878719) и статусная метка не переставилась. С точки зрения оркестратора раунд не завершён (бюджет §4 не потрачен: зелёный вердикт цикл не расходует), поэтому задача пришла на повторное r2, а не на r3. Проверено, что переиздавать нечего заново с нуля: - SHA, на котором был получен предыдущий вердикт r2: `b58136aa2cc943652af5adb8a94047b668d68dc6`. - Текущий HEAD: `3741bddc6236ffe3d85965dc630704e578561710`. - `git diff b58136a..HEAD --stat` → ровно один файл: `docs/reviews/CODE-REVIEW-252-r2.md | 249 +++++++++++++++++++++++++++++++++++++` (1 file changed, 249 insertions(+)) — это САМ ранее опубликованный документ ревью, закоммиченный шагом публикации предыдущего (упавшего после вердикта) прогона. Ни один файл `src/**`, `demo/**`, `docs/CANVAS.md` и т.п. между `b58136a` и текущим HEAD не менялся. Значит, весь код, который уже был полностью разобран в CODE-REVIEW-252-r1 (на пре-ребейзном коммите, полный разбор) и CODE-REVIEW-252-r2 (на пост-ребейзном `b58136a`, тоже полный разбор — ребейз на ушедший вперёд `dev` подпадает под §7.2), остался байт-в-байт тем же кодом. Дельта этого раунда — пустая по существу. Полный разбор AC по коду в третий раз подряд на неизменном дереве был бы именно той «потерей времени», от которой явно предостерегает инструкция («полные наборы — это предрелизный гейт, а не гейт ревью»). Дополнительно проверено расхождение с `origin/dev`, который тем временем ушёл дальше собственной прошлой проверки в CODE-REVIEW-252-r2 (там уже был учтён `a952f5f`/#256 и его review-документ `2d1fca1`, оба признаны не пересекающимися с #252 по файлам). С тех пор `dev` получил ещё два коммита: - `10999a5` — `ci(process): привести ветку к dev до код-ревью, а не после` (#257), правит только `.github/workflows/process.yml`; - `4b6331f` — `docs(process): описать приведение ветки к dev до код-ревью` (#257), правит только `PROCESS.md`. Оба — чистый процесс/CI, ноль пересечения по файлам с диффом #252 (`src/space-reference-repair.ts`, `plan-optimizer.ts`, `houseplan-card.ts`, `styles.ts`, i18n, доки CANVAS/CONFIG-COMPATIBILITY/USER-GUIDE, golden-сцены). Ретроактивно новое правило «ребейзить до ревью» на уже идущий с r1 код-ревью #252 не распространяется (правило описывает будущий шаг пайплайна перед следующим запуском ревью, а не требование к уже проверенному коду). Само слияние ветки #252 в `dev`, как и раньше, потребует технического ребейза — это не меняет оценку текущего диффа. ## Закрытие раунда r1 (CODE-REVIEW-252-r1) | Находка r1 | Чем закрыта | Где видно | |---|---|---| | Low, снято с записью на будущее: мёртвый ключ `gs.optimize_reference_warning` остался в `en.json`/`ru.json`, код его больше не вызывает | Ключ полностью удалён из обоих словарей коммитом `6c779b5` | `git diff origin/dev...HEAD -- src/i18n/en.json src/i18n/ru.json` — строка присутствует только как удаление; `test/i18n.test.mjs:77` (`assert.doesNotMatch(..., /this\._t\('gs\.optimize_reference_warning'/)`) прогнан лично в этом раунде в составе `npm test` (1140/1140) — зелёный | Это закрытие не изменилось со времени предыдущего r2 — код тот же самый. ## Унаследовано из r1 Полный построчный разбор кода (`src/space-reference-repair.ts` целиком, изменённые фрагменты `plan-optimizer.ts`/`houseplan-card.ts`/`styles.ts`, все юнит- и smoke-тесты, канонические доки, release-артефакты) взят из CODE-REVIEW-252-r1 (документ в `docs/reviews/CODE-REVIEW-252-r1.md`, SHA проверки в этом раунде — `b58136aa2cc943652af5adb8a94047b668d68dc6`, где этот разбор был повторён ПОЛНОСТЬЮ заново после ребейза, а не как наследование — см. раздел «Скоуп» того документа). Наследуется без повторного построчного чтения в этом раунде: - классификация владельца (`absent`/`live-in-missing-space`/`unverified`) — доказательная схема, три исхода, fail-closed при неполном/неавторитетном registry и при неизвестном namespace (AC1–AC3); - отчёт без внутренних id в основном тексте, id — только в свёрнутых «Подробностях» (AC4); - Preview/Cancel/Apply/Undo и идемпотентность #248 не регрессируют (AC5, AC6); - осознанное и задокументированное расширение поведения detach у #244 (немедленное авто-удаление stale-позиции заменено общей классификацией; переименованные тесты и `docs/CANVAS.md`/`docs/CONFIG-COMPATIBILITY.md` прямо называют это заменой, а не регрессом); - `docs/CANVAS.md:494-497` заменён (не дополнен), release-артефакты (оба changelog, канонические доки, синхронные bundle-копии) в одном поведенческом коммите с `User-Visible: yes`; - golden-семантика двух #252-сцен (`optimize-orphan-references-dark-en`, `optimize-orphan-references-light-ru`), доказанная полным `golden:verify` (97/97) дважды — на пре-ребейзном дереве в r1 и на пост-ребейзном `b58136a` в r2. Основание доверять этому наследованию без повторного чтения — не слова автора, а свежая проверка в этом раунде (см. ниже), что дерево с тех пор не изменилось ни на байт. ## Как проверялось в этом раунде Лично прогнано на текущем HEAD (`3741bdd`), не со слов автора и не по памяти о прошлых раундах: - `npx tsc --noEmit` → чисто, без вывода; - `npm test` → 1140/1140 pass, 0 fail, 0 skipped (включает `test/model-invariants.test.mjs` — 12/12, в т.ч. `#253: исчезнувшая запись толщины` и `readModel понимает экспорт/config/get/сырой config (#254)` — обязательный гейт, диф трогает layout-ссылки на пространства); - `npm run build` → зелёный; `sha256sum dist/houseplan-card.js custom_components/houseplan/frontend/houseplan-card.js` → `e5389ba8e8250c6030fb5365b81619b0d2b4687b4c32f2f2c227ca527b7dbcec` для обеих копий — **тот же хеш**, что зафиксирован в CODE-REVIEW-252-r1 независимо от меня в этом раунде; совпадение хеша — самостоятельное машинное доказательство того, что исходный код не менялся с r1/r2, а не доверие на слово; - `npm run bundle:sync` → пересобрал и синхронизировал нетрекаемую стенд-копию `demo/srv/assets/houseplan-card.js` (не коммитится с #255) — зелёно; - `node scripts/check-docs.mjs` → «Documentation checks passed (7 files, 10 external links)» — обязателен, диф трогает `src/**`; - `node scripts/mutation-gate.mjs --check` (дешёвый режим, без пересборки бандла на каждого мутанта) → 139/139 `ok`, включая все четыре мутанта этой темы: `orphan-space-detach-disabled`, `orphan-space-ambiguous-signature-guessed`, `orphan-cleanup-partial-registry-deletes`, `orphan-cleanup-proven-owners-kept`. Дорогой полный прогон (пересборка на каждого мутанта) не повторялся: он уже дважды пройден целиком (r1 — 136/136, r2 — переподтверждён) на этом же дереве, а `--check` подтверждает, что реестр патчей и код с тех пор не разошлись; - `node scripts/smoke-select.mjs --base origin/dev --head HEAD` → те же 6 смоков, что в r1/r2: `smoke_orphan_space_references`, `smoke_grid_snap`, `smoke_optimize_coordinate_canonicalization`, `smoke_optimize_geometry_preflight`, `smoke_optimize_micro_interval`, `smoke_warm_dialogs`; остальные 165 не пересекаются по символам (инструмент не назвал других связей); - все 6 отобранных смоков лично прогнаны headless-Chromium в этом раунде — все `OK`, все булевы поля результата `true` (в т.ч. `idsExistOnlyInClosedDetails`, `explicitCleanupRebuildsPreviewWithoutWriting`, `undoRestoresDeadRefs` из `smoke_orphan_space_references`). Не повторял в этом раунде (обоснование): - **`golden:verify` (полный, 97 сценариев).** Уже пройден целиком дважды в этом же код-ревью: в r1 на пре-ребейзном дереве и в r2 на `b58136a` — оба раза 97/97, включая обе #252-сцены. Текущее дерево байт-в-байт идентично `b58136a` (доказано выше диффом и совпадением SHA-256 бандла). Третий полный прогон на неизменном дереве не добавил бы информации и прямо противоречил бы правилу соразмерности гейтов ревью. - **`mutation-gate.mjs` без `--check` (полная пересборка на мутанта).** По той же причине — уже дважды 136+/136+ на этом дереве, `--check` в этом раунде подтвердил отсутствие расхождения. - **`python -m pytest tests_backend`** — диф не трогает `custom_components/**/*.py` (только сгенерированный JS-бандл в той же папке). - **`npm run invariants -- --config <файл>`** — точечного экспорта живой инсталляции для этой ветки по-прежнему нет; вместо него — `npm test` прогнал `test/model-invariants.test.mjs` на всех fixture-моделях проекта (см. выше), это тот же `checkReferences`, что стоит за флагом. - **Performance-профили** — не названы в AC8, путь maintenance-only, не render loop (§12 ТЗ). - **Ручное браузерное тестирование** — не входит в конвейер ревью; заменено headless smoke (свежепрогнанные) и дважды пройденным полным golden. ## Находки Нет находок уровня High или Medium. Low-находка r1 (мёртвый i18n-ключ) закрыта — см. таблицу выше. ## Итог Код не изменился с зелёного вердикта предыдущего r2 (`b58136a`) ни на байт — единственная разница в дереве это сам ранее опубликованный документ ревью. Этот раунд — техническое переиздание того же вердикта после сбоя пайплайна на шаге после публикации (перестановка метки), а не пересмотр кода. Все дешёвые гейты, обязательные для каждого раунда, лично прогнаны заново на текущем HEAD и зелёные, включая независимое совпадение SHA-256 собранного бандла с зафиксированным в r1. AC1–AC8 остаются подтверждёнными полным разбором r1/r2 (см. «Унаследовано из r1» и исходные документы). Дрейф `origin/dev` (#256, #257) не пересекается с диффом #252 и не требует пересмотра. Новых issue не требуется.