22 KiB
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 — единственный текст, детально описывающий именно то поведение, которое ТЗ меняет, и он расходится с новым контрактом в двух конкретных местах:
- Строка 59: «…while required values that cannot fit beside an edge use numbered callouts» — после AC1 это утверждение становится ложным: такое значение теперь не печатается вовсе (контракт п.1: «Размер, который не удалось разместить внутри комнаты, не печатается»), а не переносится в столбец, которого больше нет. Заодно строка 32 перечисляет «callouts» как часть собираемой сцены — тоже устаревает.
- Строки 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 → в задаче
Материал раунда
- Ветка:
dev, коммит8f5a08a60974— ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет. - Дерево материала:
0728cc169939a5f201f20df07fc695b6055a071cgit log --all --format='%H %T' | grep 0728cc169939 - Тело issue:
3c6248e088a3948ba1b6a44fafbc15118b8ad7706edb941b92b3fd9a4c8135ac - Вердикт конвейера:
yellow· High 0