# Код-ревью issue #198 — очистка изолированного микро-интервала толщины через Optimize (цикл r3) - Цикл: r3/4 - Вердикт: **зелёный** · High: 0 · Medium: 0 - Ветка: `issue/198-optimize-micro-interval` - Диапазон коммитов: `git log --oneline origin/dev..HEAD` = `3a68efa` (ТЗ), `cd17a0b` (ревью ТЗ r1), `9b05dd5` (реализация), `833e8e5` (ревью-документ r1), `a797752` (follow-up тесты на замечания r1), `ef22b23` (ревью-документ r2), `5ce3cee` (исправление находки r2 — изоляция AC5-фикстуры), `2d5fec0` (обновление screenshot provenance после технического ребейза) - Диапазон диффа: `git diff origin/dev...HEAD` - ТЗ: [`docs/specs/198-optimize-micro-interval.md`](../specs/198-optimize-micro-interval.md) (ревью ТЗ зелёное, r1) - Предыдущий цикл: [`docs/reviews/CODE-REVIEW-198-r2.md`](CODE-REVIEW-198-r2.md) (жёлтый · Medium-1 — новый AC5-тест не хермитичен к сценарию входной мутации, который он должен доказывать) ## Скоуп Тот же, что в r1/r2: явное действие «Общие настройки → Оптимизировать планы» получает строго ограниченный lossy-шаг — схлопывание одиночного изолированного интервала толщины стены короче половины шага сетки между двумя одинаковыми коллинеарными соседями, при отсутствии room-vertex/ opening-узла на его концах (контракт §6 ТЗ). Runtime и редактор остаются lossless. Этот цикл закрывает единственную оставшуюся находку r2 (Medium-1, AC5): тест «micro-interval cleanup is endpoint, input-order and room-order invariant without mutation» (`test/plan-optimizer.test.mjs`) строил `baseline`-вызов на непроклонированном `fixture`, а затем читал `fixture.walls[1]` для `reversedMiddle` уже после этого вызова — если бы `collapseIsolatedWallThicknessIslands` начала мутировать входной массив на месте, `baseline`-вызов сам испортил бы источник для второго сценария раньше, чем снимался снимок «до», и тест прошёл бы тривиально. Продуктовый код (`src/plan-optimizer.ts`, `src/wall-thickness.ts`) в этом цикле не менялся — `git diff ef22b23..HEAD -- src/` пуст. Единственная содержательная правка — `5ce3cee` (3 строки в `test/plan-optimizer.test.mjs`): `baseline` теперь строится из независимого `baselineFixture = microIntervalFixture()`, а `detached` для этого вызова клонируется через `structuredClone`, так что `fixture.walls[1]`, используемый дальше для `reversedMiddle`, никаким предыдущим вызовом не затрагивается. Коммит `2d5fec0` — provenance-обновление `docs/images/screenshots.json` после того, как ветка была технически перебазирована ещё раз (см. «Как проверялось»): `git diff` содержимого #198 между старым и новым срезом ребейза пуст, изменился только `sourceFingerprint`/скриншот вслед за уже слитыми в `dev` посторонними issue. Класс C, `User-Visible: no`, вне продуктового поведения #198. ## Как проверялось **Инцидент с устаревшим срезом ветки.** Сессия ревью изначально была развёрнута на локальном `HEAD=b411a58` (после `git fetch origin dev` в начале сессии). Перед вынесением вердикта повторный `git fetch origin issue/198-optimize-micro-interval` показал, что удалённая ветка уже продвинулась до `2d5fec0` — тем же набором из восьми коммитов #198, но перебазированным поверх более нового `origin/dev`, куда за время ревью успел влиться не связанный с #198 issue #205 (vacuum trail resume grace). Сверил `git diff b411a58 2d5fec0` — единственная разница это файлы #205 (уже находящиеся в `dev`, значит не относящиеся к диапазону #198) плюс 1-байтовый шум provenance-скриншота; для всех файлов, содержательных для #198 (`src/plan-optimizer.ts`, оба тестовых файла, ТЗ, три предыдущих ревью-документа, `docs/WALL-THICKNESS.md`, targeted smoke), diff между двумя срезами ребейза пуст. Переключился на актуальный `2d5fec0` (`git checkout 2d5fec0`) и весь гейт-прогон ниже выполнен именно на нём — иначе вердикт судил бы уже не тот код, что лежит на удалённой ветке. | Гейт | Результат | Команда | |---|---|---| | typecheck | green | `npx tsc --noEmit` | | unit | green, 924/924 (было 917/917 в r2 на предыдущем ребейзе; рост числа тестов — от слияния #204/#205 в `dev`, не от этого issue) | `npm test` | | build + 3 копии бандла | green, побайтово идентичны | `npm run build && cmp dist/houseplan-card.js custom_components/houseplan/frontend/houseplan-card.js && cmp dist/houseplan-card.js demo/srv/assets/houseplan-card.js` | | targeted smoke (AC7) | green, все 10 подпроверок | `node demo/smoke_optimize_micro_interval.mjs` | | mutation-gate, целевой мутант (AC9) | мутант пойман 1/1 | `node scripts/mutation-gate.mjs --id=optimizer-micro-interval-cleanup-disabled` | | mutation-gate, полный self-check | green, 37/37 | `node scripts/mutation-gate.mjs --check` | | docs-gate (затронут `2d5fec0`) | green | `node scripts/check-docs.mjs --external` | | whitespace | чисто | `git diff --check origin/dev...HEAD` | | process-gate (офлайн) | green, предупреждений 0 | `node scripts/process-gate.mjs` (диапазон `origin/dev..HEAD`, 8 коммитов) | | трейлеры всех 8 коммитов диапазона | `Issue: #198` на каждом; ровно один `User-Visible: yes` — на `9b05dd5` (реализация), и именно в нём оба changelog правятся тем же коммитом | `git log`, `git show 9b05dd5 --stat` | | **адверсариальная проверка исправленного AC5-теста** | тест ловит ровно ту мутацию, из-за которой в r2 был выставлен Medium-1 | см. «Проверка дисциплины „тест должен уметь падать“» ниже | **Не прогонялось и почему (без изменений относительно r1/r2 — диапазон по продуктовому коду и UI-поверхности не менялся с `9b05dd5`):** - `npm run golden:verify` и смежные `smoke_grid_snap`/`smoke_align_guides` — этот цикл не трогает `src/plan-optimizer.ts`, рендер, геометрию, стили или UI-пайплайн Optimize; изменился только один тестовый файл на 3 строки плюс provenance двух docs-артефактов. Тот же код уже прогонялся против той же UI-поверхности зелёным в r1. - `python -m pytest tests_backend -q` — ни один файл `custom_components/**/*.py` не входит в диапазон #198 ни в одном из трёх циклов (подтверждено: `git diff origin/dev...HEAD --name-only | grep '\.py$'` — пусто). - Производительность/бенчмарки — не названы в AC; изменения этого цикла — три строки теста и docs-provenance, никаких perf-путей не затронуто. - Полный набор из 127+ browser-смоков — диапазон r3 не касается ни одной новой UI-поверхности; единственный релевантный (`smoke_optimize_micro_interval`) прогнан и зелёный. ### Проверка дисциплины «тест должен уметь падать» для исправленного AC5-теста Не ограничился чтением диффа `5ce3cee` — воспроизвёл ровно ту адверсариальную мутацию, которой в r2 было доказано отсутствие защиты, и подтвердил, что теперь тест её ловит. В `src/plan-optimizer.ts` временно заменил иммутабельный ``` out = out.map((wall) => { if (!exactMatches(wall, candidate)) return wall; exactMatch = true; return wall.cm === target ? wall : { ...wall, cm: target }; }); ``` на мутирующий цикл ``` for (const wall of out) { if (!exactMatches(wall, candidate)) continue; exactMatch = true; wall.cm = target; } ``` (тот же самый объект, на который ссылаются и `out`, и исходный `walls`, поскольку `out = walls.slice()` — поверхностная копия массива). Пересобрал `test-build` (`npx tsc -p tsconfig.test.json && node scripts/fix-test-build.mjs`) и прогнал изолированно только целевой тест: `node --test --test-name-pattern="invariant without mutation" test/plan-optimizer.test.mjs`. **Результат: 1 fail** — `assert.deepEqual(walls, beforeWalls, 'helper must not mutate the walls array or entries')` красный, `walls[2].cm` меняется с `15` на `22`. Это ровно тот класс регрессии, для которого r2 показал тривиальное прохождение теста. Откатил мутацию (`git checkout -- src/plan-optimizer.ts`), пересобрал `test-build` заново и подтвердил: тот же тест снова зелёный (1/1), а `npm test` на чистом дереве — 924/924, `git status --short` пуст. Правка `5ce3cee` действительно устраняет корень находки r2: `baseline` теперь строится из отдельного вызова `microIntervalFixture()` (`baselineFixture`), а `detached`-комната для этого вызова клонируется, так что второй сценарий (`reversedMiddle`, `walls`, `rooms`) строится из `fixture`, который первый вызов никогда не видел — «до»-снимок снимается до единственного вызова над этим конкретным входом, ровно как просил r2. ## Находки Не выявлено. Обе Medium-находки r1 (AC5, AC8) и оставшаяся Medium-находка r2 (нехерметичность AC5-теста) закрыты и подтверждены исполнением, а не только чтением диффа. ## Что проверено и корректно - **Находка r2 (Medium-1, AC5) закрыта полностью и доказана заявленным способом.** См. адверсариальную проверку выше — тест теперь дискриминативен именно для сценария мутации входа на месте, который он обязан ловить. - **Продуктовый код не менялся с r1/r2.** `git diff ef22b23..HEAD -- src/` пуст: контракт §6 ТЗ (строгий порог `0.5 × pitch`, коллинеарность через `roomWallProfile`, topology-guard по room vertex/open cut, non-cascading snapshot через `targets.size === 1`, разрешение неоднозначности) не требовал повторной проверки по существу в r2 и в этом цикле — он был предметно проверен чтением и тестами в r1 (см. `docs/reviews/CODE-REVIEW-198-r1.md`) и с тех пор не тронут ни одним коммитом. Перепроверены только пересобранные гейты на актуальном коде — все зелёные. - **Ребейз безопасен.** Технический ребейз перед r3 (упомянутый владельцем в issue-комментарии «Технический ребейз перед r3») не изменил ни одной строки, относящейся к #198: единственная разница между срезом ребейза, на котором сессия стартовала (`b411a58`), и актуальным (`2d5fec0`) — это уже слитый в `dev` посторонний issue #205 и провенанс-обновление скриншота. Конфликт при ребейзе (упомянутый в комментарии «объединением changelog/STATUS») не оставил следов расхождения: `docs/CHANGELOG.md`, `docs/CHANGELOG.ru.md`, `docs/STATUS.md` содержат обе записи (#198 и соседние issue), пересечения или потери контента не найдено. - **AC1–AC4, AC6, AC7, AC9, AC10** — доказаны так же, как в r1/r2 (перечисление unit/negative-matrix/smoke/mutation-gate), код не менялся, гейты перепрогнаны и зелёные на актуальном коде. - **AC8** — закрыт в r1-follow-up, подтверждён адверсариальной мутацией в r2 (`degradeWalls()` мутация `walls[0].cm = 999` ловится новым unit), код и тест с тех пор не менялись. - **Трейлеры и changelog.** Все 8 коммитов диапазона несут `Issue: #198`; ровно один `User-Visible: yes` (`9b05dd5`), и именно в нём правятся оба changelog в том же коммите — подтверждено `git show 9b05dd5 --stat`. Ветка называется `issue/198-optimize-micro-interval`, соответствует трейлерам. - **Три копии бандла синхронны.** `cmp` дал побайтовое совпадение `dist/`, `custom_components/houseplan/frontend/` и `demo/srv/assets/` после `npm run build` на актуальном `HEAD=2d5fec0`. - **Docs-provenance корректен.** `node scripts/check-docs.mjs --external` зелёный (7 файлов, 10 внешних ссылок) на актуальном срезе после второго ребейза. ## Чего не проверял - Полный HA backend harness (`tests_backend` под настоящим Home Assistant) — диапазон #198 не содержит правок в `custom_components/**/*.py` ни в одном цикле. - `npm run golden:verify` и полный browser-smoke набор (127+ файлов) — диапазон r3 не трогает рендер/геометрию/UI-пайплайн сверх уже проверенного в r1; продуктовый код не менялся. - Производительность/бенчмарки — не названы в AC, изменения r3 — три строки теста и provenance. - Повторное чтение контракта §6 в `src/plan-optimizer.ts` построчно — выполнено предметно в r1 (см. `CODE-REVIEW-198-r1.md`) и не повторялось заново построчно в r2/r3, так как `git diff` подтверждает отсутствие изменений в этом файле с тех пор; вместо повторного чтения перепрогнаны все относящиеся гейты и целевая мутация на актуальном коде. ## Вывод High-находок нет, Medium-находок нет. Обе находки r1 (AC5, AC8) и оставшаяся находка r2 (нехерметичность AC5-теста) закрыты и лично перепроверены исполнением — включая независимое воспроизведение той же адверсариальной мутации, которой r2 доказал дефект. Продуктовый код с r1 не менялся; все обязательные и относящиеся к диапазону гейты зелёные на актуальном срезе удалённой ветки (`2d5fec0`), включая процессный `process-gate.mjs`. Вердикт: **зелёный**, задача готова к слиянию в `dev`.