diff --git a/docs/reviews/CODE-REVIEW-290-r4.md b/docs/reviews/CODE-REVIEW-290-r4.md new file mode 100644 index 00000000..489ea776 --- /dev/null +++ b/docs/reviews/CODE-REVIEW-290-r4.md @@ -0,0 +1,152 @@ +# CODE-REVIEW-290-r4 + +- **Issue:** #290 — «Уступ в один шаг на общей стене создаётся молча и невидим при обычном зуме» +- **Этап:** код-ревью (PROCESS.md §2.7) +- **Заход:** r4 · блокирующих циклов израсходовано на входе 2 из 4 +- **Ветка:** `issue/290-near-axis-authoring-repair` +- **HEAD на момент ревью:** `01512081db0d41cb86c921a61961275c8c18aedb` +- **Предыдущий раунд:** r3, жёлтый, `docs/reviews/CODE-REVIEW-290-r3.md`, + SHA на котором получен вердикт — `d33ae88da4049bdbdfd02c57f935199007157cf3` + (назван в самом документе r3, раздел «HEAD на момент ревью» — в этот раз + находки в процессе нет). + +## Разбор по дельте (PROCESS.md §2.10) — почему сокращённый объём законен + +Дельта: `git diff d33ae88d..HEAD` — **2 коммита, 2 файла, только классы C и B**: + +``` +docs/reviews/CODE-REVIEW-290-r3.md | 218 ++++++++++++++++ (класс C, документ ревью r3) +test/near-axis.test.mjs | 15 ++ (класс B, только новый тест) +``` + +Ни одного файла класса A. `src/**` этим раундом не тронут вовсе — проверено +`git diff --stat d33ae88d..HEAD -- src/`: пусто. Ветка полностью содержит +`origin/dev` (`git merge-base --is-ancestor origin/dev HEAD` — true, `dev` не +уходил вперёд после r3), контракт поведения не менялся, новая подсистема не +задета, объём дельты (15 строк теста) многократно меньше исходной задачи. +Ни одно из исключений §2.10 («ребейз на ушедший вперёд dev», «смена контракта», +«новая подсистема», «объём сопоставим с задачей») не применяется → +разбор ограничен закрытием единственной находки r3 (M1) и дешёвыми гейтами, +которые гоняются в любом раунде. + +## Скоуп раунда + +Единственная находка r3 (Medium M1, в скоупе задачи, жёлтый вердикт) требовала +механического source guard на AC1: «Authoring, Optimize и renderer #279 +импортируют один threshold source; source guard запрещает отдельные литералы +`0.25` в этих classifiers» (`docs/specs/290-near-axis-authoring-and-repair.md:173-174`). +Автор выбрал вариант (а) из двух, предложенных r3: статический guard-тест, +читающий исходники трёх потребителей как текст и требующий literal import + +literal вызов/присваивание общего near-axis API, а не инлайн-константу. + +## Закрытие раунда r3 + +| Находка | Чем закрыта | Где это видно | +|---|---|---| +| **M1** (Medium, в скоупе): AC1 обещает source guard против инлайн-литерала `0.25` в renderer/authoring/Optimize; такого guard'а не было — числовое равенство в `test/near-axis.test.mjs:23` не ловит дублирование значения, только его совпадение | Новый тест `#290 authoring, Optimize and renderer use one near-axis threshold source` (`test/near-axis.test.mjs:22-34`, коммит `01512081`) читает `src/wall-thickness.ts`, `src/houseplan-card.ts`, `src/plan-optimizer.ts` как текст и требует буквального `import … from './near-axis'` плюс использования соответствующего экспорта в каждом из трёх файлов | Воспроизвёл ровно ту же мутацию, что и r3: заменил в `src/wall-thickness.ts:85` импортированную ссылку на инлайн-литерал `= 0.25` (удалив зависимость от `NEAR_AXIS_MAX_DEGREES`) — новый guard-тест **краснеет** (`The input did not match the regular expression /import \{ NEAR_AXIS_MAX_DEGREES \} .../`). Дополнительно проверил ту же схему для двух других потребителей: убрал `import { snapNearAxisEndpoint } from './near-axis';` из `src/houseplan-card.ts` — тест краснеет; убрал `import { repairNearAxisRoomWalls } from './near-axis';` из `src/plan-optimizer.ts` — тест краснеет. После каждой мутации файл восстановлен, `git status --short` пуст, `npm test` снова зелёный (см. таблицу гейтов) | + +Других находок в r3 не было (High:0, остальные Medium:0) — закрывать больше +нечего. + +## Как проверялось — гейты + +| Гейт | Команда | Результат | +|---|---|---| +| Typecheck | `npx tsc --noEmit` | чисто, без ошибок | +| Unit (полный, дешёвый гейт гоняется в любом раунде) | `npm test` | 1235 тестов: 1234 passed, 1 skipped, 0 failed (на 1 больше, чем в r3 — новый guard-тест); подтверждено `npm run inventory` (Node unit: 1235) | +| Build + сверка копий бандла | `npm run build`; `diff dist/houseplan-card.js custom_components/houseplan/frontend/houseplan-card.js`; `git status --short` | пересобранный `dist` побайтово идентичен закоммиченному и копии в `custom_components`; `git status` после пересборки чист — рабочая копия не разошлась с закоммиченными артефактами | +| Дисциплина «тест умеет падать» — M1 (renderer) | откат `src/wall-thickness.ts:85` к инлайн-литералу `0.25` без импорта, `npx tsc -p tsconfig.test.json && node scripts/fix-test-build.mjs && node --test --test-name-pattern="#290 authoring, Optimize and renderer..." test/near-axis.test.mjs` | тест краснеет (regex import не совпал); после отката файла — снова зелёный, `git status` чист | +| Дисциплина «тест умеет падать» — M1 (authoring) | тот же приём для `import { snapNearAxisEndpoint } from './near-axis';` в `src/houseplan-card.ts` | тест краснеет; откат — зелёный | +| Дисциплина «тест умеет падать» — M1 (Optimize) | тот же приём для `import { repairNearAxisRoomWalls } from './near-axis';` в `src/plan-optimizer.ts` | тест краснеет; откат — зелёный | +| Smoke-select по дельте раунда | `node scripts/smoke-select.mjs --base d33ae88d --head HEAD` | «Исполняемого frontend-диффа нет (src/**/*.ts не тронут). Browser-smoke этим диффом не выбираются — выбирать нечего» — согласуется с тем, что дельта не трогает `src/**` | +| Docs fingerprint | не перезапускал | не требуется этим раундом: `src/**` дельтой не тронут (см. «Разбор по дельте»); последний прогон — r3, «Documentation checks passed (7 files, 10 external links)», наследуется | +| Инварианты геометрии | не перезапускал | не требуется: дельта не трогает геометрию/`layout`/`marker.space`/`open_spans`; последний прогон — r3, код 0 на обоих real-plan фикстурах, наследуется | +| Golden | не перезапускал | не требуется: дельта не меняет рендер/UI; последний прогон — r3, 161/161 passed, наследуется | +| Backend pytest | не запускал | `git diff --stat d33ae88d..HEAD -- 'custom_components/**/*.py'` пуст | + +## Находки + +Нет. High: 0, Medium: 0. + +## Что проверено и корректно + +- **M1 закрыт по существу, не только по факту существования теста** — три + прямые мутации (см. таблицу гейтов) доказывают, что guard действительно + ловит инлайн-литерал в каждом из трёх названных AC1 потребителей + (`wall-thickness.ts`, `houseplan-card.ts`, `plan-optimizer.ts`), а не только + в одном из них. +- **Guard читает исходник, а не собранный бандл** — `sourceOf()` + (`test/near-axis.test.mjs:20`) делает `readFileSync` из `src/*.ts` напрямую, + минуя `test-build/`; расхождение между исходником и типизированной сборкой + тест не маскирует. +- **Регекс на вызов, а не только на импорт** — `assert.match(authoring, + /snapNearAxisEndpoint\(/)` и `assert.match(optimizer, + /repairNearAxisRoomWalls\(space\)/)` отдельно требуют реального использования + импортированного символа, а не только присутствия строки импорта (мёртвый + импорт не прошёл бы эту часть проверки). +- **Трейлеры и класс изменения** — `01512081` (`test: enforce shared near-axis + threshold source`) несёт `Issue: #290`, `User-Visible: no`; правильно, это + чисто тестовый файл (`test/**`, класс B), продуктовое поведение не менялось, + changelog не требуется. +- **Дерево не расходится с гейтами** — до, во время и после всех мутационных + проверок `git status --short` оставался чист; финальная пересборка `dist` + побайтово совпадает с обеими закоммиченными копиями. +- **AC2–AC10, H1-регрессия r1, mutation-gate ×6, golden, инварианты, single + source of numbers, совместимость, performance** — не пересматривались в этом + раунде: дельта их не задевает (см. «Разбор по дельте»), полностью + унаследованы из r3 (раздел ниже), который сам разобрал их читая весь + `origin/dev...HEAD` (а не по дельте) из-за промежуточного ребейза. + +## Чего не проверял + +- **`node scripts/check-docs.mjs`, `npm run invariants`, `npm run + golden:verify`, полный `mutation-gate`, touch-смоки, `pytest tests_backend`** + — не перезапускал в этом раунде; все они не выбираются ни диффом (`src/**` + не тронут), ни AC, задетым этой дельтой (только AC1/source guard). Их + последние зелёные прогоны — в r3, см. таблицу гейтов там и «Унаследовано» + ниже. +- **Внешний Linux CI Validate на точном HEAD `01512081`** — не проверял + напрямую; последняя подтверждённая владельцем ссылка (Validate 32733382537) + относится к SHA `d33ae88d`, до этого раунда. Пересборка/тесты локально + воспроизводят зелёное состояние, но отдельного прогона CI на новом HEAD в + комментариях issue не найдено на момент ревью. +- **Комментарий-хендофф автора после r3** (issue #290, id `5396316118`, + 2026-08-24T14:03:02Z) обрывается на полуслове: «Исправление Medium M1 из + code review r3 запушено в \» — SHA коммита-фикса в нём не назван. Это не + влияет на вердикт: коммит независимо идентифицирован через `git log + d33ae88d..HEAD` (`01512081`, единственный коммит с продуктовым/тестовым + содержимым в дельте) и проверен напрямую, а не со слов автора. Отмечаю как + находку процесса, не как находку кода — вердикт не снижается, править + нечего в этой ветке. + +## Унаследовано из r3 + +Документ: `docs/reviews/CODE-REVIEW-290-r3.md`, SHA `d33ae88da4049bdbdfd02c57f935199007157cf3` +(назван в самом документе r3). Принято без повторной проверки в этом раунде, +так как дельта `d33ae88d..HEAD` их не задевает: + +- **AC1 (кроме source guard), AC2–AC10** — весь предметный разбор по коду + (`src/near-axis.ts`, `src/houseplan-card.ts`, `src/resize.ts`, + `src/plan-optimizer.ts`, `src/wall-thickness.ts`, + `scripts/model-invariants.mjs`) — r3 прочитал и перепроверил их **полностью** + (не по дельте относительно r2, а заново, из-за промежуточного ребейза на + ушедший вперёд `dev`), включая прямой откат H1-регрессии r1 и все 6 + обязательных mutation-gate ID индивидуально. +- **Golden (161/161 сценариев)**, **docs fingerprint** (7 файлов, 10 внешних + ссылок), **near-axis профиль на обоих real-plan фикстурах** (`--near-axis` + CLI), **bundle parity на момент r3** — гоняны и зафиксированы в r3, дельта + этого раунда не меняет ни один из задействованных там файлов. +- **Продуктовые решения владельца** (авто-выравнивание без modifier bypass, + подтверждаемый lossy-пас Optimize с отчётом/Undo, единый допуск `0.25°`) — + зафиксированы на этапе ТЗ (`SPEC-REVIEW-290-r2`, зелёный, `dc9f9a6c`) и не + относятся к коду; ребейз и текущая дельта их не касаются. +- **Backend** — `custom_components/**/*.py` не менялся ни в r3, ни в этой + дельте; `pytest tests_backend` по-прежнему не требуется. + +## Вывод + +Единственная находка предыдущего раунда (M1) закрыта и подтверждена прямой +мутацией на всех трёх названных в AC1 потребителях, а не только заявлением +автора. Новых находок дельта не создаёт. Дешёвые гейты (`tsc`, `npm test`, +`build` + сверка бандла) зелёные на текущем HEAD. High: 0, Medium: 0 → +**зелёный вердикт**.