Files
houseplan-card/docs/reviews/SPEC-REVIEW-500-r2.md
T
2026-09-10 11:41:16 +00:00

292 lines
25 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.
# 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