# Код-ревью #592 · заход r2 SHA материала: `1403fb440b572cec04d2a82a23e29f72bfbeb73f`. Ветка: `issue/592-extract-dialogs`. Заход r2, блокирующих циклов израсходовано 1 из 4. Предыдущий раунд: **r1**, вердикт жёлтый, документ `docs/reviews/CODE-REVIEW-592-r1.md`, материал `61c74a70128a29871547519750bae32695f6e56a` (коммиты `b76f3e57` + `61c74a70`). SHA в вердикте r1 назван прямо (в отличие от типичного пробела #227) — дельта считается от него без дополнительного розыска. ## Дельта r1 → r2 ``` git diff --stat 61c74a70..1403fb44 -- . ':!dist' ':!custom_components/houseplan/frontend' ':!docs/reviews' ``` ``` scripts/mutation-registry.mjs | 13 ++--- scripts/no-new-any.mjs | 121 +++++++++++++++++++++++++++++++++--------- test/no-new-any.test.mjs | 102 +++++++++++++++++++++++++---------- 3 files changed, 176 insertions(+), 60 deletions(-) ``` Плюс некодовый коммит `d2443298` (`docs: review document for #592`, добавляет сам документ r1 в дерево). `src/**`, `test/core-file-budget.test.mjs`, `test/editor-dialog-modules.test.mjs`, `test/houseplan-source.mjs`, `test/space-dialog.test.mjs`, `dist/**`, `custom_components/houseplan/frontend/**` между r1 и r2 не менялись (проверено `git diff --stat` по каждому пути — пусто). Дельта локальна: один коммит `1403fb44`, три файла, ровно правка Medium-находки r1 (M1). Оснований для полного разбора (ребейз на ушедший вперёд `dev`, смена контракта, новая подсистема, объём дельты сравним с исходной задачей) нет — разбор по дельте. ## Закрытие раунда r1 | Находка r1 | Чем закрыта | Где это видно | |---|---|---| | **M1** (Medium, в скоупе): `no-new-any.mjs` признавал перенесённой любую добавленную строку, если где-то **в любом файле того же диффа** нашлась дословно совпадающая удалённая строка — без привязки к паре файлов реального переноса. Демонстрировался рабочий обход: несвязанная уборка удаляет строку с `any`, новый файл добавляет текстуально совпадающую — гейт молчит. | Сопоставление переписано на уровне непрерывных **блоков** ≥ `MOVED_BLOCK_MIN` (5) строк подряд, привязанных к конкретному файлу-источнику (`sourcePath` из `--- a/...`), а не к произвольному месту диффа. Одиночное совпадение больше не засчитывается. Добавлен тест, воспроизводящий ровно сценарий обхода из r1, и мутант заменён так, чтобы явно проверять эту дыру. | `scripts/no-new-any.mjs:152-250` (новая `movedLinesByFile`, `MOVED_BLOCK_MIN`); тест `test/no-new-any.test.mjs:172-197` — «#592 одиночное совпадение переносом не считается (M1)» — использует буквально тот же пример (`${this.host._t(k as any)}`), что и в тексте находки r1; мутант `scripts/mutation-registry.mjs` переименован в `no-new-any-forgives-a-single-matching-line`, патч опускает `MOVED_BLOCK_MIN` 5→1, что и есть найденная дыра. | Проверено исполнением, не на слово автора: - `node --test test/no-new-any.test.mjs` на `1403fb44` → 15/15 pass, включая новый тест M1. - `node scripts/mutation-gate.mjs --check` → весь реестр `ok`, в том числе `no-new-any-forgives-a-single-matching-line` (мутант применён, `guard` покраснел, патч возвращён — дисциплина «мутант умеет убивать» соблюдена). - `node scripts/no-new-any.mjs --base origin/dev --head HEAD` на реальном диффе задачи → «Новых any нет», 1296 из 1395 добавленных строк признаны перенесёнными (было 1297 в r1 при построчном сопоставлении — минус одна строка, и это ровно то случайное совпадение, из-за которого M1 и была найдена; коммит-сообщение `1403fb44` называет то же число). M1 закрыта фактически, не декларативно: старый обход (пример из r1, `room-settings-dialog.ts:66` / `space-settings-dialog.ts:240`) воспроизведён как регрессионный тест и красит гейт при откате блокового порога до 1 строки. ## Унаследовано из r1 Всё, что дельта не задевает, принимается без повторной проверки — документ `docs/reviews/CODE-REVIEW-592-r1.md`, SHA `61c74a70128a29871547519750bae32695f6e56a`: - Побайтовая идентичность переноса всех четырёх диалогов (AC1/AC2, К1) — дельта r2 не трогает ни `src/editors/*.ts`, ни `houseplan-editor-runtime.ts`. - AC2 (golden, 15 названных эталонов — 0 расхождений; два незаявленных кадра `device-icon-state-table-{light,dark}` — предсуществующий дрейф Chromium 152/151, отделён экспериментом на чистом `origin/dev`) — рендер не задет. - AC3 (потолок `houseplan-editor-runtime.ts`, `test/core-file-budget.test.mjs`) — файл не менялся. - AC4 (состояние не переехало, `test/editor-dialog-modules.test.mjs`, тест проверен мутацией) — файл не менялся. - AC5 (ленивый граф, `bundle:budget`) — `dist/**` и `custom_components/houseplan/frontend/**` между r1 и r2 не менялись (проверено `git diff --stat`, пусто). - AC6 (якоря мутантов, кроме самого переименованного в этом раунде) — реестр не менялся, кроме записи M1 (see выше). - AC7 (полный набор `npm test` / `gate-small`) — состав тестов вне `test/no-new-any.test.mjs` не менялся. - Смоки AC1 (10 поимённо + `smoke_value_face_source`) и выборка 18 смоков по диалогам — `demo/**` в дельте r2 не тронут (`git diff --stat` пусто). - Три отклонения от ТЗ, разобранные автором в хендоффе (сборщик `test/houseplan-source.mjs`, `test/space-dialog.test.mjs`, имена файлов) — файлы не менялись. - Трейлеры коммитов `b76f3e57`/`61c74a70` и отсутствие правок в CHANGELOG — не переоценивались повторно. ## Как проверялось (дельта r2) Прочитан diff трёх файлов целиком (`git diff 61c74a70..1403fb44 -- scripts/mutation-registry.mjs scripts/no-new-any.mjs test/no-new-any.test.mjs`), включая новую реализацию `movedLinesByFile` построчно: - парсинг `--- `/`+++ ` теперь требует пробел после трёх дефисов (точнее прежнего `raw.startsWith('---')`, который ловил любые три дефиса); - удалённые строки группируются по `sourcePath` (файл, из которого строка реально удалена), а не по произвольному ключу — это и есть привязка к паре файлов, которой не хватало в r1; - добавленные строки группируются в непрерывные по номеру строки куски (`runs`), затем для каждого куска ищется наибольший по длине непрерывный блок совпадения среди ещё не потраченных (`spent`) удалённых строк любого файла-источника; засчитывается только если длина ≥ `MOVED_BLOCK_MIN`; - бюджет ведётся позиционно (`spend`/`isSpent`), поэтому повторная вставка того же блока (тест «один удалённый кусок оплачивает ровно одну вставку») корректно не покрывается дважды. Три ручных прогона (перечислены выше в разделе «Закрытие раунда r1») плюс чтение реализации — основной объём проверки этого раунда. ### Что не является находкой Разбирал один пограничный случай парсинга: требование `raw.startsWith('--- ')` (с пробелом) технически может ошибочно принять за заголовок диффа удалённую строку кода, буквально начинающуюся с `"-- "` (например, `-- comment` → в диффе `--- comment`). В TypeScript-файлах, к которым единственно применяется гейт (`isProductTypeScript`, `src/**/*.ts`), такая строка кода практически невозможна (`--` не встречается как первый токен строки вне декремента, а тот не пишется с пробелом после). Прежняя реализация имела ту же двусмысленность (`raw.startsWith('---')`, ещё шире) и просто молча роняла такую строку из подсчёта — по факту новая версия строже прежней, а не слабее. Не поднимаю это до находки: нет реалистичного сценария воспроизведения на продуктовом коде, и это не тот класс дыры, которую M1 описывала (единичное текстовое совпадение между несвязанными файлами) — блоковый порог её по-прежнему закрывает. ## Находки Нет новых находок этого раунда. M1 (Medium, r1) закрыта — см. таблицу выше. ## Что проверено и корректно - M1 закрыта по существу: регрессионный тест воспроизводит ровно задокументированный в r1 обход и падает без фикса (проверено мутантом, не декларативно). - Блоковый порог (`MOVED_BLOCK_MIN = 5`) обоснован в комментарии и коммит-сообщении: случайное совпадение пяти строк подряд между несвязанными файлами практически невозможно даже при 887 явных `any` в базе; настоящий перенос подсистемы по определению состоит из таких кусков. - Привязка удалённых строк к файлу-источнику (`sourcePath`) убирает и вторую часть проблемы r1 — сопоставление больше не глобальное по всему диффу без привязки к файлу, хотя поиск блока всё ещё ищет по всем файлам-источникам диффа (это осознанный выбор: перенос может тянуть из нескольких старых мест — задокументировано в комментарии), что при пороге в 5 строк не открывает находку M1 заново. - Бюджет позиционный (`spend`/`isSpent`), не мультимножественный как в r1: тест на повторную вставку блока подтверждает, что вторая копия остаётся новым кодом. - Реальный дифф задачи по-прежнему проходит гейт («Новых any нет»), дельта в количестве признанных перенесёнными строк (1297→1296) — ожидаемая и объяснённая автором (одна случайная строка перестала засчитываться). - Мутант переименован и переориентирован на найденную дыру, а не на соседнюю; `mutation-gate.mjs --check` подтверждает, что весь реестр, включая его, жив. - Трейлеры `1403fb44`/`d2443298`: `Issue: #592`, `User-Visible: no` — корректно, правка гейт-инструментария без изменения продукта, правок CHANGELOG не требуется. - Изменения — класс B (`scripts/**`, `test/**`), переиспользуют номер #592 по правилу AGENTS.md, отдельного issue не требуют. ## Чего не проверял - `npx tsc --noEmit`, `npm run build` (со сверкой бандла), `npm run bundle:budget`, `golden:verify`, `python -m pytest tests_backend`, `npm run invariants`, полный набор `demo/smoke_*.mjs`, `check-docs.mjs` — дельта r2 не трогает `src/**`, `dist/**`, `custom_components/**/*.py`, геометрию/`layout`/`marker.space`/`open_spans`, рендер или `demo/**`; эти гейты и не запускались в r1 сверх необходимого, и здесь дельта их не задевает. Validate на этом SHA (`1403fb44`) зелёный (прогон 35385526275, назван в контексте раунда) — `tsc`/`npm test`/`npm run build` подтверждены им, повторно не гонял. - `npm test` целиком (2770+ тестов) не перегонял ради факта — целевой файл `test/no-new-any.test.mjs` прогнан отдельно (`node --test`, 15/15), он же часть полного набора, Validate на этом SHA подтверждает, что остальной набор зелёный. - Смоки — не запускал ни одного: дельта не трогает `demo/**` и не меняет видимое поведение; `smoke-select.mjs` не запускал по той же причине (нет изменённых символов продукта в дельте, только dev-гейт). ## Вердикт Зелёный. Дельта r1→r2 — точечная правка единственной Medium-находки r1, проверена исполнением (тест воспроизводит ровно заявленный обход и умеет падать; мутант подтверждён; реальный дифф задачи по-прежнему проходит гейт). Новых находок нет. Всё, что дельта не касается, унаследовано из r1 по перечню выше. --- ## Материал раунда - Ветка: `issue/592-extract-dialogs`, коммит `1403fb440b572cec04d2a82a23e29f72bfbeb73f`. - Предыдущий раунд: r1, документ `docs/reviews/CODE-REVIEW-592-r1.md`, SHA `61c74a70128a29871547519750bae32695f6e56a`. --- ## Материал раунда - Ветка: `issue/592-extract-dialogs`, коммит `1403fb440b57` — ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет. - Дерево материала: `fde11c38b97a36045e7148f01f2f459c8a2b88a3` ``` git log --all --format='%H %T' | grep fde11c38b97a ``` - Тело issue: `b105f7b3eb3f70575ee5751d1a917940da949981400e342e9cb133153057f673` - Вердикт конвейера: `green` · High 0