From 9bbba66d113e29fbd12c3f2d18880ad699042903 Mon Sep 17 00:00:00 2001 From: "claude[bot]" <209825114+claude[bot]@users.noreply.github.com> Date: Thu, 10 Sep 2026 07:17:50 +0000 Subject: [PATCH] docs: review document for #500 Issue: #500 User-Visible: no --- docs/reviews/SPEC-REVIEW-500-r2.md | 291 +++++++++++++++++++++++++++++ 1 file changed, 291 insertions(+) create mode 100644 docs/reviews/SPEC-REVIEW-500-r2.md diff --git a/docs/reviews/SPEC-REVIEW-500-r2.md b/docs/reviews/SPEC-REVIEW-500-r2.md new file mode 100644 index 00000000..56144da5 --- /dev/null +++ b/docs/reviews/SPEC-REVIEW-500-r2.md @@ -0,0 +1,291 @@ +# SPEC-REVIEW-500-r2 + +- **Issue:** https://github.com/Matysh/houseplan-card/issues/500 +- **Этап:** ревью ТЗ (PROCESS.md §2.4) +- **Заход:** r2 · блокирующих циклов израсходовано 1 из 4 до этого раунда +- **Материал:** ветка `issue/500-config-adoption-boundary`, коммит + `c35ecf13206bb5037f86f1e55fc27b8ed46604ee` (docs-only поверх ревью-документа + r1 `565a0c4a`), файл ТЗ `docs/specs/500-config-adoption-boundary.md`, + дерево `9a354a9fa366fcca195773d22a15f7947caf5d14`, + блоб ТЗ `5cfc36e2614361d1e90d4d9a8a9a7358d4bec3b9`. +- **Ревьюер:** Claude (роль «ревьюер ТЗ», отдельно от автора). +- **Предыдущий раунд:** `docs/reviews/SPEC-REVIEW-500-r1.md`, вердикт жёлтый, + материал `ca07067386dee0ce2bfad64c169404c9284cd242` (докс-коммит поверх + `origin/dev` = `6b31e945`). + +## Скоуп ревью (по §2.10 — объём по дельте) + +Разбор по дельте: r1 нашёл две High-находки, обе — неполная инвентаризация +кода, на которой построены AC. Автор ответил коммитом `c35ecf13` (поверх +`ca070673`), диапазон изменений — только `docs/specs/500-config-adoption-boundary.md`. +Дельта не локальна в смысле «одна строка»: она вводит новое понятие (два +профиля adoption вместо одного) и переписывает §3, §4, §6.3, §6.4, AC1–AC4, +§11, §12 — то есть проверке подлежит вся дельта диффа `ca070673..c35ecf13`, +а не только формальное «закрыт ли пункт High-1/High-2». AC5–AC8, §7–10, §13–14 +дельта не задевает — унаследованы из r1 без повторной проверки (раздел ниже). + +Как и в r1, факт-чек количественных и структурных утверждений сделан прямым +чтением `src/**` на рабочей копии, соответствующей материалу (докс-only ветка +поверх текущего `dev`, код идентичен тому, что описывает ТЗ). + +## Как проверялось + +``` +grep -rn "_adoptStructuralResponses" src/ +grep -rn "_serverCfg\s*=[^=]" src/ --include=*.ts # исключая === сравнения +grep -rn "_layoutRev\s*=[^=]" src/ --include=*.ts +grep -rn "_cfgContentFingerprint\s*=[^=]" src/ --include=*.ts +grep -rn "_layoutContentFingerprint\s*=[^=]" src/ --include=*.ts +sed -n '85,115p' src/editors/vacuum-maps-section.ts src/vacuum-calibration-write.ts +sed -n '460,485p' src/houseplan-onboarding-runtime.ts +sed -n '8680,8700p;9560,9600p;9720,9760p' src/houseplan-editor-runtime.ts +grep -n "_adoptInitialSpace\|_resumePendingNavMode\|_restoreZoom" src/houseplan-editor-runtime.ts src/houseplan-onboarding-runtime.ts +``` + +Гейты кода не запускались — этап ревью ТЗ, продуктовый код не меняется этим +коммитом. Проверка гейтов относится к код-ревью (§2.7). + +## Закрытие раунда r1 + +| Находка r1 | Чем закрыта в r2 | Где это видно | +|---|---|---| +| **High-1** — 4 названных вызывающих `_adoptStructuralResponses` против 7 реальных; дубликат `space/delete` в `houseplan-editor-runtime.ts:8693` с тем же дефектом M1 #490 не учтён AC2/AC4 | §3 п.2–3 переписаны: семь вызывающих в двух профилях (`reload` ×3, `post-write` ×4, включая оба `space/delete`); §6.3 вводит явный параметр `profile`; §6.4 добавляет host-интерфейс editor-runtime (`:842`); AC2 — «имя не встречается в `src/**` вне модуля» (было: «не существует как метод хоста», не видело второй `_adoptStructuralResponses` в `houseplan-editor-runtime.ts:842` как объявление интерфейса); AC4 расширен на все четыре post-write пути и оба входа `delete`, новый смок `demo/smoke_post_write_adoption.mjs` | Диф §3 п.2–3, §6.3, §6.4, AC2/AC4 в `docs/specs/500-config-adoption-boundary.md`. Построчно перепроверено чтением: все 7 вызовов (`houseplan-card.ts:4350,4527`; `summary-panel-runtime-loaded.ts:625`; `houseplan-onboarding-runtime.ts:476`; `houseplan-editor-runtime.ts:8693,9577,9736`) подтверждены на материале ревью | +| **High-2** — 16/6 заявлено, фактически 18 присваиваний `_serverCfg =` в 8 модулях; `vacuum-calibration-write.ts:108` и `editors/vacuum-maps-section.ts:107` не решены AC1 | §3 п.1 — «18 присваиваний в 8 модулях», оба vacuum-файла названы с номером строки; §6.4/AC1 — оба файла явно в объёме переноса на `beginOptimistic`/`stageLocalConfig`/`rollbackOptimistic` модуля, не в allowlist (AC1 требует ноль) | Диф §3 п.1, §6.4, §4 п.5, AC1. Построчно перепроверено: `grep -rn "_serverCfg\s*=[^=]" src/` даёт ровно 18 строк в 8 файлах, включая обе названные; оба vacuum-writer'а вызывают `optimisticAttempt`/`rollbackOptimistic` из `serialized-write-queue.ts`, что подтверждает применимость переноса | + +Обе High-находки r1 закрыты содержательно, не декларативно: числа и строки +кода, названные в r2, совпадают с кодом на материале ревью (см. «Что +проверено и корректно» ниже — с деталями по каждому пересчитанному числу). + +## Находки + +### High-3 — новый текст r2 неверно утверждает, что `_adoptInitialSpace` сегодня не вызывается ни на одном post-write пути; фактически он вызывается в `import/apply` для фиксированного этажа, и именно там AC3/AC4/риск-таблица опираются на обратное + +**Резюме.** Понятие «два профиля» и связанные с ним утверждения — целиком +новый текст r2, ранее не рецензировался. §6.3 (`docs/specs/500-config-adoption-boundary.md:213-216`) +формулирует это как факт о текущем коде: + +> Пост-шаги профиля `post-write`: … `_adoptInitialSpace`/`_resumePendingNavMode`/ +> `_restoreZoom` в этот профиль **не входят**: сегодня их там нет, а выбор +> пространства после удаления делает `_commitSpace` вызывающего. + +Список «Особенности вызывающих» (`:226-227`) для `import/apply` называет +только `_dirtyPos`/`_sentPos`/`_defPos`, снапшоты устройств, +`_signer.invalidate`+`_resign`, `_cfgEpoch++` — без единого слова о выборе +пространства. + +Чтение `src/houseplan-editor-runtime.ts` на материале ревью (обработчик +`import/apply`, окружает вызов `_adoptStructuralResponses` на строке 9736) +показывает обратное: + +``` +9751: const spaces = this.host._serverCfg?.spaces || []; +9752: const nextSpace = result.kind === 'space' && result.space_id +9753: ? result.space_id +9754: : spaces.some((space) => space.id === previousSpace) +9755: ? previousSpace : spaces[0]?.id || this.host._space; +9756: if (this.host._hasFixedFloor) this.host._adoptInitialSpace(this.host._model, true); +9757: else this.host._commitSpace(nextSpace); +``` + +`_adoptInitialSpace` (объявлен `houseplan-card.ts:3708`, делегат в +host-интерфейсе editor-runtime `:841`) — та же функция, что вызывают +«reload»-пути `_loadFromServer`/`_reloadConfigOnly` (`houseplan-card.ts:4352,4529`). +Она **не** сохраняет `previousSpace`: `_initialSpaceSelection` внутри неё +выбирает пространство по собственным правилам (hash/saved/default) и вызывает +`_commitSpace(selection.id, true)` независимо от вычисленного тут же +`nextSpace`. То есть сегодня после `import/apply` в конфигурации с +зафиксированным этажом видимое пространство определяет `_adoptInitialSpace`, +а не «попытка сохранить `previousSpace`», которую код вычисляет, но +отбрасывает в этой ветке. + +**Почему это блокирует.** Три места ТЗ прямо опираются на утверждение +«`_adoptInitialSpace` не вызывается ни на одном post-write пути сегодня»: + +1. §6.3 — сам факт, процитирован выше; +2. AC3 (`:283`) — «профиль `post-write` не вызывает + `_adoptInitialSpace`/`_resumePendingNavMode`/`_restoreZoom`» как часть + доказательства поведенческой нейтральности; +3. §12, строка риска (`:320`) — «Профиль `post-write` случайно получит шаги + `reload`… смок AC4 проверяет, что видимое пространство после Import не + меняется» — это неверно для конфигурации с зафиксированным этажом: там + пространство **должно** активно выбираться `_adoptInitialSpace`, а не + оставаться прежним. + +Если реализовать буквально по тексту — разработчик, доверяя §6.3, вправе не +перенести вызов `_adoptInitialSpace` в обработчик `import/apply` внутри +модуля (ведь профиль его «не вызывает» по документу), и после рефакторинга +импорт в план с зафиксированным этажом перестанет корректно выбирать +пространство — новая регрессия ровно того класса, который issue #500 и +задуман закрыть (новый/переписанный путь пропускает правило adoption). Если +же разработчик решит буквально удовлетворить AC4/смок «пространство после +Import не меняется» — это будет расходиться с сегодняшним поведением +фикс-этажных планов, то есть сам смок и есть некорректный свидетель +поведенческой нейтральности для этого случая. + +**Что не задето.** `_resumePendingNavMode` и `_restoreZoom` на post-write +путях действительно не встречаются (проверено тем же grep — оба совпадения в +`houseplan-editor-runtime.ts:2443,7534` относятся к geometry-undo и +inbox-навигации устройства, не к четырём post-write путям). Часть +утверждения §6.3 верна; неверна ровно доля про `_adoptInitialSpace`. + +**Что нужно.** Скорректировать §6.3 (снять «сегодня их там нет» в части +`_adoptInitialSpace`), явно дописать в «Особенности вызывающих» для +`import/apply` ветку `_hasFixedFloor` (`_adoptInitialSpace` вместо +`_commitSpace(nextSpace)`) как caller-specific поведение, которое переносится +1:1 и не входит в общие пять шагов профиля `post-write`; исправить +формулировку AC3 и риск-строку §12, чтобы смок AC4 проверял неизменность +пространства только для не-фикс-этажного случая, а для фикс-этажного — +что `_adoptInitialSpace` по-прежнему вызывается после adoption (не +теряется). + +## Что проверено и корректно (сверх унаследованного) + +Все точечные числа, добавленные или изменённые в r2, перепроверены построчным +чтением и совпадают с кодом на материале ревью: + +- **Семь вызывающих `_adoptStructuralResponses`** — подтверждено: 3 reload + (`houseplan-card.ts:4350,4527`; `summary-panel-runtime-loaded.ts:625`) + 4 + post-write (`houseplan-onboarding-runtime.ts:476`; + `houseplan-editor-runtime.ts:8693,9577,9736`), плюс объявление метода + (`houseplan-card.ts:4231`) и два интерфейсных объявления + (`summary-panel-host.ts:87`, `houseplan-editor-runtime.ts:842`) — ровно как + в §3 п.2 и AC2. +- **Оба `space/delete` — байт-в-байт близкий дубликат с тем же дефектом**: + построчное сравнение `houseplan-onboarding-runtime.ts:466-478` и + `houseplan-editor-runtime.ts:8683-8695` подтверждает идентичную + структуру и идентичную перезапись `_cfgRev`/`_layoutRev` из ответа + `delete` после adoption в обеих копиях — ровно так, как описывает §3 п.3. +- **`_serverCfg =` — 18 присваиваний в 8 модулях.** Полный пересчёт (с + исключением ложных `===`/`!==` срабатываний, которые давал более грубый + паттерн) даёт: `houseplan-card.ts` 6, `houseplan-editor-runtime.ts` 6, + `plan-optimize-write.ts` 1, `serialized-write-queue.ts` 1, + `space-copy-runtime.ts` 1, `summary-panel-runtime-loaded.ts` 1, + `vacuum-calibration-write.ts` 1, `editors/vacuum-maps-section.ts` 1 — сумма + 18, модулей 8. Совпадает с §3 п.1 дословно, включая номера строк обоих + vacuum-файлов (107/108). + Побочное наблюдение: r1 в скобках утверждал, что + `houseplan-editor-runtime.ts` «фактически даёт 8, не 6» — при пересчёте это + не подтвердилось: грубый `grep` без фильтра `!==`/`===` ловит сравнения + `this.host._serverCfg === cfg` (`:7521,8243`) как ложные совпадения; после + фильтра реальных присваиваний — ровно 6, как и было в исходном ТЗ и как + осталось в r2. Не находка (число в r2 верно), но стоит явно + зафиксировать: сам аргумент r1 в этой частности был артефактом паттерна, + а не второй скрытой находкой, оставшейся неисправленной. +- **`_layoutRev =` — 7 присваиваний в 4 модулях** (`houseplan-card.ts` 4, + `houseplan-onboarding-runtime.ts` 1, `houseplan-editor-runtime.ts` 1, + `plan-optimize-write.ts` 1, после исключения объявления поля) — совпадает + с §3 п.1. +- **fingerprint config — 10 мест в 5 модулях, fingerprint layout — 5 мест** — + оба числа пересчитаны (после исключения объявлений полей) и совпадают с + §3 п.1 дословно. +- **Оба vacuum-writer'а действительно используют `optimisticAttempt`/ + `rollbackOptimistic`** из `serialized-write-queue.ts` и пишут + `host._serverCfg =` напрямую в обход какого-либо централизованного шага — + подтверждено чтением тел функций (`vacuum-maps-section.ts:89-113`, + `vacuum-calibration-write.ts:85-113`); включение их в перенос на + `beginOptimistic`/`stageLocalConfig` (§6.4/AC1) методологически корректно. +- Внутренняя согласованность новых разделов: `reason` в сигнатуре + `adoptAuthoritativeGated` расширен на `'optimize-undo'`/`'import-apply'` + синхронно с §3/§6.3; мутант `post-write-skips-asset-gate` (§11) корректно + заменяет прежний `space-delete-skips-asset-gate`, покрывая все четыре пути, + а не один. +- §15 п.7 — новый пункт «два профиля, а не один» — обоснование не + голословно: реальный набор пост-шагов у `reload` и `post-write` сегодня + действительно разный (подтверждено тем же чтением, что и выше), склейка + профилей была бы поведенческим изменением, а не только рефакторингом. + +## Унаследовано из r1 + +Следующее принято без повторной проверки в r2, дельта их не касается: + +- Обязательные разделы §7.1 присутствуют все — проверено в + `docs/reviews/SPEC-REVIEW-500-r1.md`, материал `ca070673`. Раздел «Статус + ТЗ» и структура файла в r2 не менялись содержательно (только пометка + «r2 — учтены High-1/High-2»). +- Отказ от `small` обоснован названными критериями §5 (сложность/риск 7/10, + 7 модулей, state-контракт) — §4 ТЗ не менялся в r2, вывод r1 остаётся в силе. +- Продуктовых вопросов владельцу нет, блок §15 «принято предположительно» + использован по назначению — методология не изменилась, новый пункт §15.7 + ей соответствует (проверен отдельно выше). +- `houseplan-card.ts` — 13699 строк при бюджете `core-file-budget` 13700 + (§3.5/AC7) — число не затронуто дельтой r2, унаследовано из r1. +- Восемь методов `SummaryPanelHost` (`summary-panel-host.ts:84-94`), + сведение к одному — не затронуто дельтой (§6.4 в этой части не менялся), + унаследовано из r1. +- Смок `demo/smoke_summary_panel.mjs` содержит сценарий + `recoveryPreparesBackdropBeforeAdoption` — не затронуто дельтой, + унаследовано из r1. +- I2/I4 (ревизия только вместе с телом; ссылочная идентичность без + клонирования) корректно отражают текущее поведение — раздел §6.1-6.2 не + менялся дельтой r2 по существу, унаследовано из r1. +- Скоуп по `docs/SCOPE.md` (J6, техдолг, без новой пользовательской + поверхности) и корректность §5 «не входит» (lifecycle registry #493/#425, + umbrella-неизменяемость #34/#425) — не затронуты дельтой, унаследованы из + r1. +- AC5 (тёплый старт), AC6 (optimistic rollback), AC7 (бюджеты), AC8 — + дельта r2 их не редактировала (кроме синхронизации со счётчиками §3, что + проверено выше отдельно) — содержательно унаследованы из r1, где они уже + входили в «что проверено и корректно». + +Материал для обеих ссылок — `docs/reviews/SPEC-REVIEW-500-r1.md`, SHA +`ca07067386dee0ce2bfad64c169404c9284cd242` (дерево +`183f1ddc6b6261a2345fcc84502883e6274a9b70`). + +## Чего не проверял + +- Достижимость самого рефакторинга (перенос кода) — кода ещё нет, ревью ТЗ + оценивает план. +- Автотесты/typecheck/build не запускались — класс изменения r2 докс-онли, + как и r1; гейты кода — предмет код-ревью (§2.7). +- Полный построчный аудит `plan-optimize-write.ts`/`space-copy-runtime.ts` на + предмет иных, не названных ни в r1, ни в r2 расхождений — не проводился; + фокус остался на утверждениях, которые дельта r2 реально вводит или + пересчитывает (профили, семь вызывающих, писатели identity), а не на + повторном полном аудите всех шести исходно названных модулей. +- Не проверялось, есть ли у `optimize_undo`/`import/apply` собственные + расхождения того же типа, что найден у `import/apply` (`_adoptInitialSpace`), + сверх этого одного — например, полная построчная сверка `_cfgEpoch++`/ + `_signer.invalidate`/`_resign` списка на предмет пропущенных вызовов; + проверена только заявленная в находке High-3 область (выбор пространства). + Если High-3 будет чиниться правкой этого блока, стоит перечитать соседние + строки той же функции ещё раз в r3. + +## Вердикт + +High: 1 (High-3, в скоупе задачи — чинится автором тем же текстовым +изменением ТЗ, отдельный issue не заводится). Medium: 0. + +Обе High-находки r1 закрыты содержательно и проверяемо. Но дельта r2 ввела +новое понятие («два профиля»), и именно в нём — в описании того, что +`import/apply` делает сегодня — обнаружена новая, ранее не рецензированная +фактическая неточность того же класса, что High-1/High-2: уверенное +утверждение о коде («сегодня их там нет»), которое не подтверждается чтением, +и на которое опирается конкретный AC (AC3) и конкретный смок (AC4). Задача не +может уйти в разработку, пока инвентаризация `import/apply` не учитывает +`_adoptInitialSpace` в ветке `_hasFixedFloor` — иначе рефакторинг рискует +тихо уронить выбор пространства после импорта в конфигурациях с +зафиксированным этажом, то есть повторить дефект класса M1 #490 внутри +задачи, которая создана его закрыть. + +**Вердикт: жёлтый · заход r2 · блокирующих циклов 2/4 · High: 1 · Medium: 0 → в задаче** + +--- + + + +--- + + + +## Материал раунда + +- Ветка: `issue/500-config-adoption-boundary`, коммит `c35ecf13206b` — ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет. +- Дерево материала: `9a354a9fa366fcca195773d22a15f7947caf5d14` + ``` + git log --all --format='%H %T' | grep 9a354a9fa366 + ``` +- ТЗ `docs/specs/500-config-adoption-boundary.md`, блоб `5cfc36e2614361d1e90d4d9a8a9a7358d4bec3b9` + ``` + git log --all --find-object=5cfc36e2614361d1e90d4d9a8a9a7358d4bec3b9 -- docs/specs/500-config-adoption-boundary.md + ``` +- Вердикт конвейера: `yellow` · High 1