19 KiB
SPEC-REVIEW-53-r2 — Экспорт пространства в PDF: чистый архитектурный план
- Issue: https://github.com/Matysh/houseplan-card/issues/53
- Этап: ТЗ на ревью (PROCESS.md §2.4), полный трек
- Заход: r2 · блокирующих циклов израсходовано 1 из 4 (зелёный вердикт этого раунда бюджет не тратит, #227)
- Материал:
docs/specs/053-pdf-export.mdиdocs/SCOPE.mdна коммите299629d2d27231267c47afc583801fdcf553d687(HEAD,origin/issue/53-pdf-export) - Материал предыдущего раунда (r1):
docs/specs/053-pdf-export.mdна коммитеcc93e93da9277a5e20c677e35ea1a0be0af57791— так явно названо в шапкеdocs/reviews/SPEC-REVIEW-53-r1.md. Подтверждено таймстампами:cc93e93dсоздан 2026-09-07 02:09:38+03, документ ревьюee72e393— 2026-09-07 02:21:30+03, то есть послеcc93e93dи до299629d2(02:28:56+03).
Замечание к машинному блоку «Материал раунда» в документе r1. Автогенерируемый
конвейером якорь в конце SPEC-REVIEW-53-r1.md называет коммит 48a17f5dc998
(дерево be8a4d9c9e36…), а не cc93e93d, на котором реально проводился разбор
согласно шапке того же документа. 48a17f5d — предыдущая ревизия ТЗ (без
уточнения владельца по внутренним/внешним размерам из cc93e93d), не
осиротевшая и не удалённая — SHA живой, поэтому это не случай «SHA не
резолвится» из §2.10. Расхождение между шапкой документа и его же машинным
якорем не блокирует этот раунд: шапка недвусмысленно называет материал, и
таймстампы подтверждают, что cc93e93d предшествовал самому ревью, а 48a17f5d
— нет (правки по внутренним/внешним размерам в 48a17f5d ещё не было, но
именно они разбирались в Medium-2/3 r1). Использовал cc93e93d как базу дельты.
Это наблюдение о механике конвейера, не находка по ТЗ — фиксирую на случай,
если генератор якоря путает коммит issue-тела с фактическим коммитом ревью.
Скоуп ревью
Второй заход, разбор по дельте (PROCESS.md §2.10, issue #214): предмет —
git diff cc93e93d..299629d2 (два файла: docs/SCOPE.md,
docs/specs/053-pdf-export.md). Дельта локальна — это не ребейз на ушедший
dev, контракт поведения не сменился, новая подсистема не задета, объём
дельты (2 файла, точечные абзацы) существенно меньше исходного ТЗ. Полный
разбор не требуется; проверяю дельту плюс всё, до чего она дотягивается:
AC5, AC6, §7.3 целиком, §11, §15, §16, §9 (i18n), §13 (план тестов) —
поскольку правки задевают ровно эти разделы.
Закрытие раунда r1
| Находка r1 | Чем закрыта | Где это видно |
|---|---|---|
Medium-1 — docs/SCOPE.md не содержит заявленного исключения для #53 |
Добавлен абзац в docs/SCOPE.md, раздел Out of scope, по образцу записи для #89: ссылка на #53, обе даты решений (2026-08-15, 2026-09-07), явное описание узкого read-only исключения. §11 ТЗ переписан с «уже записано, проверить формулировку» на «узкое read-only исключение #53, принятое 2026-08-15 и суженное 2026-09-07» |
docs/SCOPE.md:107-114 (коммит 299629d2, строки перед «A general dashboard framework»); docs/specs/053-pdf-export.md:316-317 |
| Medium-2 — коллизии противоречили гарантии «нельзя опускать» для непрямоугольных комнат | Абзац «Коллизии» переписан: для непрямоугольного контура число из-за коллизии больше не скрывается — сначала кегль 6 pt и сдвиг вдоль ребра, а при нехватке места — компактная метка R<n> с выносной линией и полное значение в нумерованном нижнем блоке «Внутренние размеры». Нумерация детерминирована (room.id, лексикографически минимальная вершина по часовой стрелке). AC5 переформулирован: «значения не исчезают из-за коллизий» |
docs/specs/053-pdf-export.md:156-168 (§7.3 «Коллизии»), AC5 строка 330, свидетель pdf-room-edge-dropped (§13, строка 351) теперь проверяет именно это |
| Medium-3 — AC6 проверял правило (порог 30 см), которого не было в контракте внешних размеров | В абзац «Внешние размеры планировки» добавлено явное предложение: порог 30 см для внутренних рёбер здесь не применяется, каждая физическая наружная грань — включая короче 30 см — получает число; при нехватке места оно выносится наружу на полку, а не скрывается. AC6 переформулирован без клаузы про засечку, вместо неё — «включая грань < 30 см… тесные значения не скрываются, а детерминированно выносятся» | docs/specs/053-pdf-export.md:146-150 (§7.3 «Внешние размеры планировки»), AC6 строка 331 |
| Medium-4 — порог производительности ссылался на несуществующую конфигурацию (60 комнат) | §15 и риск в §16 переписаны на фактическое число: «одно текущее пространство large-house: 20 комнат» вместо «60 комнат» |
docs/specs/053-pdf-export.md:359-361 (§15), :372 (§16, строка риска про 4+ числа на комнату) |
Все четыре Medium-находки r1 закрыты правкой текста, без обращения к владельцу, как и предписывал вердикт r1. Ни одна не осталась «оставленной в тексте ревью» — везде видна конкретная строка контракта, а не заявление автора.
Унаследовано из r1
Без повторной проверки в этом раунде принято (документ
docs/reviews/SPEC-REVIEW-53-r1.md, материал cc93e93d, разделы дельтой не
задеты):
- Сверка 10 из 12 фактических утверждений ТЗ с кодом (таблица r1: модель
spaceModels, резолвер севера, поля подложки,decor[]/furniture-art-runtime,geometryArea, грань-в-грань линейка ресайза_rszEdgeLabels, образец ленивого чанкаiso-scene-render, потолокhouseplan-card.ts, терминология кнопки и порядок иконок, touch-контракт диалога) — дельта эти строки не трогает, коду ничего не противоречит. - Обязательные разделы §7.1 PROCESS.md присутствуют и однозначны — структура документа не менялась, кроме уточнённых абзацев §7.3/§11/§15/§16.
- AC1-AC15 пронумерованы без пропусков и дублей, у каждого указан способ доказательства из принятого словаря — дельта меняет только текст AC5/AC6, не их номер и не наличие доказательства.
- Q1/Q2/Q3 §18 — принятые предположения с обоснованием, не эскалированы владельцу, ревьюер согласен — дельта раздел 18 не трогает.
- AC12 (ручная проверка на Android) — прецедент из #473 признан достаточным в r1, дельта AC12 не касается.
- «Один источник числа» для площади и внутренних размеров (
_rszEdgeLabels/_cleanFloor→geometryArea→formatArea) — подтверждено в r1 чтением кода, дельта этот путь не меняет. - Свидетели §13 покрывают по имени защитные сценарии таблицы §7.1/§7.3/§8 —
состав свидетелей не расширялся и не сокращался этой дельтой (те же шесть
имён, включая
pdf-room-edge-droppedиpdf-outer-face-inside), изменилось только описываемое ими поведение (см. таблицу закрытия выше).
Находки
Low-1 — предложение об i18n-потребителях в §9 не покрывает новый ключ на листе
§9 заканчивается фразой «Тест i18n-dead-keys требует потребителя у каждого
— все используются диалогом или подвалом». Это предложение не менялось в
cc93e93d→299629d2 буквально, но список ключей перед ним — да: в него
дельтой добавлен pdf.internal_dimensions (заголовок нумерованного блока
«Внутренние размеры» из фикса Medium-2, §7.3). Этот ключ печатается на теле
листа — в свободном поле рядом с геометрией, а не в диалоге и не в подвале
(«Масштаб 1:N», линейка, легенда, дата, версия — это подвал по §7.5).
Итоговое предложение теперь неточно описывает собственный список: оно
по-прежнему верно для одиннадцати из двенадцати ключей, но не для
добавленного.
Почему не Medium. Место использования ключа не создаёт двусмысленности
для реализации — §7.3 однозначно описывает, что и где печатается; тест
i18n-dead-keys по смыслу (потребитель у каждого ключа существует) от этой
неточности не пострадает, он не проверяет где именно строка используется,
только что она используется хоть где-то. Дефект чисто редакционный.
Воспроизведение: grep -n "internal_dimensions\|диалогом или подвалом" docs/specs/053-pdf-export.md — строки 291 и 294 в одном разделе, второе неточно описывает первое.
Решение ревьюера: снимаю как Low с записью, не блокирует. Правится тривиально при следующей правке текста (например, «диалогом, листом или подвалом»); отдельного цикла ради одной фразы не открываю.
Что проверено и корректно
- Все четыре Medium-находки r1 закрыты по существу, а не переформулированы
без изменения смысла — проверено построчным сравнением
git diff cc93e93d..299629d2(таблица закрытия выше), а не заявлением автора. - Правка Medium-2 не создаёт новой внутренней коллизии: приоритет коллизий
(§7.3 «Коллизии» — внешние → площади → внутренние длинные → короткие) и
правило «дубликаты у прямоугольников опускаются первыми» остались
согласованы с обновлённым fallback для непрямоугольных контуров
(кегль 6 pt → сдвиг → метка
R<n>в нумерованном блоке); ни один путь коллизии больше не приводит к молчаливому исчезновению числа. - Нумерация меток
R<n>в §7.3 детерминирована явно («комнаты — поroom.id, рёбра — от лексикографически минимальной вершины по часовой стрелке»), что не ломает детерминизм байт PDF, заявленный в §8.4 и требуемый AC10 — источник порядка не зависит от времени исполнения. - Правка Medium-3 не оставила противоречия между AC6 и текстом §7.3: клауза
«рёбра < 30 см — засечка без числа» убрана из AC6 полностью, а не
продублирована с оговоркой — единственный порог 30 см в документе теперь
относится только к внутренним рёбрам комнаты (проверено
grep -n "30 см" /tmp/spec_r2.md: три вхождения, все — внутренние размеры/коллизии; одно — явное «здесь не применяется» для внешних граней). - Правка Medium-4 арифметически соответствует фикстуре:
large-house.mjsстроитFLOOR_COUNT=3пространства поROOMS_PER_FLOOR=20— новое число «20 комнат на текущее пространство» в §15/§16 совпадает с тем, что PDF реально получит на входе (одно пространство за раз, §5). - Запись в
docs/SCOPE.mdтекстуально согласована с решениями владельца в комментариях issue (2026-08-15 — исключение принято; 2026-09-07 — сужение до текущего пространства, read-only, без второй геометрической модели) — сверено дословно, а не по духу. - Изменение
docs/specs/052-view-dimensions.md(о неприменимости раздела «Длины» к бумаге) не входит в дельту этого раунда (cc93e93d..299629d2его не трогает) — это уже было внесено до r1 и разобрано им; лишний раз не проверял (см. «Унаследовано»), но сверил, что дельта его не откатывает.
Чего не проверял
- Код фичи по-прежнему не существует (
src/pdf/**нет в дереве) — дешёвые гейты (tsc,test,build), golden, смоки, мутационные свидетели не гонялись и не могли гоняться: это ревью ТЗ, а не код-ревью. Автор привёлnode scripts/check-docs.mjs --screenshots=warn(зелёный) в хендоффе — необязательный для docs-only правки шаг (screenshots касаютсяsrc/**, дельта его не трогает), не перепроверял, поскольку он не входит в состав обязательных гейтов ревью ТЗ. - Не проверял вручную макет печатного листа с нумерованным блоком «R» — на этапе ТЗ такого артефакта не существует; в r1 это же было явно указано как непроверяемое на этой стадии.
- Не переоценивал заново все десять фактических утверждений ТЗ, подтверждённых в r1 (модель, резолверы, ленивый чанк, потолок ядра, терминология, touch-контракт) — дельта их не задевает (см. «Унаследовано из r1»).
- Не проверял i18n-ключи
de/frпострочно — переводы появятся вместе с кодом; на этапе ТЗ фиксируется только список ключей и их обязательность, что не изменилось дельтой (кроме добавления одного ключа, см. Low-1).
Вердикт
Все четыре Medium-находки r1 закрыты в тексте ТЗ, ни одна не осталась непокрытой и ни одна не переоткрылась дельтой (проверено построчно, не на слово автора). Единственная новая находка этого раунда — Low, снята решением ревьюера с записью, реализации не мешает. High-находок нет.
Вердикт: зелёный · заход r2 · блокирующих циклов 1/4 · High: 0 · Medium: 0
Материал раунда
- Ветка:
issue/53-pdf-export, коммит299629d2d272— ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет. - Дерево материала:
f83621000ce142c143b4896b3c267dbb7ca1b552git log --all --format='%H %T' | grep f83621000ce1