From 2364d6dd43a03a46c0da4f790fe3deaf6c7f457f Mon Sep 17 00:00:00 2001 From: "claude[bot]" <209825114+claude[bot]@users.noreply.github.com> Date: Wed, 26 Aug 2026 07:09:22 +0000 Subject: [PATCH] docs: review document for #314 Issue: #314 User-Visible: no --- docs/reviews/CODE-REVIEW-314-r1.md | 405 ++++++++++++++--------------- 1 file changed, 198 insertions(+), 207 deletions(-) diff --git a/docs/reviews/CODE-REVIEW-314-r1.md b/docs/reviews/CODE-REVIEW-314-r1.md index b8183a2c..ea8c2a27 100644 --- a/docs/reviews/CODE-REVIEW-314-r1.md +++ b/docs/reviews/CODE-REVIEW-314-r1.md @@ -1,99 +1,111 @@ -# CODE-REVIEW-314-r1 +# CODE-REVIEW-314-r2 Issue: [#314](https://github.com/Matysh/houseplan-card/issues/314) — Model v8: рисование комнат отклоняется, draft IDs теряются, возможны ложные перегородки. +Этап: code (PROCESS.md §2.7). Ветка: `issue/314-v8-draft-write-regression`. Материал ревью: диапазон -`origin/dev...HEAD`, вершина `ce18012fb9f3c2addc957f19360a1beb9dbf96f7` -(`git rev-parse HEAD`, сверено непосредственно перед подведением итогов — §2.7). -Спека: `docs/specs/314-v8-draft-write-regression.md`. Спек-ревью r1 зелёное, -High: 0, Medium: 0 (комментарий issue от `claude`, 2026-08-26T05:38:59Z). -Заход код-ревью: **r1** — первый реальный прогон; две предыдущие попытки -(06:05:40Z, 06:10:57Z) не состоялись из-за конфликта ребейза на `dev` и не -расходуют бюджет циклов (код не читался, вердикта не было). +`origin/dev...HEAD`, вершина `64e9bf8f6b7dca3be5431613875e287b2cecc7be` +(`git rev-parse HEAD`, сверено непосредственно перед подведением итогов). -## Скоуп +## Поправка нумерации захода -18 файлов, +1219/-196: `src/houseplan-card.ts`, `src/wall-segment-model.ts`, -`custom_components/houseplan/validation.py`, новый -`demo/smoke_v8_draft_write.mjs`, `test/wall-segment-model.test.mjs`, -`tests_backend/test_wall_segment_model.py`, два мутанта в -`scripts/mutation-gate.mjs`, документация (`ARCHITECTURE.md`, -`CONFIG-COMPATIBILITY.md`, `TESTING.md`, оба `CHANGELOG`, `specs/README.md`), -сгенерированные копии бандла и `docs/images/screenshots.json` (только -`sourceFingerprint`, `imageSha256` не изменился — визуал не тронут). +Вводная к этой сессии называла заход **r1** с бюджетом **0/4**. Это не +соответствует фактическому состоянию issue: в репозитории уже лежит +`docs/reviews/CODE-REVIEW-314-r1.md` (закоммичен `069c4867`) — это реальный +завершённый прогон код-ревью с **жёлтым** вердиктом, опубликованным +комментарием issue в `2026-08-26T06:43:32Z`: -Три коммита класса A/B несут поведение и трейлеры `Issue: #314`; коммит с -поведением (`2347e8df`, `User-Visible: yes`) правит оба changelog в том же -коммите. Два後 hoc-коммита (`0e18038c`, `ce18012f`) — правки provenance -скриншотов и статуса спеки после ребейза, `User-Visible: no`, без -продуктового кода — проверено чтением diff каждого. +> Вердикт: жёлтый · заход r1 · блокирующих циклов 1/4 · High: 0 · Medium: 1 → в задаче + +Автор ответил хендофф-комментарием `2026-08-26T06:59:45Z` («Code review r1 — +M1 исправлено» → «Возвращаю в S7 для code review r2») с точным SHA `64e9bf8`. +Жёлтый вердикт расходует бюджет циклов (§4; зелёный — нет, #227), поэтому +фактическое состояние — **заход r2, блокирующих циклов израсходовано 1/4**, а +не 0/4. Действую по фактическому состоянию, а не по (неверной) вводной: +дальше в этом документе — разбор по дельте (PROCESS.md §2.9), с разделами +«Закрытие раунда r1» и «Унаследовано из r1». SHA предыдущего вердикта назван +в самом комментарии (`ce18012f`, подтверждён и в шапке +`CODE-REVIEW-314-r1.md`) — находки тут нет. + +## Скоуп разбора + +1. Вердикт r1 и его SHA найдены в issue (см. выше): жёлтый, `ce18012f`, + Medium M1 — не хватает обязательного по ТЗ (AC8) исполняемого теста на + плотной обезличенной fixture с прогоном `npm run invariants`. +2. Дельта: `git diff ce18012f..64e9bf8f` — 8 файлов, +452/-20: + `demo/fixtures/v8-draft-regression.mjs` (новый, 86 строк), правки + `demo/smoke_v8_draft_write.mjs` (+83/-3), `docs/TESTING.md` (+7), + `docs/specs/314-v8-draft-write-regression.md` (+7, AC8), плюс перегенерированные + `dist/houseplan-card.js` и `custom_components/houseplan/frontend/houseplan-card.js` + (байт-в-байт синхронизированы между собой и с текущим `npm run build`, + проверено `sha256sum` — совпадают), `docs/images/screenshots.json` + (только `sourceFingerprint`, все 10 `imageSha256` не изменились), и + собственно `docs/reviews/CODE-REVIEW-314-r1.md` (публикация предыдущего + ревью — не продуктовый код). +3. Дельта не трогает ни одного файла `src/**/*.ts`, ни + `custom_components/houseplan/*.py`, ни `tests_backend/**` — она + полностью в тестовом/фикстурном/документационном слое. Это подтверждено + `git diff ce18012f..64e9bf8f --stat` (список выше) и независимо + `node scripts/smoke-select.mjs --base ce18012f --head 64e9bf8f`, который + прямо сообщил: «Исполняемого frontend-диффа нет (src/**/*.ts не тронут)». +4. Дельта локальна: не ребейз на ушедший вперёд `dev` (это тот же топ ветки, + `origin/dev` не двигался между раундами — `git merge-base` совпадает), + не меняет контракт поведения (контракт §5–§6 спеки не правился, только + добавлен абзац AC8 с указанием, где лежит доказательство), не задевает + новую подсистему. Полный повторный разбор не требуется: проверяется + закрытие M1 и всё, до чего дельта дотягивается — а именно AC8 целиком (обе + его половины: «не удаляет» и «не плодит новые violations») и общие + гейты, которые дёшевы и гоняются каждый раунд независимо от размера + дельты (§2.10). ## Как проверялось -### Дешёвые гейты (гоняются в каждом раунде, §2.10) +### Дешёвые гейты — прогнаны заново на актуальном HEAD | Команда | Результат | |---|---| -| `npx tsc --noEmit` | зелёный | -| `npm test` | 1336 passed, 1 skipped | -| `npm run build` + `cmp dist/… custom_components/…` + `bundle:sync` | три копии бандла байт-в-байт идентичны (SHA-256 `29f2d4c7…`) | -| `node scripts/check-docs.mjs` | «Documentation checks passed (7 files, 10 external links)» — обязателен, diff трогает `src/**` | +| `npx tsc --noEmit` | зелёный, без вывода | +| `npm test` | 1336 passed, 1 skipped, 0 failed — совпадает с числом из r1 | +| `npm run build` | успешно; `sha256sum dist/houseplan-card.js custom_components/houseplan/frontend/houseplan-card.js` — идентичные хеши | +| `npm run bundle:sync` | зелёный, `git status --porcelain` после — пусто (бандл уже был синхронизирован автором) | +| `node scripts/check-docs.mjs` | «Documentation checks passed (7 files, 10 external links)» — обязателен, полный `origin/dev...HEAD` трогает `src/**` (правки унаследованы из r1) | -### Смоки — выбор по дельте, не весь набор +### Смок AC8 — прогнан лично, не со слов автора -`node scripts/smoke-select.mjs --base origin/dev --head HEAD` даёт 46 прямых -совпадений и 50 слабых (порог «широкого» превышен: 29 символов на -изменённых строках). Причина раздутого числа прямых совпадений — -`_frame`/`_modelCache`/`_wallUnionCache`/`_physicalBodiesCache = null` внутри -нового `_rollbackRejectedPhysicalWrites`: это стандартная идиома -инвалидации кэша, которая уже встречается по всему файлу, а не признак -того, что диф действительно затрагивает рендер/юнионы стен/junction-логику. -Полный прогон 46+ смоков здесь не задача ревью — это предрелизный масштаб. -Прогнаны сам как ревьюер, независимо от заявленного автором списка: +`node demo/smoke_v8_draft_write.mjs` → **16/16 checks, OK** (~11 c с учётом +запуска браузера и двух реальных прогонов `npm run invariants` внутри). Пять +новых проверок из дельты все `true`: +`denseFixtureHasOwnerReportShape`, `denseFixtureRoomSurvivesReload`, +`denseFixturePreservesIndependentGeometry`, +`denseFixtureKnownDebtIsExplicit`, `denseFixtureAddsNoInvariantViolations`. -| Смок | Результат | Почему выбран | -|---|---|---| -| `smoke_v8_draft_write.mjs` (новый) | OK, 11/11 | прямое совпадение, написан для #314 | -| `smoke_config_writer.mjs` | OK | прямое совпадение, ядро write-serialization | -| `smoke_lattice_write_barrier.mjs` | OK | прямое совпадение, `_pendingPhysicalWrites` — центральная структура фикса | -| `smoke_room_autoclose.mjs` | OK | прямое совпадение, AC5 закрывает комнату тем же путём | -| `smoke_unified_wall_tool.mjs` | OK | прямое совпадение, `_draftSegmentCms`/`_geometryHistory` в цепочке рисования | -| `smoke_wall_chain_merge.mjs` | OK | прямое совпадение, промоушен draft→partition — риск root cause C | -| `smoke_save_race.mjs` | OK | прямое совпадение `_reloadConfigOnly`, ровно про очередь записи под конфликт | -| `smoke_ws_resilience.mjs` | OK | прямое совпадение `_reloadConfigOnly`, отказ WS/восстановление | +Отдельно проверил вручную, что CLI действительно вычисляет то, что смок +утверждает, а не что смок просто доверяет своей же обёртке: -Первые шесть — то же, что назвал автор в хендоффе; `smoke_save_race` и -`smoke_ws_resilience` добавлены мной: они прямо совпадают по -`_reloadConfigOnly`, который получил новую ветку вызова -(`_reloadRejectedPhysicalWrite`), и не были явно упомянуты в отчёте автора. -Остальные 38 прямых и все 50 слабых совпадений не прогонялись — они -объясняются тем же кэш-инвалидационным идиомом или общими полями -(`_maybeRebuildDevices`, `_cfgRev` в несвязанных смоках устройств/освещения), -без содержательной связи с сегментной identity, stale-guard или rollback. +``` +npm run invariants -- --config --json +``` -`golden:verify` не запускался: визуальный результат не меняется — -`imageSha256` во всех 10 сценариях `screenshots.json` идентичен дифу, -только `sourceFingerprint` сдвинулся вслед за `src/**`. +на выходе ровно один violation — `hidden_obstacles/unusable_draft`, +`owner: "space-314-dense:draft-314-known-debt"` — байт-в-байт то, что +проверяет `denseFixtureKnownDebtIsExplicit`. Это реальный вызов канонического +`scripts/model-invariants.mjs` (`spawnSync('npm', ['run', 'invariants', ...])` +в самом смоке — не мок, не заглушка), совпадает с текстом AC10 п.15 и +формулировкой M1 «нужен прогон `npm run invariants` до/после». -### Бэкенд — реально исполнено, не переписано с чужих слов +Тест умеет падать не декларативно, а по конструкции: `denseFixtureAddsNoInvariantViolations` +сравнивает строгое JSON-равенство сигнатур находок до/после через +`checkAll()` (`demo/serve.mjs`), которая при любом `false` выставляет +`process.exitCode = 1` — появление хотя бы одной новой находки после +закрытия комнаты немедленно красит смок. -В песочнице изначально не было `homeassistant`; установил его -(`pip install homeassistant`, кэшированные wheels, ~15 c) и прогнал: +`smoke-select.mjs` по дельте `ce18012f..64e9bf8f` не выбрал ни одного +дополнительного браузерного смока (выбирать нечего — дельта не в `src/**`), +что корректно: сама дельта — это и есть новый/расширенный смок, его я и +прогнал напрямую. -- `python -m pytest tests_backend/test_wall_segment_model.py -q` → - **13 passed** — совпадает с заявленным автором числом; -- `python -m pytest tests_backend -q --ignore-glob="tests_backend/test_ha_*.py"` → - 206 passed, 1 skipped, 1 failed. Упавший тест — - `test_coordinate_canonicalization.py::test_storage_helpers_are_the_final_canonical_barrier`, - падает из-за отсутствия плагина `pytest-asyncio` в этой песочнице - (`async def functions are not natively supported`), файл дифом не - тронут — не регрессия #314, а гэп окружения. -- `test_ha_*.py` пропущены офлайн-частью (нет `pytest-homeassistant-custom-component`), - как и предупреждает `AGENTS.md`: «зелёный результат без HA ничего не - доказывает» — сказано прямо здесь, а не заявлено «verified». - -### Мутационные гейты — оба новых мутанта проверены лично +### Мутационные гейты #314 — оба перепроверены ``` node scripts/mutation-gate.mjs --id=v8-draft-sanitation-shifts-segment-identity @@ -102,159 +114,138 @@ node scripts/mutation-gate.mjs --id=v8-rejected-physical-write-keeps-optimistic- → поймано 1 из 1 ``` -### Дисциплина «тест умеет падать» — проверено самостоятельно, не со слов автора +Второй мутант гоняет «чистый прогон» +`npm run bundle:sync && node demo/smoke_v8_draft_write.mjs` — значит baseline +уже включает пять новых dense-fixture проверок и остаётся зелёным на чистом +коде, а мутация ловится (тест краснеет) с грязным — обе половины дисциплины +подтверждены. -Спека требует для AC5: «Тест обязан падать на `dev` до исправления». Поднял -временный `git worktree` на `origin/dev@98a0a24` (до фикса), перенёс туда -**только** новый `demo/smoke_v8_draft_write.mjs` и `tests_backend/test_wall_segment_model.py`, -собрал бандл этой (старой) ветки и прогнал: +### Бэкенд, инварианты проекта, golden — унаследованы из r1 без повторного прогона -- `node demo/smoke_v8_draft_write.mjs` на pre-fix коде → - завершается некэймд-исключением (`Cannot read properties of undefined - (reading '0')` — `server.spaces[0].room_drafts` пуст, потому что fake WS - отверг payload с `v8 draft wall segments require ids` ровно как - предсказывает root cause A), exit code 1; -- `pytest -k "test_current_v8_independent_geometry… or test_downgraded_independent_partition_round_trip_is_hydrated"` - на pre-fix `validation.py` → оба падают с - `WallModelClientOutdatedError: stored model=8; unchanged wall catalogue` / - `submitted model=0` — ровно root cause B. +Дельта не трогает `custom_components/houseplan/*.py`, `tests_backend/**`, +`src/**` — доказательства r1 для AC1–AC7, AC9 не могли быть задеты этой +правкой. `npm run invariants -- --config ` на отдельном экспорте +владельца не прогонялся отдельно — предмет именно этой правки уже покрыт +реальным вызовом того же CLI внутри `smoke_v8_draft_write.mjs` (см. выше). +`golden:verify` не прогонялся повторно: `imageSha256` во всех 10 сценариях +`screenshots.json` не изменились между r1 и r2 (сверено `git diff` по файлу), +предмета для повторной проверки нет. -Оба падения воспроизводят ровно те симптомы, которые ТЗ приписывает -исходному багу. Тесты не декоративны. +## Закрытие раунда r1 -### Инварианты модели — не прогонялись, с обоснованием +| Находка r1 | Чем закрыта | Где это видно | +|---|---|---| +| **M1** — ТЗ требует для AC8 исполняемый тест на минимизированной обезличенной fixture формы отчёта владельца (13 комнат/44 wall_segments/24 partitions/1 unusable draft) с прогоном `npm run invariants` до/после; в дифе r1 такого теста не было, только generic-проверка на одном объекте | Добавлена fixture `demo/fixtures/v8-draft-regression.mjs`, воспроизводящая ровно заявленную форму (13/44/24/1, проверено `wallSegments.length !== 44` guard внутри самой fixture и явными счётчиками в новых assertion). `smoke_v8_draft_write.mjs` загружает её, рисует и закрывает новую комнату реальным editor-путём, дважды по-настоящему вызывает `npm run invariants -- --config --json` (до/после) и сравнивает сигнатуры находок строгим равенством | `demo/fixtures/v8-draft-regression.mjs` (весь файл); `demo/smoke_v8_draft_write.mjs` строки с `runInvariants`/`denseFixtureKnownDebtIsExplicit`/`denseFixtureAddsNoInvariantViolations`; лично воспроизведено прогоном смока (16/16) и отдельным ручным вызовом CLI на той же fixture (см. «Как проверялось») | -Диф не трогает вычисление рёбер/толщины (`degradeWalls`, `commitWallSegmentModel`, -`deterministicWallSegmentId`) и не меняет `layout`/`marker.space`/`open_spans`. -Правки исчерпываются: (a) сохранением уже существующего client-side -`draft.segments[].id` при санитации/Undo — сама геометрия точек не -пересчитывается, дедуп той же функцией `samePoint`, что и раньше; (b) веткой -сравнения на backend, определяющей, что считать «изменением каталога»; (c) -атомарным откатом уже существующего `SpaceGeometryState`. Ни один из трёх -классов дефектов, для которых заведены инварианты (#253 потеря записи -толщины, #244/#252 неразрешимая ссылка, #258/#259 несовпадение ключа записи -толщины с ключом решёточного ребра), этой правкой не затрагивается: draft ID -— клиентский временный идентификатор с префиксом `draft:`, а не решёточный -ключ. `npm run invariants` сознательно не прогонялся. +Единственная Medium-находка r1 закрыта буквальным исполняемым тестом, а не +формальной отпиской: я независимо перезапустил и смок, и сам CLI на той же +fixture и получил тот же результат, который проверяет assertion. + +## Унаследовано из r1 + +Без повторной проверки в r2 принято по документу +`docs/reviews/CODE-REVIEW-314-r1.md` (SHA вердикта `ce18012fb9f3c2addc957f19360a1beb9dbf96f7`) — +дельта r1→r2 не трогает ни один из затронутых файлов: + +- **AC1** (ID переживают sanitation) — `test/wall-segment-model.test.mjs` + + backend schema fixture, прогнаны в r1, код `_dropLegacySegments()` не + менялся. +- **AC2** (Undo сохраняет lineage) — `smoke_v8_draft_write.mjs:successfulWriteHasStableIds`, + прогнан повторно в составе моего перезапуска смока (16/16), логика `_undoPoint()` не менялась. +- **AC3** (валидные independent writes не stale) — backend parametrized + tests, прогнаны в r1 (`test_current_v8_independent_geometry_does_not_require_contour_catalog_change` + и др.), `validation.py` дельтой не тронут. +- **AC4** (настоящий stale writer остаётся закрыт) — backend negative tests, + прогнаны в r1 (13/13 в `test_wall_segment_model.py`), файл не менялся. +- **AC5** (комната сохраняется end-to-end) — `smoke_v8_draft_write.mjs` + основной сценарий, лично подтверждено падение на pre-fix `dev@98a0a24` в + r1 (worktree-эксперимент), продуктовый код `_writeConfig()` не менялся. +- **AC6/AC7** (rollback rejected write, серийная очередь F1/F2) — + `rejectionRollsBackSynchronously`/`successQueueRetainsF2` и построчный разбор + `_rollbackRejectedPhysicalWrites()`/`_pendingPhysicalWrites` в r1, код не + менялся. +- **AC9** (совместимость/View/touch) — `imageSha256` не изменились (проверено + и в r2), backend compatibility tests из r1 не затронуты дельтой. +- **AC10** (документация и локальный гейт) — RU/EN changelog обновлены в + коммите `2347e8df` (`User-Visible: yes`) ещё в r1; в r2 дополнительно + обновлён `docs/TESTING.md` (сам предмет этого раунда) — см. выше. +- **Трейлеры и changelog** — `2347e8df` (`User-Visible: yes`) правит оба + changelog в том же коммите, ‑ подтверждено в r1 и не переоткрывалось. Новый + коммит раунда `64e9bf8f` — `User-Visible: no`, поведения не меняет + (тест/фикстура/документация), changelog ему не нужен — проверил трейлеры + напрямую (`git show -s --format=%B 64e9bf8f`). ## Находки -### Medium (в скоупе, чинится в этом issue) — M1: не поставлен обязательный тест AC8 +Ни одной High или Medium-находки не обнаружено. Одно наблюдение без статуса +находки: -**Файл:** `docs/specs/314-v8-draft-write-regression.md` (контракт) vs -отсутствие соответствующего теста в дифе. +Мой прогон `npm test` дал **1336 passed, 1 skipped** (дважды подряд, +детерминированно) — тем же числом, что и в r1. Хендофф-комментарий автора +после исправления M1 заявляет «1335 passed, 2 skipped». Разница — не +регрессия: оба пропускаемых теста в проекте условны и зависят от окружения +(`test/process-gate.test.mjs` — доступность `git`/`gh`-стаба; +`test/resize-optimize.test.mjs` — наличие приватной fixture #281), значит +дополнительный skip у автора — вопрос локального окружения, а не кода. Не +завожу как Low: 0 failed в обоих случаях, число и причина расхождения +объяснимы существующими условными `t.skip()` в этих же двух файлах, которые +дельтой не тронуты. -Раздел «Обязательные регрессионные тесты» реви́юенного (зелёного) ТЗ прямо -требует: - -> минимизированная обезличенная fixture из отчёта владельца: исправление не -> меняет существующие намеренные стены и не создаёт новых invariant -> violations. - -AC8 называет способ доказательства «model-invariant unit + review кода». -В дифе нет ни одной синтетической fixture в форме отчёта владельца (13 -комнат / 44 wall_segments / 24 partitions / 1 draft с уже существующим -`unusable_draft`), и нигде в новых тестах не вызывается -`npm run invariants` / модуль `scripts/model-invariants.mjs` — я проверил -это `grep` по всему дифу, совпадений нет. Единственный имеющийся прокси — -блок `sanitationPreservesExistingIndependentObjects` в -`demo/smoke_v8_draft_write.mjs`: это синтетическая пара «одна intentional -partition + один intentional draft», подтверждающая, что `_dropLegacySegments()` -их не роняет, но она не воспроизводит масштаб/плотность реального отчёта и -не считает инварианты вообще — только присутствие ID. - -**Почему это не «нашёл к чему придраться»:** сама спека ставит именно этот -тест в раздел «Обязательные», и спек-ревью r1 прошло по контракту, где этот -пункт уже был. Раз AC8 не имеет требуемого автотеста, по §2.7 остаётся -только «проверено чтением, не исполнением» — и чтением я могу подтвердить -первую половину claim (существующая логика фильтрации/дедупа `partitions` -и `wall_columns` в `_dropLegacySegments()` дифом не тронута, значит -персистентные объекты не удаляются новым кодом), но не вторую («не создаёт -новых invariant violations» на реалистично плотном плане) — это по природе -эмпирическое утверждение, а не то, что можно установить чтением diff'а. -Риск невысокий (никакая изменённая ветка не пишет геометрию заново), но -контракт, под который заведено зелёное ревью ТЗ, не выполнен буквально. - -**Воспроизведение отсутствия:** `git diff origin/dev...HEAD | grep -i invariant` -даёт только упоминания в тексте спеки/спек-ревью, ни одного исполняемого -вызова. - -**Что нужно:** синтетическая (обезличенная) fixture с формой отчёта -владельца — несколько комнат, независимые partitions, один заведомо -`unusable_draft` — и тест/скрипт-прогон `npm run invariants` до/после -операции «нарисовать и закрыть новую комнату», сравнивающий число findings. -Либо, если автор считает это избыточным против уже имеющегося покрытия -(AC5–AC7 через `smoke_v8_draft_write.mjs` косвенно проверяют тот же риск -через `rejectionCannotCreateGhostPartition`), явно снять пункт из ТЗ по -согласованию — но не оставлять расхождение между зелёным спек-ревью и -не выполненным пунктом молча. - -Других High/Medium/Low находок нет. - -## Проверка AC — сводно +## Проверка AC — сводно (только то, что задела дельта) | AC | Доказательство | Статус | |---|---|---| -| AC1 (ID переживают sanitation) | `test/wall-segment-model.test.mjs` unit + `tests_backend` schema, оба прогнаны | подтверждён | -| AC2 (Undo сохраняет lineage) | `smoke_v8_draft_write.mjs: successfulWriteHasStableIds`, прогнан | подтверждён | -| AC3 (валидные independent writes не stale) | `test_current_v8_independent_geometry_does_not_require_contour_catalog_change`, прогнан, падает на pre-fix | подтверждён | -| AC4 (настоящий stale writer блокируется) | 3 сохранённых негативных backend-теста, прогнаны в составе 13/13 | подтверждён | -| AC5 (комната сохраняется end-to-end) | `smoke_v8_draft_write.mjs`, реальный click/close/reload путь; лично проверено падение на pre-fix `dev` | подтверждён | -| AC6 (rejected write не оставляет transient geometry) | `rejectionRollsBackSynchronously`/`rejectionCannotCreateGhostPartition`, прогнаны; код `_rollbackRejectedPhysicalWrites` прочитан построчно | подтверждён | -| AC7 (успешная очередь не теряет новую команду) | `successQueueIsSerialized`/`successQueueRetainsF2`, прогнаны; логика earliest-`before`/selective-fingerprint-очистки прослежена по коду вручную (F1/F2 сценарий) | подтверждён | -| AC8 (данные владельца не чистятся молча) | частично: код-ревью подтверждает «не удаляет», «не плодит новые violations» не доказано автотестом | **см. M1**, дочитано, не подтверждено полностью | -| AC9 (совместимость/View/touch) | `imageSha256` неизменны, compatibility-тесты в составе 206 backend passed, UI-веток в дифе нет (grep по `render`/`html\`` в патче пуст вне уже обсуждённых мест) | подтверждён | -| AC10 (документация и локальный гейт) | все команды выше зелёные, `ARCHITECTURE/CONFIG-COMPATIBILITY/TESTING/CHANGELOG×2` обновлены в том же коммите | подтверждён | +| AC8 (данные владельца не чистятся молча, включая «не плодит новые invariant violations» на реалистичной плотности) | `demo/fixtures/v8-draft-regression.mjs` + `smoke_v8_draft_write.mjs` (`denseFixture*` группа, 5/5 checks); лично перезапущен смок и отдельно CLI `npm run invariants` на той же fixture | **подтверждён полностью** (в r1 была подтверждена только первая половина чтением, вторая — предмет M1) | + +Остальные девять AC — «Унаследовано из r1» выше. ## Что проверено чтением, не исполнением -- Полная логика `_writeConfig()`/`_rollbackRejectedPhysicalWrites()` для - сценария «F1 в полёте, F2 создан локально, F1 отклонён» — прослежена по - ссылкам объектов в `Map`, подтверждено, что `strictEntries` держит старую - ссылку, а `_pendingPhysicalWrites.get()` во время отката возвращает - актуальную запись с исходным (earliest) `before` — откатываются обе - команды на общую базу, как требует AC7. Смоковый сценарий это же - подтверждает эмпирически (см. таблицу выше), чтение было для проверки - «почему», а не единственным доказательством. -- `_restoreGeometryStateInConfig()` (не новый код, не в дифе) — подтверждено, - что при вызове без `preserveIdentityHints` (как в новом rollback-пути) он - делает полную замену полей геометрии, а не merge — соответствует - «восстановить earliest snapshot» из §6.3. -- `_cancelPath()` vs `_clearGeometryGesture()`: в rollback-пути используется - именно `_clearGeometryGesture()`, которая (в отличие от `_cancelPath()`) не - кладёт отклонённый `_activeDraftId` в `_resumeDraftBySpace` для - последующего «возобновления рисования» — иначе отвергнутый draft мог бы - быть предложен пользователю повторно тем же ID. Это осознанный и - правильный выбор, не описанный явно в тексте спеки, но соответствующий - её духу (п. 3 §6.3: «Undo или смена инструмента не могли воскресить - отклонённый draft»). -- Backend-проекции `_catalog_coupled_wall_geometry_projection` / - `_wall_catalog_projection`: построчно сверены со спекой §5.1/§5.2, - расхождений нет. +- `demo/fixtures/v8-draft-regression.mjs`: корректность формы (13 комнат × + 4 стены с одним разрезанным верхним ребром у первых четырёх = 44 + catalogue walls) проверена подсчётом по коду генератора и внутренним + guard'ом `if (wallSegments.length !== 44) throw`, который сам себя + проверяет при каждом запуске смока — не только чтением, но и исполнением + (смок прогнан). +- Диагональные `partitions` в fixture (`b: point(x + 8, y + 2)`, не + axis-aligned) — прочитано и подтверждено исполнением: реальный вызов + `npm run invariants` на этой fixture не даёт никаких находок про них + (единственная находка — заранее известный `unusable_draft`), значит для + целей этого теста (fake-WS смок, не backend-schema round-trip) форма + геометрична допустима. ## Чего не проверял -- `npm run invariants` — не прогонялся, см. обоснование выше и находку M1. -- `npm run golden:verify` — не прогонялся: визуальный вывод не меняется - (byte-equal `imageSha256`), AC9 явно говорит «не меняют визуальный - результат», основание не «пропуск», а отсутствие предмета проверки. -- Полный набор `demo/smoke_*.mjs` (192 файла) и «слабые связи» (50 файлов из - вывода `smoke-select.mjs`) — не прогонялся; это предрелизный масштаб - (§8), выбраны только прямые совпадения с содержательной связью к дифу - (см. таблицу «Как проверялось»). +- `tests_backend/**`, `python -m pytest` — не прогонял повторно: дельта + r1→r2 не касается `custom_components/houseplan/*.py` ни одной строкой; + результат r1 (206 passed, 1 skipped, 1 не связанный с #314 fail из-за + отсутствия `pytest-asyncio` в песочнице) наследуется без изменений. +- `npm run invariants -- --config <реальный экспорт владельца>` отдельной + командой — не запускал: сырой экспорт владельца недоступен в этой сессии + и не должен коммититься (см. спека §3); тот же CLI на той же по форме + синтетической fixture уже реально исполнен дважды (мной и внутри смока). +- `npm run golden:verify` — не прогонялся: `imageSha256` неизменны между r1 + и r2, предмета для проверки нет (AC9 явно требует «не меняют визуальный + результат»). +- Полный набор `demo/smoke_*.mjs` — не прогонялся: дельта не в `src/**`, + `smoke-select.mjs` по дельте `ce18012f..64e9bf8f` не выбрал ни одного + дополнительного файла; смок, который и есть предмет этого раунда, прогнан + напрямую. - `tests_backend/test_ha_*.py` — недоступны без - `pytest-homeassistant-custom-component`, канон для них — Linux CI. -- Performance-профили — не названы в AC, диф не в горячем пути рендера. -- Реальный экспорт владельца (13 комнат/44 сегмента/24 partitions) — - недоступен в этой сессии и не должен коммититься сырым; именно его - отсутствие как обезличенной fixture — предмет M1. + `pytest-homeassistant-custom-component`, канон для них — Linux CI; + дельта их не касается. +- Performance-профили — не названы в AC, дельта не в горячем пути рендера + (только тестовый код). ## Итог -High: 0. Medium: 1, в скоупе задачи (M1 — отсутствует обязательный по ТЗ -регрессионный тест AC8 на реалистичной обезличенной fixture с проверкой -инвариантов). Все остальные девять AC подтверждены прогнанными автотестами -или отслеживаемым по коду рассуждением; для двух самых рискованных -сценариев (AC3, AC5) лично воспроизведено падение на pre-fix `dev`. -Вердикт — жёлтый: без High это возврат автору на доработку в рамках -текущего issue, не новый issue (#202). +High: 0. Medium: 0. Единственная Medium-находка r1 (M1 — отсутствие +обязательного по ТЗ исполняемого теста AC8 на плотной fixture с реальным +прогоном `npm run invariants`) закрыта буквально: новая fixture нужной формы, +расширенный смок, лично перезапущенный и подтверждённый независимым ручным +вызовом того же CLI. Все десять AC подтверждены (девять унаследованы из +зелёной части r1, AC8 переподтверждён полностью в этом раунде). Дешёвые +гейты (`tsc`, `test`, `build`+bundle-sync, `check-docs`) зелёные на +актуальном HEAD, оба мутанта #314 пойманы. + +Вердикт: зелёный · заход r2 · блокирующих циклов 1/4 · High: 0 · Medium: 0