docs: review document for #500

Issue: #500
User-Visible: no
This commit is contained in:
claude[bot]
2026-09-10 11:41:16 +00:00
parent b4a2d5b952
commit 9bbba66d11
+291
View File
@@ -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 → в задаче**
---
<!-- material-anchors: заполняется конвейером публикации -->
---
<!-- material-anchors: сгенерировано конвейером (#414) -->
## Материал раунда
- Ветка: `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