Files
houseplan-card/docs/reviews/SPEC-REVIEW-530-r1.md
T
2026-09-11 09:46:23 +00:00

91 lines
22 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-530-r1
**Issue:** #530 — «Экспорт в PDF: план занимает половину листа A4, а столбец выносов отбирает у него целый шаг масштаба»
**Этап:** spec (ревью ТЗ, PROCESS.md §2.4)
**Трек:** полный (автор называет нарушенные критерии §5: «нет влияния на производительность» и «нет нового UX-контракта» — оба нарушены обоснованно, см. «Что проверено и корректно»)
**Заход:** r1 · блокирующих циклов израсходовано 0 из 4
## Скоуп
ТЗ живёт в теле issue #530, раздел `## ТЗ` (после аналитики S2 и текста «ТЗ готово» в комментариях). Предмет: убрать столбец «Internal dimensions» и метки `R…` из PDF-экспорта плана (`src/pdf/pdf-scene.ts`), уменьшить кегли подписей внутри плана на 25 % (размер 7→5.25, площадь 8→6, имя комнаты 9/7→6.75/5.25), не трогая ряд масштабов и правило выбора ориентации. Контракт К1-К5, AC1-AC6.
## Как проверялось
Чтением кода, без исполнения — на этапе ТЗ кода ещё нет, гейты не прогонялись (правка кода не входит в этот этап). Каждое количественное или поведенческое утверждение ТЗ сверено построчно с текущим `dev` на материале `8f5a08a6` (совпадает с SHA, на котором получена аналитика S2).
- `docs/SCOPE.md` — продуктовая рамка (J4/J6, персона «администратор дома», печатный лист как артефакт, уносимый из продукта); раздел «Out of scope» с узким исключением для #53 (сам факт существования PDF-экспорта).
- `AGENTS.md`, `PROCESS.md` §2.3-§2.5, §5, §7.1 — формальные требования к ТЗ и лёгкому/полному треку.
- `docs/PDF-EXPORT.md` — канонический документ подсистемы PDF-экспорта (не входит в список §-канонов AGENTS.md дословно, но это единственный документ, детально описывающий поведение, которое правка меняет).
- `docs/USER-GUIDE.ru.md:1835-1857` — раздел «Сохранение текущего пространства в PDF»; ссылается на `PDF-EXPORT.md` за деталями, сам текст выносок/столбца не упоминает.
- `src/pdf/pdf-scene.ts` целиком (840 строк): `PDF_SCALE_SERIES`, `betterPdfCandidate`, `buildPdfPage`, цикл подбора масштаба (770-839), блок печати меток/столбца (624-696), кегли (584-740).
- `src/i18n/{en,ru,fr,de}.json:452` — `pdf.internal_dimensions` существует во всех четырёх локалях.
- `test/pdf-scene.test.mjs` целиком (840 строк): тесты `unprintable fixed callouts fail closed…` (494-503), `oversized architecture brackets a larger printable scale…` (505-514), `dense non-rectangular rooms keep mandatory dimensions in stable callouts` (563-598), бюджетный тест на 200 мс CPU (618-679).
- `demo/golden/matrix.mjs:367-419` — сценарии `pdf-export-geometry-light`, `pdf-export-stepped-dimensions-light`, их текущие `minSceneCoverage`.
- `scripts/mutation-gate.mjs:224-247` — существующие мутанты `pdf-dimensions-ignore-toggle`, `pdf-room-edge-dropped`, их `guard`/`because`.
## Находки
### Medium — правка меняет два поведения, задокументированных в `docs/PDF-EXPORT.md`, но документ не назван ни в скоупе, ни в release-артефактах
**Файл:** тело issue #530, разделы «Чего задача не трогает» и «Release-артефакты».
`docs/PDF-EXPORT.md` — единственный текст, детально описывающий именно то поведение, которое ТЗ меняет, и он расходится с новым контрактом в двух конкретных местах:
1. Строка 59: «…while required values that cannot fit beside an edge use numbered callouts» — после AC1 это утверждение становится **ложным**: такое значение теперь не печатается вовсе (контракт п.1: «Размер, который не удалось разместить внутри комнаты, не печатается»), а не переносится в столбец, которого больше нет. Заодно строка 32 перечисляет «callouts» как часть собираемой сцены — тоже устаревает.
2. Строки 41-42: «If a fixed annotation itself cannot fit on one A4 sheet, export stops with an error instead of producing a clipped file.» Прочитан код, который это реализует: `buildPdfPage` (`src/pdf/pdf-scene.ts:770-839`) подбирает масштаб перебором `PDF_SCALE_SERIES`, а при неудаче — экспоненциальным пробегом до `maxFallbackScale = 1_000_000` (773-820), и бросает `pdf.failed` только если **вообще ни один масштаб не подошёл** (821) либо финальная проверка границ не сошлась (838). Единственный тест, дёргающий этот путь — `unprintable fixed callouts fail closed…` (494-503) — форсирует его руками через `name: 'X'.repeat(1000)`, которое превращается в строку столбца выносов (`${callout.mark} ${callout.room}: ${callout.value}`, 691) фиксированной ширины, **не сжимающуюся с ростом масштаба**. Архитектура сама по себе всегда находит подходящий масштаб (второй тест, 505-514, явно это проверяет: `output.scale > 500`, до 1:1 000 000), а подписи внутри плана (имя 9/7, размер 7, площадь 8) при нехватке места **не бросают исключение, а тихо не печатаются** (`nameSize = 0`, комментарий 674-676 «Rectangular rooms have no unambiguous numbered callout fallback… omit the unsafe label»). То есть столбец выносов — единственный известный код-путь, форсирующий отказ `pdf.failed` через «не влезающую фиксированную подпись»; когда он уходит, предложение из `PDF-EXPORT.md` перестаёт описывать что-либо достижимое обычным использованием (кроме отдельного, уже названного в контракте п.5 случая «архитектура не влезает ни в один масштаб», #53) — но сам текст документа остаётся как есть и вводит в заблуждение.
Контракт п.5 ТЗ сам по себе корректен и честен («Отказ fail closed сохраняется для архитектуры, которая не влезает ни в один масштаб (#53)») — это именно то более узкое, правильное описание, которое должно заменить нынешний текст `PDF-EXPORT.md`. Но раздел «Release-артефакты» называет только два бюллетеня changelog; раздел «Чего задача не трогает» тоже не упоминает документацию. DoR (PROCESS.md §2.5) требует «release-артефакты названы: changelog RU+EN, документация, golden/скриншоты… — либо явное "нет"» — здесь пункт «документация» просто отсутствует, а не назван явным «нет», и это не безобидный пропуск: документ буквально описывает то, что задача удаляет.
**Требуется от автора (правка ТЗ, тот же issue, без нового):** добавить `docs/PDF-EXPORT.md` в затронутые файлы/release-артефакты и явно зафиксировать, что строки 32/58-59 (упоминание «callouts»/«numbered callouts») убираются, а строки 41-42 (fail-closed для «fixed annotation») переформулируются в духе контракта п.5 — только для архитектуры, не влезающей ни в один масштаб.
Серьёзность — Medium, в скоупе задачи: без High-находок это жёлтый вердикт, правка ТЗ и повторный (второй) заход.
### Low — существующий мутант `pdf-room-edge-dropped` защищает ровно ту гарантию, которую контракт отменяет
**Файл:** `scripts/mutation-gate.mjs:235-247`.
Мутант `pdf-room-edge-dropped` (`because: 'every non-short edge must retain a direct value or a numbered callout'`) роняет из массива дедуплицированных рёбер последнее (`.slice(0, -1)`) и ожидает, что тест `dense non-rectangular rooms keep mandatory dimensions in stable callouts` его поймает. AC1 переписывает именно этот тест на противоположное утверждение («не печатается» вместо «восстанавливается через столбец»), и «every non-short edge must retain a direct value or a numbered callout» после правки более не является инвариантом продукта. AC-таблица называет только два новых мутанта (`pdf-restores-dimension-callouts`, `pdf-keeps-large-in-plan-labels`), не называя, что делать со старым `pdf-room-edge-dropped` — хотя `scripts/mutation-gate.mjs` уже назван затронутой поверхностью в аналитике S2. Не блокирует: файл уже в списке затронутых, и это естественное следствие переписывания теста, на который мутант ссылается по имени — но стоит явно решить (снять мутант или переформулировать его `because`) при реализации, а не молча оставить ссылку на несуществующую больше гарантию.
## Что проверено и корректно
- **Продуктовая рамка (§7.1).** Раздел «Продуктовая рамка» называет персону (администратор дома, `docs/SCOPE.md`), поверхность (печатный лист) и момент («уносит из продукта наружу»). «До/После» сформулированы одной фразой без терминов реализации.
- **Трек выбран верно.** Оба названных нарушенных критерия §5 подтверждаются: перф — сборка сцены имеет собственный бюджет 200 мс, проверяемый существующим тестом (618-679), состав сцены меняется; UX-контракт — печатный лист меняет ориентацию (книжная вместо альбомной на форме владельца) и теряет целый раздел (столбец), это не «поведение в рамках уже описанного».
- **Все количественные утверждения о текущем состоянии подтверждены чтением кода.** `PDF_SCALE_SERIES = [20,25,50,75,100,150,200,250,500]` (`pdf-dimensions.ts:572`); кегли — размер 7 (`pdf-scene.ts:611,643`), площадь 8 (596), имя 9/7 (584-585), метка `R…` 6 (673), заголовок столбца 8 (688), строка столбца 6 (692); хром страницы — заголовок 14 (707), название карточки 10 (710), «Scale 1:N» 8 (716), линейка/компас 7 (728,736,740) — контракт п.2 correctно утверждает, что хром не меняется. Арифметика ×0.75 (7→5.25, 8→6, 9→6.75, 7→5.25) верна.
- **AC1/К1 точно описывают, что удаляется.** Столбец (688-693), линия-выноска и счётчик `R${callouts.length+1}` (670-673), ключ `pdf.internal_dimensions` — существует во всех четырёх локалях (`en/ru/fr/de.json:452`) и больше нигде в `src/**`, кроме `pdf-scene.ts:688` и самого теста (`test/pdf-scene.test.mjs:587`).
- **Два переписываемых теста названы точно и по существу.** `unprintable fixed callouts fail closed instead of returning a clipped PDF page` (494-503) действительно форсирует переполнение именно через столбец (см. находку выше) — «теряет предмет» после удаления столбца заявлено честно, не втихую. `dense non-rectangular rooms keep mandatory dimensions in stable callouts` (563-598) действительно утверждает восстановимость через `pdf.internal_dimensions`/`R\d+` — ТЗ верно называет, что смысл теста меняется на противоположный.
- **AC5 использует существующие, а не выдуманные golden-сценарии.** `pdf-export-geometry-light` (`matrix.mjs:371`) и `pdf-export-stepped-dimensions-light` (415) существуют, у обоих уже задан `pdfSemantic.minSceneCoverage` (0.20 и 0.15), который run.mjs:292-293 сравнивает с фактическим покрытием — «поднятый до фактического» порог технически корректен как способ доказательства.
- **Контракт п.1 «правило, по которому уже сегодня живут прямоугольные комнаты, становится общим» — подтверждено.** Для прямоугольных комнат (`nonRect === false`) код уже тихо пропускает неразмещённую подпись без исключения (комментарий 674-676); правка лишь распространяет это же поведение на непрямоугольные.
- **AC4 корректно закрывает риск «потеряли подписи, убирая столбец».** Формулировка требует не меньшего числа подписей на плане (38 vs 35) и отсутствия повторной печати значения — это тот же класс требования, что и «одно число — один источник»; отдельного нарушения этого принципа в диффе нет, поскольку до правки значение печаталось либо на плане, либо в столбце, никогда в обоих местах одновременно, и после правки источник остаётся один — на плане либо нигде.
- **Риски честны и по существу.** Кегль 5.25 pt (~1.85 мм) назван на грани чертёжной практики с обозначенным путём отступления (−15 % вместо −25 %); потеря семи коротких размеров явно вынесена в changelog; необходимость пересъёмки golden на Linux CI (#455) названа как риск, а не как гейт «для галочки».
- **Блок «Принято предположительно»** сформулирован по форме, требуемой §7.1 («принято предположительно, менять свободно»), и содержит только техническую механику (пилообразное заполнение — отдельная задача; поля/шапка/подвал не трогаются; выноска у стены — альтернатива, не эта задача), без продуктовых решений, спрятанных под видом технических.
- **Откат — один revert**, данные/конфиг/публичные контракты не затронуты — соответствует реальному объёму правки (i18n-ключ + один модуль рендера + тесты + golden-кадры).
## Чего не проверял
- Не запускал `npx tsc --noEmit`, `npm test`, `npm run build`, golden — этап ТЗ, продуктового кода ещё нет, оценивать нечего.
- Не проверял точность самих числовых замеров аналитики S2 (35→38 подписей, 29 против 36 уникальных значений, доли площади листа) — не переисполнял `buildPdfPage` на синтетической фикстуре владельца; это воспроизводимый прогон Node-скрипта, а не факт из документа, и на этапе ТЗ отдельно не перепроверялся. Внутренняя согласованность утверждений (аркметика кеглей, существующие константы, имена тестов) проверена полностью и разошлась только в находках выше.
- Не проверял `demo/smoke_pdf_export.mjs` на предмет ассертов, зависящих от столбца/меток — файл назван затронутой поверхностью в S2, но правка теста туда не входит в контракт ТЗ явно; беглый grep не нашёл в нём упоминаний `internal_dimensions`/`R\d`/callout, так что видимого зазора нет, но фикстуры смока не читал построчно.
- Не проверял `docs/images/10-pdf-export.png` (скриншот диалога опций) на предмет устаревания — диалог опций (чекбоксы) визуально не меняется этой задачей, только содержимое самого PDF, так что скриншот UI, по всей видимости, не затронут; глубже не копал.
## Вердикт
Жёлтый. Контракт технически точен и хорошо обоснован для всего, что он утверждает о `src/pdf/pdf-scene.ts` и тестах (все количественные и поведенческие claims сверены с кодом построчно и подтвердились), выбор полного трека обоснован верно, продуктовая рамка и AC полны и проверяемы. Но задача меняет два конкретных предложения канонического документа `docs/PDF-EXPORT.md` (упоминание «numbered callouts» и формулировку fail-closed для «fixed annotation»), и ни «Чего задача не трогает», ни «Release-артефакты» этот документ не называют — DoR §2.5 требует явного «документация: …» или явного «нет», здесь нет ни того, ни другого. Это Medium-находка в скоупе задачи, без High.
---
Вердикт: жёлтый · заход r1 · блокирующих циклов 1/4 · High: 0 · Medium: 1 → в задаче
---
<!-- material-anchors: сгенерировано конвейером (#414) -->
## Материал раунда
- Ветка: `dev`, коммит `8f5a08a60974` — ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет.
- Дерево материала: `0728cc169939a5f201f20df07fc695b6055a071c`
```
git log --all --format='%H %T' | grep 0728cc169939
```
- Тело issue: `3c6248e088a3948ba1b6a44fafbc15118b8ad7706edb941b92b3fd9a4c8135ac`
- Вердикт конвейера: `yellow` · High 0