18 KiB
SPEC-REVIEW-676-r2
- Issue: https://github.com/Matysh/houseplan-card/issues/676
- Этап:
S4-spec-review(ревью ТЗ, PROCESS.md §2.4) - Трек: полный (метки
bug,P1,S4-spec-review; лёгкий трек — нет, подтверждено аналитиком в r1: несколько поверхностей, новый UX-контракт размещения протяжкой и подсказки, сложность > 3). - Материал: тело issue #676, раздел
## ТЗ(редакция 2, 27.09) + 6 комментариев (без изменений добавился только комментарий «редакция 2» и вердикт r1). - Заход: r2 · блокирующих циклов израсходовано 1 из 4 (эта находка сделает 2 из 4)
- Роль: ревьюер ТЗ (не автор)
Скоуп ревью (по дельте, PROCESS.md §2.10)
Предмет r2 — ровно три точечных правки редакции 2, объявленные автором в
комментарии IC_kwDOTOcLQM8AAAABXQNDXw и в шапке ТЗ («по ревью r1: AC8
покрывает все три ветви подавления, AC12 — инертность рамки, tie-break
диагонали в К1»):
- AC8 расширен до перечисления всех ветвей подавления подсказки Просмотра (К8).
- Новый AC12 — инертность рамки узлов лестницы под инструментами, отличными от «Выбор»/«Лестница».
- К1 получил детерминированный tie-break для протяжки ровно по диагонали 45°.
Код задачи не существует (ветка issue/676-* не создана; см. «Как
проверялось», п. 1) — ТЗ по-прежнему живёт только в теле issue, docs/specs/
не создаётся (issue открыт после 2026-09-10). Остальные разделы ТЗ (Сценарий,
Контракт К2–К7, i18n, модель данных, риски, откат, release-артефакты)
дельтой r1→r2 не задеты — они наследуются из r1 без повторной проверки (см.
раздел «Унаследовано из r1» ниже), кроме одного пересечения: расширенная
верификация К8/AC8 в этом раунде вскрыла состояние, не покрытое ни в r1, ни
в правке r2 (см. «Находки»).
SCOPE-проверка (docs/SCOPE.md): не меняется относительно r1 — сценарий
закрывает J6 (редактирование объектов плана без регресса) и J1/J2 через
корректную навигацию/подсказку лестницы в Просмотре.
Как проверялось
- Подтверждено, что кода задачи всё ещё нет:
git branch -a | grep -i 676не находит рабочую ветку (толькоd03a68b8 feat: добавить лестницы между этажами (#663)— это #663, не #676);git diff f1a87fba..HEAD --statмежду материалом r1 и текущимHEAD(9fe7f88c) показывает только два файла —docs/reviews/INDEX.mdи публикациюSPEC-REVIEW-676-r1.md. Дифф кода для этой задачи не существует; гейтыtypecheck/npm test/npm run build/check-docs.mjsнеприменимы на этом этапе — см. «Чего не проверял». - Прочитано тело issue #676 целиком (
gh issue view 676 --json body, sha2560b58e92a…0bdd) и все 6 комментариев (gh issue view 676 --json comments), включая новый (редакция 2) и вердикт r1. - Сверены ровно три правки редакции 2 с текстом находок r1 построчно:
- Medium-1 (AC8, r1): требовал отдельного смоук-шага на каждую из
веток
missing/deleted/fixed. Новый AC8 перечисляет четыре шага с отдельными флагами:garden(active) → тултип и снятие;target_space_id: null(missing) →null;'no-such-space'(deleted) →null; карточка сfloor: 'f1'(fixed) →null. Текстуально закрывает ровно то, что просил r1. - Medium-2 (инертность рамки, r1): рекомендовал новый AC12 со
смоук-шагом «под инструментом не-«Выбор»/не-«Лестница» клик по точке
узла воздействует на инструмент, а не перехватывается рамкой».
Новый AC12 в точности содержит эту проверку (
.hp-stair-frameотсутствует в DOM,_path.lengthрастёт под «Стенами»). К2 также получил явное «рамка не рендерится вовсе» — согласовано с AC12. - Low (tie-break, r1): К1 получил «при равных |dx| и |dy| доминирует
x», с прямой ссылкой на прецедент
resizeFurnitureTransform— снято.
- Medium-1 (AC8, r1): требовал отдельного смоук-шага на каждую из
веток
- Проверено, что AC12 ссылается на реально существующий, а не
выдуманный контракт:
src/houseplan-editor-runtime.ts:2213-2229(_markupClick) подтверждает, что клик под инструментомdraw(«Стены») продвигаетthis.host._path(_path.lengthрастёт при добавлении точки, см._canAppendWallPoint,:2200-2210) — заявление AC12 о «клик ставит точку стены» не расходится с существующим кодом инструмента. - Так как AC8 — именно та зона, которую r1 уже признал недостаточно
покрытой веткой, для r2 она перепроверена не только текстуально, а по
исходной функции, которую К8/AC8 описывают:
stairTargetState()(src/stairs-editor-model.ts:97-104) возвращает пять состояний —active | missing | self | deleted | fixed, — не четыре.self(target_space_id === currentSpaceId) — не выдуманное состояние: оно документировано в канонеdocs/STAIRS.md:21-22(«A missing, self or deleted target leaves the stair visible and editable but shows a repair warning...») в одной группе сmissing/deleted, и данные, порождающие его, не отвергаются моделью (src/stairs.ts:84проверяет только тип строкиtarget_space_id, не запрещает равенство текущему пространству). Диалог свойств лишь исключает текущее пространство из выпадающего списка на вкладке, где лестница редактируется сейчас (src/stairs-editor.ts:505,.filter((item) => item.id !== this.owner._space)) — это не мешаетselfвозникнуть после переноса/слияния помещений или ручной правки конфигурации, поэтому уже существующий кодstairs-view.ts:64-73отдельно проверяет ровно пять веток через ту же функцию и намеренно не считаетselfнавигируемым (active = interactive && targetState === 'active'). К8/AC8 этой задачи вводят новый вызов той же функции для тултипа, но описывают только четыре из пяти её исходов; вывод — в «Находки». - Разделы, не тронутые дельтой (К2–К7 кроме упомянутого в п. 3 уточнения, i18n, модель данных, откат, release-артефакты, план автотестов кроме строки AC12), не перечитывались заново построчно — они приняты из r1 без повторной проверки, см. «Унаследовано из r1».
Находки
Medium (в скоупе) — AC8/К8 перечисляют четыре ветви stairTargetState, а не пять; состояние self выпадает из контракта тултипа
Что не так. К8: «Лестница без цели, с битой целью или в карточке с
закреплённым этажом подсказки не показывает» — это missing, deleted,
fixed. AC8 после правки r2 прямо называет себя «все четыре ветви» и
перечисляет active/missing/deleted/fixed. Но функция, которую К8
описывает, stairTargetState() (src/stairs-editor-model.ts:97-104),
возвращает пять значений: active | missing | self | deleted | fixed.
Ветвь self — лестница с валидным, непустым target_space_id, который
совпадает с ID текущего пространства, — не упомянута ни в К8, ни в AC8, ни
в плане смоук-шагов.
Почему это находка, а не придирка. self — не гипотетическое, а
документированное состояние: docs/STAIRS.md:21-22 группирует его вместе
с missing/deleted («shows a repair warning... makes View activation a
no-op»), и модель (src/stairs.ts:84) не отвергает данные, где
target_space_id равен текущему пространству — такое может возникнуть при
переносе/слиянии помещений (J6, не защищено на этом уровне валидацией) или
ручной правке YAML. Существующий, не-скоуповый код Просмотра
(stairs-view.ts:64-73) уже трактует self как ненавигируемое наравне с
missing/deleted/fixed через ту же функцию. Реализация тултипа,
написанная буквально по К8 («не показывать при missing, deleted или
fixed»), — то есть перечислением трёх суппрессирующих веток, а не
инверсией targetState !== 'active', — пропустит self через «иначе» и
покажет пользователю подсказку «Переход на этаж {название текущего же
этажа}» на лестнице, которая никуда не ведёт (клик по ней не
сработает, потому что навигация уже верно проверяет === 'active'). Это
ровно тот класс дефекта, который r1 уже указал для этой же строки ТЗ:
неполная ветка гварда, которая пройдёт AC1–AC12 буквально и оставит
конкретное поведение и непроверенным, и вероятно неверным. Формулировка
«все четыре ветви» (в отличие от r1, где пропуск был очевиден по счёту)
создаёт ложное чувство полноты — именно поэтому дельта r1→r2 не отловила
её сама.
Чем закрывается в скоупе задачи. Один из двух путей, оба технические
(не продуктовый вопрос владельцу): (а) переформулировать К8/AC8 как
инверсию — «подсказка показывается только при targetState === 'active',
во всех остальных случаях, включая уже перечисленные и self, не
показывается» и добавить пятый смоук-шаг (лестница с
target_space_id, равным ID текущего пространства, — _tip остаётся
null); либо (б) явно решить, что self в контексте тултипа — не
предусмотренная владельцем ситуация и обосновать, почему её можно
не тестировать (например, если правка К8 формулируется как «показывать
только при active», для чего пятая ветка тривиально следует из первых
четырёх и не требует отдельного теста, а лишь явного упоминания в тексте
контракта). Любой вариант не меняет скоуп и не требует эскалации
владельцу — решение техническое, не продуктовое.
Что проверено и корректно
- Обе Medium-находки r1 закрыты именно так, как рекомендовал r1: AC8 —
четырьмя отдельными флагами вместо одного; AC12 — новым смоук-шагом,
подтверждённым существующим кодом инструмента «Стены»
(
_markupClick/_canAppendWallPoint). - Low-находка r1 (tie-break диагонали) закрыта явным правилом «доминирует x» со ссылкой на прецедент.
- AC12 корректно вставлен в «План автотестов» (
demo/smoke_stairs.mjs: «расширить … шагами … AC12»), а не оставлен только в таблице АС без соответствующей строки плана. - К2 согласован с AC12 текстуально: оба говорят «рамка не рендерится вовсе» под другими инструментами, а не «рендерится, но игнорирует события» — единственное число, один источник истины для этого поведения.
- Правки редакции 2 не меняют ничего вне заявленных трёх пунктов: секции Сценарий, К1 (кроме tie-break), К2 (кроме одной фразы), К3–К7, модель данных, i18n, риски, откат и release-артефакты текстуально идентичны тому, что уже проверено в r1 (сверено построчным чтением текущего тела issue против содержания, процитированного в SPEC-REVIEW-676-r1.md).
Чего не проверял
- Не запускал
npm run typecheck/npm test/npm run build,node scripts/check-docs.mjs, смоки,golden:verify, инварианты модели — как и в r1, кода задачи не существует (git branch -aне находит веткуissue/676-*;git diff f1a87fba..HEADсодержит только публикацию документа r1). Дифф для этих гейтов не существует, они относятся к этапу код-ревью. - Не проверял заново по коду весь фактический фундамент контракта
(константы, файлы, существующие функции, 4 golden-сцены, i18n-паритет,
math сектора курсора К2) — это сделано в r1 и не задето дельтой r2, кроме
точки пересечения с
stairTargetState, которую я перепроверил заново специально из-за характера правки AC8 (см. «Как проверялось», п. 5). - Не проверял, действительно ли
selfпрактически достижим через существующий UI (перенос/слияние помещений, J6) в разбивке по шагам — для находки достаточно, что модель (stairs.ts) не отвергает такие данные и канон (STAIRS.md) явно документирует состояние как поддерживаемое; глубже, чем чтение этих двух источников, не уходил. - Не проверял #663/#669 по существу — не в скоупе этой задачи, как и в r1.
Вердикт
Вердикт: жёлтый · заход r2 · блокирующих циклов 2/4 · High: 0 · Medium: 1 → в задаче
Материал раунда
- Ветка:
dev, коммит9fe7f88cb3ec— ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет. - Дерево материала:
85bef1cf4119abfcaf8c9ff7275010213306fb9agit log --all --format='%H %T' | grep 85bef1cf4119 - Тело issue:
e28517c301f2de5676ccf6e30f0997db7f3a5a36d10f7a1d66070909bb1b1d9d - Вердикт конвейера:
yellow· High 0