Files
houseplan-card/docs/reviews/CODE-REVIEW-592-r2.md
T
2026-09-18 19:31:17 +00:00

203 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Код-ревью #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)» — использует буквально тот же пример (`<span>${this.host._t(k as any)}</span>`), что и в тексте находки 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`.
---
<!-- material-anchors: сгенерировано конвейером (#414) -->
## Материал раунда
- Ветка: `issue/592-extract-dialogs`, коммит `1403fb440b57` — ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет.
- Дерево материала: `fde11c38b97a36045e7148f01f2f459c8a2b88a3`
```
git log --all --format='%H %T' | grep fde11c38b97a
```
- Тело issue: `b105f7b3eb3f70575ee5751d1a917940da949981400e342e9cb133153057f673`
- Вердикт конвейера: `green` · High 0