Files
houseplan-card/legacy/reviews/v1.65.0/CODE-REVIEW-198-r3.md
T
Claudeandclaude[bot] 0991c45374 fix(tools): архив переписывает относительные ссылки перенесённых документов (#682)
Ревью #682 r1, Medium: перенос добавляет документу уровень вложенности
(`docs/reviews/X.md` → `legacy/reviews/<тег>/X.md`, `docs/specs/` →
`legacy/specs/`), а относительные ссылки внутри перенесённых документов и в
соседях, ссылавшихся на них, никто не пересчитывал — на `97d19268` 53 битые
ссылки в 46 файлах (заявление «все 26 резолвятся» в `7feb6177` было верно
только до переноса документов ревью). Гейты архив не смотрят.

`reviews-archive.mjs`: `repairLinks` пересчитывает ссылку, если она не
резолвится от нового места, а цель находится от нового или старого места
через карту переносов; битая и до переноса ссылка не трогается. `--apply`
делает это само, `--repair-links=<rev>` — для всех переименований
`<rev>..HEAD`, `--check-links` печатает битые. Этим коммитом
`--repair-links=origin/dev` переписал ровно 53 ссылки в 46 файлах; остались
две прежние «...»-заглушки в CODE-REVIEW-448-r2 (битые и на dev). Тесты:
перенесённый документ, сосед со ссылкой в архив, ТЗ со ссылкой на позже
перенесённое ревью, битая-до-переноса не трогается, в `legacy/` битых нет;
мутант `reviews-archive-links-from-new-place-only`. PROCESS §2.10 и
DEVELOPMENT › Release называют переписывание и `--check-links`.

Issue: #682
User-Visible: no
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
2026-09-27 22:10:47 +00:00

218 lines
18 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.
# Код-ревью 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`.