22 KiB
CODE-REVIEW-484-r1
Issue: #484 — PDF: внешняя размерная цепь теряет размеры ступенчатого фасада
ТЗ: docs/specs/484-pdf-exterior-dimension-chain.md, ревью ТЗ SPEC-REVIEW-484-r1.md — зелёный.
Ветка: issue/484-pdf-exterior-dimensions. Материал ревью: git diff origin/dev...HEAD, HEAD на момент вывода вердикта — 904b8b225159e61546aba9a1ad46f548a1b3f4db (сверено непосредственно перед выводом, git status чист).
Заход: r1 (первый заход, разделов «Унаследовано»/«Закрытие r0» не требуется — §2.10 применяется со второго цикла).
1. Скоуп диффа
62 файла, ядро изменения — два продуктовых файла:
src/pdf/pdf-collision.ts— новый opt-in режимallowStartExitвpdfSegmentTouchesGeometry(): собирает пересечения extension line с архитектурными rings в отсортированные breakpoints, ищет первый свободный интервал («выход из собственного узла») и после него красит любое касание/пересечение/overlap как коллизию.src/pdf/pdf-scene.ts— только две внешние extension lines (entry.a→entry.oa,entry.b→entry.ob) переведены сallowStartBoundaryнаallowStartExit; dimension line, shelf, labels и внутренние размеры остались на строгом пути.
Остальное — тесты (test/pdf-collision.test.mjs, test/pdf-scene.test.mjs,
test/golden-matrix.test.mjs), два обязательных мутанта в
scripts/mutation-gate.mjs, новый browser-смок кейс в demo/smoke_pdf_export.mjs,
привязка символов в scripts/smoke-links.mjs, golden-фикстура/сценарий
(demo/golden/harness.mjs, demo/golden/matrix.mjs, demo/golden/run.mjs,
новый эталон pdf-export-stepped-dimensions-light + два обновлённых
pdf-export-geometry-light/pdf-export-polish-light), документация
(ARCHITECTURE.md, PDF-EXPORT.md, USER-GUIDE.md/.ru.md, оба CHANGELOG,
STATUS.md, docs/specs/README.md) и пересобранный бандл/фингерпринт
скриншотов.
2. Как проверялось
| Гейт | Команда | Результат | Источник |
|---|---|---|---|
| typecheck+build | npm run build (= tsc --noEmit && rollup -c) |
зелёный | прогнан лично на HEAD |
| unit — целевые | npx tsc -p tsconfig.test.json && node scripts/fix-test-build.mjs && node --test --test-name-pattern="external dimension extension|stepped exterior|reconstructable dimension chain" test/pdf-collision.test.mjs test/pdf-scene.test.mjs test/golden-matrix.test.mjs |
3/3 pass | прогнан лично |
| unit — полный набор + backend | — | не перегонял отдельно | зелёный Validate на точном SHA 904b8b22 (https://github.com/Matysh/houseplan-card/actions/runs/34151982086), см. условие сужения в задании ревью |
мутант AC3 pdf-own-exit-prefix-disabled |
node scripts/mutation-gate.mjs --id=pdf-own-exit-prefix-disabled |
«тест покраснел, как обязан», 1/1 | прогнан лично |
мутант AC4 pdf-post-exit-collision-ignored |
node scripts/mutation-gate.mjs --id=pdf-post-exit-collision-ignored |
«тест покраснел, как обязан», 1/1 | прогнан лично |
смок (по smoke-select.mjs) |
node demo/smoke_pdf_export.mjs (после npm run bundle:sync) |
зелёный, steppedExteriorKeepsCompleteDimensionChain: true, 0 EXC, exit 0 |
прогнан лично |
check-docs.mjs |
node scripts/check-docs.mjs |
«Documentation checks passed (7 files, 12 external links)» | прогнан лично |
golden:verify |
npm run golden:verify |
зелёный, exit 0; отдельно подтверждены три PDF-сценария: pdf-export-geometry-light passed, pdf-export-polish-light passed, pdf-export-stepped-dimensions-light passed |
прогнан лично дважды (второй раз — построчный лог) |
| model-invariants | — | не запускал | диф не трогает геометрию плана/сохранённую модель — только временную PDF-сцену коллизий поверх уже построенных architectureRings; §6.3/§8 ТЗ явно фиксируют read-only и отсутствие изменений модели |
pytest tests_backend |
— | не запускал | Python/бэкенд не тронуты (0 файлов custom_components/**/*.py в диффе) |
| performance-профили сверх стандартных | — | не запускал | диф не трогает src/iso-*, src/live-*, src/render-*, houseplan-render-lifecycle.ts, houseplan-card.ts; названный в AC6 бюджет <200 ms проверяется существующим неизменённым тестом test/pdf-scene.test.mjs:618 («current 20-room large-house space builds... under 200 ms»), покрыт зелёным полным прогоном test в Validate на точном SHA |
node scripts/smoke-select.mjs --base origin/dev --head HEAD вернул ровно одну
зарегистрированную связь — demo/smoke_pdf_export.mjs, объявленную через
символы pdfSegmentTouchesGeometry, SegmentIntersection, segmentIntersection,
COORDINATE_EPSILON, PdfCollisionPoint, forEachRingSegment, pointOnSegment,
segmentParameter (добавлены автором в scripts/smoke-links.mjs этим же диффом).
Других смоков (из 229 в дереве) diff не касается — прогон одного соответствует
объёму задачи (изменение локально для src/pdf/**).
3. Разбор по AC
- AC1 (полная цепь ступени, оба winding) — доказано
test/pdf-scene.test.mjs«stepped exterior keeps the complete reconstructable dimension chain»: цикл по[step, [...step].reverse()], точные значения1.20/2.40/3.60/3.75присутствуют для обоих обходов. Прогнано лично, зелёный. - AC2 (единственность глубины, dedup, без диагонали) — тот же тест:
values.filter(v => v === '1.20 m').length === 1,values.length === 6(4 внешних + общие width/height),command.angle % 90 < 1e-8для всех подписей. Зелёный. - AC3 (разрешённый начальный выход) —
test/pdf-collision.test.mjsпокрывает коллинеарный выход вдоль примыкающей ступени, выход через тело инцидентной стены (квадрат), сегмент, не достигающий свободного участка (fail closed), и ограничение исключения только сегментами, реально стартующими на своей границе. Защитный мутантpdf-own-exit-prefix-disabledкрасит тест при отключении режима — проверено лично, эффект подтверждён. - AC4 (запрет повторного входа после выхода) — тот же файл: второй wall
после первого свободного интервала (
combinedSolid, солид-коллизия) и касание постороннего угла без солид-интервала (stepSolid-only, точечное касание за счёт финальной проверкиhits.some(hit.t > exitT...)) — оба даютtrue. Защитный мутантpdf-post-exit-collision-ignoredкрасит тест при отключении финальной проверки — проверено лично, эффект подтверждён. Разобрано и по коду (см. §4 ниже) — механизм state-machine «префикс → выход → любое дальнейшее касание» соответствует §6.2 ТЗ построчно. - AC5 (label/dimension line/shelf/внутренние размеры не изменились) —
проверено чтением:
grep -n "segmentTouchesArchitecture" src/pdf/pdf-scene.tsпоказывает ровно один вызов (для двух extension lines внешнего размещения);allowStartExitне используется больше нигде вsrc/pdf/pdf-scene.ts;allowStartBoundaryдля внешних сегментов заменён полностью (не оставлен как мёртвый код), а как публичный APIpdfSegmentTouchesGeometryсо старым поведением по-прежнему покрыт своими исходными тестами (не тронуты этим диффом). Внутренние размеры комнат этот collision-helper вообще не вызывают. - AC6 (winding-независимость, детерминизм,
<200 ms) — winding-цикл в AC1-тесте покрывает независимость от обхода; perf-бюджет — существующий неизменённый тест, зелёный в Validate на точном SHA. - AC7 (визуальное присутствие цепи в PDF) — двойное доказательство:
browser-смок
demo/smoke_pdf_export.mjsреально скачивает PDF из карточки в Chromium, разбирает черезpdfjs-distи проверяет присутствие всех шести подписей (1.20/2.40/3.60/3.75/7.35/9.75 m) — прогнано лично, зелёный; goldenpdf-export-stepped-dimensions-lightсpdfSemantic.requiredDimensionLabelsиforbiddenText— прогнано лично (golden:verify), passed. - AC8 (документация, changelog, docs/golden fingerprints) — оба CHANGELOG
ссылаются на #484 в одном коммите с кодом (
e88ef93c),PDF-EXPORT.mdиARCHITECTURE.mdописывают узкий source-exit контракт словами, совпадающими с §6.2 ТЗ,USER-GUIDE.md/.ru.mdформулируют пользовательское обещание.check-docs.mjsзелёный. Скриншот-фингерпринт обновлён отдельным коммитом7b826c19с трейлеромBaseline-Reviewed: <ссылка на прогон>— пиксели десяти doc-кадров не изменились (decision.replace.length === 0,lastWriteWasFingerprintOnly: true), что ожидаемо: правка изолирована в PDF export collision-геометрии и не задевает ни один из синтетических doc-сценариев.
4. Разбор ключевой логики чтением (без исполнения, дополнительно к тестам)
Прочитан построчно новый ветвь allowStartExit в pdfSegmentTouchesGeometry()
(src/pdf/pdf-collision.ts:168-208). State machine:
- Если старт не на границе архитектуры —
true(нет смысла в исключении, fail closed). - Собираются все пересечения
start→endс рёбрами всех rings (hits), включая точечные касания (t === endT) и коллинеарные перекрытия (overlap: true, диапазон[t, endT]). - Отрезок делится на интервалы по отсортированным уникальным breakpoints
(
0, 1, hit.t…, hit.endT…, схлопнутым сparameterEpsilon); каждый интервал помечается «blocked», если его середина внутриisSolidили внутри collinear-overlap. - Идёт слева направо: пока не найден первый свободный интервал — копится
«собственный префикс» (не важно, что именно его блокирует — коллинеарный
участок примыкающей грани или солид инцидентной стены, оба разрешены §6.2);
как только
exitTнайден — любой следующий blocked-интервал даётtrue. - Если свободного интервала не нашлось вовсе —
true(line 206, буквально реализует «линия, которая так и не получает свободного участка» из §6.2 — fail closed). - Финальная строка (207-208) ловит случай, который шаг 4 пропускает: точечное
касание постороннего угла ПОСЛЕ выхода, не создающее blocked-интервал (обе
соседние середины остаются свободны), но являющееся касанием по существу —
hits.some(hit.t > exitT || hit.endT > exitT). Именно этот путь проверяет тест «a later point contact remains a collision even without a solid interval», и именно его выключает мутантpdf-post-exit-collision-ignored.
Прослежено вручную на fixture из ТЗ/бага ((10,3)→(16,3) вдоль ступени
(0,0)→(10,0)→(10,3)→(12,3)→(12,6)→(10,6)→(10,10)→(0,10)): начальный коллинеарный
перекрыв с ребром (10,3)-(12,3) даёт blocked-префикс [0, 0.333], exitT=0.333,
после чего свободно до конца — линия принимается без коллизии, что и требуется
для восстановления глубины выступа. Согласуется с зелёным AC1/AC3-тестами.
Существенных расхождений между кодом, ТЗ §6.2 и текстом issue не найдено.
5. Находки
Находок High или Medium нет. Two Low-замечания, ни одно не блокирует и не требует правки:
- Low. Комментарий в
pdf-collision.ts:204-205объясняет назначение финальной проверки, но не называет её отдельно как «point-contact detector» — при последующей правке этой функции легко принять её за дублирование шага 4 и удалить как «мёртвый код», сломав ровно тот случай, который ловит мутантpdf-post-exit-collision-ignored. Не блокирует: тест и мутант физически предотвращают тихую регрессию, комментарий — не более чем удобство чтения. Оставляю как есть, автор/ревьюер могут усилить комментарий по желанию, отдельного цикла это не заслуживает. - Low.
docs/images/screenshots.jsonполучил"acceptedOn": "win32"вместо"linux"в этом же диффе (коммит7b826c19,Baseline-Reviewedуказывает на CI-прогон). Механизм#401/#455(scripts/docs-accept.mjs) намеренно допускает приёмку с любой машины при условии совпадения witness-хешей с уже закоммиченными кадрами — это не нарушение процесса, а штатный путь «fingerprint-only» accept (ни один из 10 PNG не заменён:lastWriteWasFingerprintOnly: true). Отмечаю только потому, что поле смены платформы бросается в глаза при чтении диффа; проверка (check-docs.mjs) зелёная, разбирать глубже не требуется.
6. Что проверено и корректно
- Контракт
allowStartExitреализует именно ту state machine, которую описывает §6.2 ТЗ, включая fail-closed при недостижимости свободного участка и защиту от post-exit re-entry (в т.ч. точечных касаний). allowStartBoundary(старое поведение) не удалён и не изменён — используется собственными старыми тестами и остаётся доступным API; ни один существующий вызывающий код кроме двух extension lines не переключён наallowStartExit.- AC1/AC2 подтверждены точными числовыми значениями на fixture из issue, для обоих направлений обхода ring.
- Оба обязательных защитных мутанта (AC3, AC4) реально красят целевой тест при снятии соответствующей защиты — проверено запуском, а не по описанию.
- Browser smoke реально качает PDF из собранного бандла (
dist→bundle:sync→ demo stand) и разбирает егоpdfjs-dist, а не читает внутреннее состояние — сквозной путь до реального артефакта. golden:verify(полный матрикс, обязателен процессом — частичный запуск инструмент отвергает) зелёный, включая обновлённые/новый PDF-эталоны.- Документация (оба changelog, PDF-EXPORT.md, ARCHITECTURE.md, USER-GUIDE и .ru.md, STATUS.md, specs/README.md) согласована с контрактом ТЗ и терминологией USER-GUIDE.ru.md.
- Traceability:
Issue: #484иUser-Visibleесть на всех коммитах диапазона; оба changelog правились в одном коммите (e88ef93c) с поведенческим фиксом.
7. Чего не проверял и почему
- Полный
npm test/npx tsc --noEmitотдельно отnpm run build— не перегонял: Validate зелёный на точном SHA904b8b22(https://github.com/Matysh/houseplan-card/actions/runs/34151982086), что по условию задания достаточно для этой пары дешёвых гейтов;buildвсё же прогнал лично (нужен был живойdistдля смока). python -m pytest tests_backend— не запускал, диф не касаетсяcustom_components/**/*.py.node scripts/model-invariants.mjs— не запускал: диф не меняет геометрию плана,layout,marker.space,open_spansили записи толщины стен; PDF export работает поверх уже готовой read-only сцены и не пишет обратно в модель (подтверждено §8 ТЗ и чтением: изменения ограниченыpdf-collision.ts/pdf-scene.ts).- 228 из 229 файлов
demo/smoke_*.mjs, не названныхsmoke-select.mjsи не упомянутых в AC, — не прогонял: diff не расширяется дальше PDF-экспорта, полный прогон — предрелizный гейт (§8 PROCESS.md), не гейт ревью. - Performance-профили
large-house-isometric-v1/large-house-interaction-v1— не запускал: правка не трогаетsrc/iso-*/src/live-*/src/render-*/houseplan-render-lifecycle.ts/houseplan-card.ts, названные в §8 условия их подключения к Validate не выполняются. - Живой снимок скринов документации (
npm run docs:accept) — не переснимал:check-docs.mjsуже зелёный на текущем fingerprint, коммит7b826c19уже содержит доказательство (Baseline-Reviewed).
8. Вердикт
Зелёный. Все 8 AC доказаны автотестом (с проверенной способностью падать через два обязательных мутанта) либо разобраны чтением там, где защита тривиальна (AC5 — отсутствие новых вызовов). High: 0. Medium: 0. Low: 2, обе сняты без правки с записью выше (п. 5).
Материал раунда
- Ветка:
issue/484-pdf-exterior-dimensions, коммит904b8b225159— ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет. - Дерево материала:
0b96360647a80380beab99dba4225b3f72556d30git log --all --format='%H %T' | grep 0b96360647a8 - ТЗ
docs/specs/484-pdf-exterior-dimension-chain.md, блоб84fb3a65e309f90c2c8c149b6f7bb4f6bf876f4dgit log --all --find-object=84fb3a65e309f90c2c8c149b6f7bb4f6bf876f4d -- docs/specs/484-pdf-exterior-dimension-chain.md