Files
houseplan-card/docs/reviews/SPEC-REVIEW-361-r1.md
T
2026-08-29 07:55:23 +00:00

187 lines
16 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-361-r1
Issue: [#361](https://github.com/Matysh/houseplan-card/issues/361) — «Мебель: физическая
толщина линий не масштабируется при zoom»
ТЗ: `docs/specs/361-furniture-stroke-zoom.md`, commit `ccaf34d6` (`docs: specify furniture
stroke zoom`, Issue: #361, User-Visible: no)
Этап: spec (§2.4) · трек: полный (аналитика #361 явно назвала критерий `small`,
который задача не проходит: сложность/риск 4–5, геометрический render-контракт) ·
заход r1 · блокирующих циклов израсходовано 0 из 4 до этого раунда
## Скоуп проверки
Ревью первого захода на полном треке — разбор полный (§2.10 к этому раунду не
применяется, второго раунда ещё не было). Проверялись:
1. соответствие обязательным разделам ТЗ (PROCESS.md §7.1);
2. однозначность и доказуемость каждого AC1…AC9;
3. отсутствие догадки, выданной за факт, — каждое техническое утверждение
сверено с текущим кодом или каноническим документом;
4. соответствие `docs/SCOPE.md` (какую строку core user jobs закрывает) и
отсутствие расширения скоупа за пределы аналитики;
5. терминология интерфейса против `docs/USER-GUIDE.ru.md`;
6. согласованность плана автотестов с `AGENTS.md`/`PROCESS.md` о порядке
запуска гейтов.
## Как проверялось
Файловое ревью, без исполнения кода (артефакта ещё нет — стадия spec).
Прочитаны и сверены с текстом ТЗ:
- `docs/SCOPE.md`, `AGENTS.md`, `PROCESS.md` — рамка процесса и продукта;
- тело issue #361 и три комментария (аналитика, занятие, хендофф автора ТЗ);
- `src/houseplan-card.ts` (строки ~8049–8153, 8074–8076, 8132–8153) — оба
render-пути мебели (`_renderFurniturePlacementPreview`, `_renderDecorLayer`
ветка `kind === 'furniture'`) и соседние line/rect/ellipse для контраста;
- `src/houseplan-card.ts` (строки ~11060–11073) — корневой `<svg class="plan-svg">`
с `viewBox` от `_applyView(zoom, …)` и `preserveAspectRatio="xMidYMid meet"` —
подтверждает механизм "camera zoom" через `viewBox`, на который и опирается
технический контракт ТЗ;
- `src/houseplan-editor-runtime.ts` (строки ~10930–10981) — проверено, что
другие использования `non-scaling-stroke` (room-draft/partition/column hit-areas)
не относятся к мебели и не задеты скоупом;
- `src/styles/plan.styles.ts` (строка 704, 713) — `.derasehit` стилизуется
отдельным CSS-классом с собственной шириной, подтверждает заявление ТЗ, что
erase-hit не станет физической линией;
- `docs/FURNITURE.md` — подтверждает нынешнее (ошибочное) канон-утверждение
«the user's decor colour, opacity and physical line width remain authoritative»
вместе с `non-scaling-stroke», которое AC9 обязуется исправить;
- `docs/USER-GUIDE.ru.md` (строки 61, 169, 174, 1247, 1258) — термины
«Редактор подложки», «мебель», «толщина… хранится физически» совпадают с ТЗ;
- `docs/CANVAS.md` (§ Render frame vs. view, §3) — подтверждает существование
величины "screen pixels-per-unit" в существующей модели камеры, на которую
ТЗ явно не претендует (оставлено «принято предположительно»);
- наличие `demo/smoke_furniture.mjs`, `test/furniture.test.mjs` и прецедентов
raster-измерения (`demo/smoke_grid_scale_invariance.mjs`,
`demo/smoke_device_icon_pixel_alignment.mjs`) — подтверждает техническую
реализуемость заявленного растрового доказательства.
Код ещё не написан, поэтому гейты (`typecheck`/`test`/`build`/смоки) на этом
этапе не запускались — они не относятся к предмету ревью ТЗ.
## Находки
### Medium — план автотестов противоречит порядку гейтов из `AGENTS.md`
**Файл:** `docs/specs/361-furniture-stroke-zoom.md`, раздел «План автотестов», пункт 7.
**Формулировка ТЗ:**
> В цикле реализации запускать только `npm run typecheck`, `npm test`,
> `npm run build` по принятому процессу. Browser smoke, golden и полный gate
> запускаются в предрелизном прогоне; тестовые сценарии и assertions входят в
> продуктовый коммит заранее.
**Почему это находка.** `AGENTS.md` (раздел «Гейты», решение владельца от
2026-08-14, issue #151) прямо требует другого: *«before moving an issue to
`S7-code-review`, run the smokes named in its AC locally — `node
demo/smoke_<name>.mjs`. A red smoke that reaches the review costs a cycle; run
locally it costs a minute»*. Для #361 это не абстрактное правило: AC1, AC3,
AC4 и AC6 сами называют `demo/smoke_furniture.mjs` способом доказательства. То
есть именно этот смок обязателен к локальному прогону **до** перевода issue в
`S7-code-review`, а не отложен до предрелизного прогона.
Пункт 7 в его нынешней редакции — это почти дословный пересказ
`PROCESS.md §11.4`, но эта статья описывает другое: набор гейтов, которые
физически невозможно прогнать раньше (полный HA-харнесс, `golden`,
`performance_smoke`, полный набор `demo/smoke_*.mjs`) — и относится к окну
**после** `S8-merged`. Она не отменяет более узкое и более новое правило
`AGENTS.md` про смок, названный в AC самой задачи.
**Сценарий отказа.** Автор реализации следует тексту ТЗ буквально, не
запускает `demo/smoke_furniture.mjs` локально, переводит issue в
`S7-code-review` с падающим (или просто непроверенным) смоком — ровно тот
случай, который `AGENTS.md` называет стоящим целого цикла ревью, при том что
локальный прогон стоит минуту. Prewiew/commit parity (AC4) и anisotropic
resize (AC2/AC3) — это как раз те инварианты, которые легко сломать при первой
реализации и почти невозможно заметить без растрового смока.
**Как закрыть.** Строка правится на месте — пункт 7 должен предписывать
локальный прогон `node demo/smoke_furniture.mjs` перед переводом issue в
`S7-code-review` (как того требует `AGENTS.md`), оставляя `golden`, полный
набор `demo/smoke_*.mjs` и `performance_smoke` предрелизному прогону. High
здесь нет: это правится редактированием одного абзаца ТЗ, не меняет ни один
AC, ни контракт поведения, ни скоуп.
## Что проверено и корректно
- **Обязательные разделы §7.1** — все на месте: сценарий, «что человек увидит
до/после», проблема, скоуп/не-скоуп, контракт поведения, UX, модель данных и
миграция, i18n, AC1…AC9 с доказательством, план автотестов, риски, откат,
release-артефакты, блок «принято предположительно». Двусторонняя ссылка
issue ↔ ТЗ на месте.
- **Диагноз дефекта грамотно обоснован кодом, не догадкой.** Проверено
построчно: `line/rect/ellipse` (houseplan-card.ts:8093–8130) получают
`stroke-width` без `vector-effect` и поэтому масштабируются вместе с
`viewBox`; сохранённая мебель (8144–8148) и preview (8071–8076) получают тот
же физический `stroke-width`, но дополнительно `vector-effect="non-scaling-stroke"`,
который по спецификации SVG игнорирует весь CTM выше элемента — включая и
локальный `scale(W2/art.viewW H2/art.viewH)` (нужный), и внешний
`viewBox`-масштаб камеры (нежелательный побочный эффект). Корневой
`plan-svg` действительно меняет `viewBox` от `_applyView(zoom, …)`
(houseplan-card.ts:11069–11073) — то есть камера здесь именно
viewBox-driven, а не внешний CSS-transform, и технический контракт ТЗ
(«внешний plan CTM/viewBox» как источник camera zoom) сформулирован верно, а
не предположительно.
- **Anisotropic-инвариант не выдуман.** Причина, по которой простое удаление
`vector-effect` создало бы новый дефект, прослеживается по тому же коду:
только у мебели есть локальный `scale(x, y)` от `art.viewW/viewH` к боксу
`w/h`, у line/rect/ellipse такого transform нет — значит анизотропия
штриха возможна только у мебели, и ТЗ верно ограничивает риск именно этим
render-путём.
- **Erase-hit и миниатюры палитры корректно выведены из скоупа.**
`.derasehit` в `plan.styles.ts` стилизуется отдельным CSS-классом с
собственной шириной, не зависящей от `width_cm` — заявление «текущая
интерактивная ширина не должна сузиться» проверяемо и уже верно сегодня, фикс
его не касается по построению.
- **Терминология.** «Редактор подложки», «мебель», «толщина… физически» — из
`docs/USER-GUIDE.ru.md`, не изобретены.
- **Изометрия выведена корректно.** Публичной калибровки iso-режима в скоупе
нет (Labs-эксперимент, презентационный слой), при этом требование «не терять
мебель и не падать» остаётся — разумная и проверяемая граница, не
продуктовый вопрос, требующий владельца.
- **Технически реализуемо.** В демо-каталоге уже есть прецеденты растрового
измерения (`smoke_grid_scale_invariance.mjs`,
`smoke_device_icon_pixel_alignment.mjs`) и оба целевых файла
(`demo/smoke_furniture.mjs`, `test/furniture.test.mjs`) существуют и готовы
к расширению — план не полагается на несуществующую инфраструктуру.
- **Продуктовых вопросов владельцу нет**, и это верно: единственная реальная
двусмысленность (нужен ли отдельный golden-сценарий) — техническая, явно
оставлена «принято предположительно», разрешается по факту достаточности
растрового доказательства на код-ревью, а не эскалацией.
- **AC проверяемы и не выданы за уже решённые.** Ни один AC не описывает
поведение, которого нет ни в одном документе, без пометки допущения; числовые
допуски (`max(1 CSS px, 10%)`) заданы явно там, где они важны для теста, и
оставлены свободными там, где это чисто техническая деталь (реализация
pixel measurement, точные тестовые symbols).
## Чего не проверял
- Не оценивалась реализуемость конкретной формулы `min(viewportWidth /
viewBoxWidth, viewportHeight / viewBoxHeight)` в деталях (например, как
измеряется `viewportWidth` для конкретного `<svg>` в браузере/Playwright) —
это заявлено в ТЗ как «принято предположительно, поменять свободно» и не
является продуктовым контрактом; будет предметом код-ревью.
- Не запускались `npm run typecheck`/`test`/`build` — кода нет, гейты к этому
этапу не относятся.
- Не проверялась совместимость с `docs/CONFIG-COMPATIBILITY.md` построчно —
ТЗ явно заявляет «schema/config version и backend validation не меняются»,
и это подтверждено отсутствием изменений в модели данных (persisted schema
в разделе ТЗ идентична текущей в `FURNITURE.md`); отдельного анализа
миграции не требуется, потому что миграции нет.
- Не проверялась текущая numeric-точность существующих raster-тестов
decor-line (на который ссылается AC1 «тот же raster tolerance») — она не
названа в ТЗ явным числом, и это оставлено на усмотрение автора при
реализации теста, а не продуктовое решение.
## Вердикт
High: 0. Medium: 1, в скоупе задачи (см. выше) — не заводится отдельным issue,
правится в тексте `docs/specs/361-furniture-stroke-zoom.md` в рамках этого же
раунда. Технический диагноз проверен по коду и корректен, контракт исполним,
AC доказуемы. Единственная находка — процедурная (порядок запуска гейта
`demo/smoke_furniture.mjs`), не продуктовая и не архитектурная; правки в
поведение, AC или скоуп не требует.
**Вердикт: жёлтый · заход r1 · блокирующих циклов 1/4 · High: 0 · Medium: 1 →
в задаче**