17 KiB
SPEC-REVIEW-669-r1
- Issue: https://github.com/Matysh/houseplan-card/issues/669
- Этап:
S4-spec-review(ревью ТЗ, PROCESS.md §2.4) - Трек: полный (метки
bug,P1,S4-spec-review; ниsmall, ниtrivialне выставлены — документ, а не только комментарий). - Материал: тело issue #669, раздел
## ТЗ, + 3 комментария: (1) «Взял» автора с планом A/B-замера, (2) аналитика — таблица замеров, причина регрессии, оценка, решение «вопросов владельцу нет», ссылка на новый #675, (3) поправка к одной строке таблицы замеров. - Заход: r1 · блокирующих циклов израсходовано 0 из 4
- Роль: ревьюер ТЗ (не автор)
Скоуп ревью
Регрессия производительности в cleanFloorForRoom (src/clean-floor.ts:47):
каждый промах кэша чистого пола сразу вычитает все лестницы пространства
(geometryAreaMinusStairs → geometryMinusStairs, src/stairs.ts:234-250) —
хотя площадь читают только два потребителя (подсказка комнаты, PDF), а путь
пола (нужный пяти... по факту четырём местам рендера) вычитания не требует.
Задача переносит вычисление площади на момент первого чтения (лениво,
мемоизировано в том же кэше) и добавляет отбор лестниц по пересечению bbox
перед polyclip. Это разблокирует v1.78.0-beta.5, у которой красный
performance-smoke на точном SHA 09873251.
SCOPE-проверка (docs/SCOPE.md): чисто внутренняя оптимизация без нового
видимого поведения — под J1 («показать дом сейчас», включая скорость первого
кадра) как выполнение уже принятого бюджета, а не новый job. Ничего из
«никогда не строить» не задевается.
Как проверялось
- Прочитаны
docs/SCOPE.md,docs/process/REVIEWER.mdцеликом; по ссылкам конспекта открыт PROCESS.md §2.3, §2.4, §2.5, §4, §7.1, §7.2 (§2.10 не применялся — это r1). - Прочитано тело issue #669 целиком и все 3 комментария (
gh issue view 669 --json body,comments). - Сверены обязательные разделы §7.1: Сценарий, Что человек увидит до/после, Проблема, Скоуп/Не-скоуп, Контракт поведения, UX/модель данных и миграция/i18n/touch/производительность, Критерии приёмки AC1–AC4 со способом доказательства, План автотестов, Риски, Откат, Release-артефакты, Затронутые файлы, «Принято предположительно» — все на месте, первые два раздела продуктовые и без терминов реализации.
- Прочитан текущий код
src/clean-floor.tsиsrc/stairs.tsцеликом, чтобы проверить, что описание проблемы соответствует коду, а предложенный скоуп реализуем без переписывания контракта:- подтверждено:
path/geomвcleanFloorForRoomвычитают только физические тела (floorMinusBodies), лестницы в путь никогда не попадали — вычитание лестниц целиком живёт в полеarea. Значит лениво откладывать нужно именно и только его; контракт «путь не меняется» (AC1, «Контракт поведения») уже выполняется today и задача его не трогает — только переносит момент вычисленияarea. - подтверждено количество мест, читающих только
.path:grep -n "_cleanFloor("даёт 4 попадания (src/houseplan-card.ts:9059, 9229, 10459, 11011), а не 5, как сказано в разделе «Проблема». Пятое место (:11873) читает весь объект ради.area(подсказка комнаты). Второй потребитель площади —resolveRoomArea(:10184) → та же._cleanFloor(...).area. Отдельно вsrc/pdf/pdf-scene.ts:311есть independent fallback-вызовgeometryAreaMinusStairsв обходcleanFloorForRoom(когдаresolveRoomAreaне вернул число) — он не проходит через ленивый кэш и в ТЗ не упомянут, но и не входит в «Затронутые файлы»; поскольку это редкий fallback-путь, а не типовой рендер кадра, на бюджет AC3 он не давит — оставляю без находки, только для полноты цепочки. - подтверждено, что
geometryMinusStairsSteps(сводная панель) реализован отдельно отgeometryMinusStairsи не переиспользует его — фильтр по bbox внутриgeometryMinusStairs(место, названное в «Принято предположительно») не заденет сводную панель автоматически, как и требует «Не-скоуп». - сверены оба файла бюджетов
demo/performance/budgets-*-smoke.json:spaceSwitchMs— обязательная (hardMaxMs) метрика обоих профилей, а не информационная; лимиты (modelReadyMs3000/2500,firstStableRenderMs3500/3000,spaceSwitchMs1800/1500) совпадают с числами из таблицы наблюдений в issue.
- подтверждено:
- Сверены данные аналитики (таблица A/B) с самой формулировкой AC3 — см. находку M1 ниже.
- Проверена логика ролбэка/release-артефактов на согласованность с политикой
docs/STATUS.md(STABLE-тело исключает баги, которые «introduced and fixed strictly inside the beta line»):v1.78.0ещё не имеет стабильного релиза (git tag -l 'v1.78.0*'— только беты 1–4), значит «changelog — нет» обоснованно, регрессия #663 нигде не выходила стабильным релизом.
Находки
Medium (в скоупе) — AC3 не гарантирует заявленную цель issue
Что не так. «Ожидаемое поведение» (преамбула issue) и AC3 обещают: «После
исправления полный Validate точного release-candidate SHA проходит с первого
воспроизводимого запуска» / «Полный Validate кандидата слияния и кандидата
беты: performance_smoke обоих профилей … проходит modelReadyMs и
firstStableRenderMs с первого прогона при неизменных бюджетах». Формулировка
называет только эти две метрики, но performance_smoke — единый job с
несколькими обязательными (hardMaxMs) метриками в том же прогоне, включая
spaceSwitchMs. По собственным замерам аналитики в этом же issue
spaceSwitchMs уже проваливается на изометрическом профиле точного SHA
09873251 (1821.2 мс и 1813.5 мс против лимита 1800 мс — два из трёх
зафиксированных провальных прогонов), и признаётся «не относящейся к
лестницам» — не-скоуп, вынесено в отдельный #675. Ни ТЗ, ни преамбула не
говорят, что делать с этим фактом: если #675 не сольётся раньше или вместе с
кандидатом beta.5, тот же самый exact-SHA performance_smoke может остаться
красным на spaceSwitchMs даже после идеального выполнения AC1–AC4 этой
задачи — то есть заявленная цель issue («кандидат стабильно проходит
действующие performance-бюджеты… полный Validate… проходит с первого
воспроизводимого запуска») не гарантируется тем, что реально проверяет
AC3.
Почему это находка, а не придирка к тексту. Разработчик и последующий код-ревьюер будут закрывать именно AC3 как написано — двумя названными метриками. Формально «AC3 выполнен» при этом возможен ровно в момент, когда release по-прежнему заблокирован тем же job'ом на другой метрике; никто не обязан заметить разрыв между «AC выполнен» и «issue решает свою же преамбулу», а этот разрыв прямо запрещён REVIEWER.md («жёлтый вердикт допустим и при выполненных AC, если изменение не решает заявленный сценарий»).
Чем закрывается в скоупе задачи (без нового инженерного объёма).
Переформулировать AC3 и/или преамбулу «Ожидаемое поведение», выбрав одно из:
(a) явно ограничить обещание двумя названными метриками и добавить в
«Не-скоуп»/«Влияние» прямую фразу «для полностью зелёного performance_smoke
кандидата требуется также #675»; либо (b) если у автора/владельца есть
основания считать spaceSwitchMs эпизодически проходящим независимо от
данной задачи (шум, а не устойчивый провал) — явно это утверждать со ссылкой
на замер, а не оставлять предположение читателю. Технический характер этого
уточнения (не продуктовый вопрос) означает, что owner не привлекается —
решение принимает автор ТЗ, я его снимаю как открытый вопрос и фиксирую сюда.
Low — фактическая неточность в «Проблема» (не блокирует)
Раздел «Проблема» утверждает: «пять мест рендера запрашивают только путь
пола». Чтением кода (grep -n "_cleanFloor(" src/houseplan-card.ts)
подтверждаю четыре таких места (:9059, :9229, :10459, :11011); пятый
вызов (:11873) читает .area, а не только .path, и относится к другой
категории потребителей (подсказка комнаты), уже перечисленной отдельно.
Утверждение не входит ни в один AC и не влияет на реализуемость — рекомендую
исправить число при следующей правке ТЗ, не считаю это блокером и снимаю сам.
Что проверено и корректно
- Обязательные разделы §7.1 присутствуют, в правильном порядке; первые два — продуктовые, без терминов реализации.
- AC1 и AC2 однозначны, имеют явную пару «доказательство / чем краснеет»,
тестовый план реализуем без изменения контракта (
geom/pathуже не зависят от лестниц; ленивость нужна только дляarea). - AC4 и контракт поведения («в пределах относительной погрешности 1e-9») корректно фиксируют неизменность видимого числа площади.
- «Не-скоуп» корректно ограждает
geometryMinusStairsSteps(независимая реализация — трогатьgeometryMinusStairsеё не заденет) и 2.5D-рендер лестниц, с явным условием возврата в ТЗ, если остаточных издержек окажется достаточно, чтобы провалить AC3 на изометрии — хорошая гигиена против молчаливого расширения скоупа. - UX/модель данных/i18n/touch — «нет изменений», согласовано с кодом (никаких новых полей конфигурации, никакого нового текста).
- Release-артефакты («changelog — нет») обоснованы политикой
docs/STATUS.mdо STABLE-теле:v1.78.0ещё ни разу не выходил стабильным релизом (только beta.1–4), регрессия #663 нигде не публиковалась стабильно. - Риск «устаревшая площадь при копировании объекта» реалистично описан и
сейчас не имеет ни одного нарушителя (
grepна spread/Object.assignрезультата_cleanFloor/cleanFloorForRoom— пусто), т.е. риск корректно назван, а не выдуман. - «Принято предположительно» корректно помечает две технические развилки
(геттер vs
areaOf, место фильтра bbox) как свободно изменяемые — не требует ответа владельца, соответствует §7.1 (не продуктовый вопрос). - Открытых продуктовых вопросов к владельцу нет — согласен с автором: видимое поведение не меняется, различие в AC3 (находка Medium выше) — техническая формулировка, а не продуктовый выбор.
Чего не проверял
- Не запускал
npm test/typecheck/build— на этапе spec кода ещё нет, реализация не начата (веткаissue/669-*не создана). - Не запускал perf-смок и не проверял поведение реального
polyclip-tsна 250 лестницах — AC3 по определению доказывается на кандидате в код-ревью, не на этапе ТЗ. - Не проверял
#675по существу (отдельная задача); использовал только факт её существования и формулировку не-скоупа для находки Medium выше.
Вердикт
Вердикт: жёлтый · заход r1 · блокирующих циклов 0/4 · High: 0 · Medium: 1 → в задаче
Материал раунда
- Ветка:
dev, коммитc9cd6cbf1f5b— ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет. - Дерево материала:
2c7a4c124868cf7499d7b5c725ee028d3810eb62git log --all --format='%H %T' | grep 2c7a4c124868 - Тело issue:
1500ed85bc2e116a29201cc0f77d438e9e6381195fffc70053acc9f250e1dff6 - Вердикт конвейера:
yellow· High 0