mirror of
https://github.com/Matysh/houseplan-card
synced 2026-10-05 22:29:05 +00:00
91 lines
22 KiB
Markdown
91 lines
22 KiB
Markdown
# 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
|