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

16 KiB
Raw Blame History

SPEC-REVIEW-361-r1

Issue: #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 → в задаче