25 KiB
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 поверх ревью-документа r1565a0c4a), файл ТЗ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 пути сегодня»:
- §6.3 — сам факт, процитирован выше;
- AC3 (
:283) — «профильpost-writeне вызывает_adoptInitialSpace/_resumePendingNavMode/_restoreZoom» как часть доказательства поведенческой нейтральности; - §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.ts6,houseplan-editor-runtime.ts6,plan-optimize-write.ts1,serialized-write-queue.ts1,space-copy-runtime.ts1,summary-panel-runtime-loaded.ts1,vacuum-calibration-write.ts1,editors/vacuum-maps-section.ts1 — сумма 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.ts4,houseplan-onboarding-runtime.ts1,houseplan-editor-runtime.ts1,plan-optimize-write.ts1, после исключения объявления поля) — совпадает с §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-budget13700 (§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— ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет. - Дерево материала:
9a354a9fa366fcca195773d22a15f7947caf5d14git log --all --format='%H %T' | grep 9a354a9fa366 - ТЗ
docs/specs/500-config-adoption-boundary.md, блоб5cfc36e2614361d1e90d4d9a8a9a7358d4bec3b9git log --all --find-object=5cfc36e2614361d1e90d4d9a8a9a7358d4bec3b9 -- docs/specs/500-config-adoption-boundary.md - Вердикт конвейера:
yellow· High 1