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

25 KiB
Raw Blame History

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