From b989c84b7122bf101a96d82e5d3ec76926342dea Mon Sep 17 00:00:00 2001 From: Matysh Date: Fri, 14 Aug 2026 11:40:57 +0300 Subject: [PATCH] docs: remove originals after the move to docs/reviews and legacy Issue: #142 User-Visible: no --- SPEC-REVIEW-089-isometric-view-stage1.md | 503 ----------------------- 1 file changed, 503 deletions(-) delete mode 100644 SPEC-REVIEW-089-isometric-view-stage1.md diff --git a/SPEC-REVIEW-089-isometric-view-stage1.md b/SPEC-REVIEW-089-isometric-view-stage1.md deleted file mode 100644 index a8e8cdca..00000000 --- a/SPEC-REVIEW-089-isometric-view-stage1.md +++ /dev/null @@ -1,503 +0,0 @@ -# Ревью ТЗ `docs/specs/089-isometric-view-stage1.md` - -Дата ревью: 2026-08-11 -Issue: [#89](https://github.com/Matysh/houseplan-card/issues/89) -Проверенная версия ТЗ: локальный `dev`, после `v1.62.0-beta.1`, SHA `2cf5c27` - -## Итог - -Направление выбрано правильно: фиксированная SVG-проекция, отсутствие новой -модели данных, каноническая геометрия стен, плоские редакторы, скрытая поставка -через Labs и обязательные golden/performance gates хорошо соответствуют -текущей архитектуре House Plan. - -Однако статус **«готово к реализации» пока преждевременен**. В ТЗ остаются -восемь блокирующих неоднозначностей. Главная из них — документ описывает -проекцию точек, но не определяет переход между тремя реально существующими -системами координат и не задаёт новый контракт viewport/frame. Если начать -реализацию буквально по текущему тексту, наиболее вероятный результат — -прыжок масштаба при переключении, рассинхронизация HTML-маркеров и SVG, неверный -warm-remount либо двойное обратное преобразование pointer events. - -Рекомендация: внести блокеры B1–B8 и существенные замечания M1–M8 в ТЗ, после -чего документ можно переводить в `approved`. Переписывать продуктовую часть -или менять выбранный renderer не требуется. - -## Что уже зафиксировано хорошо - -1. Labs не меняет backend, schema/config/layout и не попадает в backup. -2. Плоский вид остаётся default и fallback; редакторы остаются плоскими. -3. CSS 3D, WebGL и многократное клонирование SVG явно запрещены. -4. Источник wall geometry — `wallBodiesGeometry()`, а не новый параллельный - контур. -5. Проёмы должны быть настоящими разрывами masonry geometry. -6. Glow сохраняет один регион на источник и один blur на слой по `LIGHT.md`. -7. Кэш не должен зависеть только от `_cfgEpoch`. -8. У stage 1 нет публичного обещания и пользовательской миграции. - ---- - -## Блокирующие замечания - -### B1 — Не определены системы координат, pivot и projected frame - -**Где:** §4, §7, §8, AC 8. -**Критичность:** blocker. - -`projectPoint(p, z, cam)` и `unprojectPoint(screen, cam)` недостаточны для -существующего renderer. Сейчас House Plan различает как минимум: - -- plan/model units (`room`, `wall`, `marker`); -- координаты SVG scene/viewBox (`_view`); -- client pixels внутри `.stage` (`_screenToVb()`). - -В объёмном виде plan units и scene units перестают совпадать. Кроме того, -`IsoCamera` не содержит pivot/origin и масштаба оси Z. Проекция вокруг `(0, 0)` -сместит план при смене пространства, а старый `_baseVb()` не включает поднятые -верхние грани и начнёт обрезать стены. - -**Что добавить в ТЗ:** - -```ts -type PlanPoint = readonly [number, number]; -type ScenePoint = readonly [number, number]; - -interface IsoCamera { - rotDeg: number; - tiltDeg: number; - xyScale: number; - zScale: number; - origin: PlanPoint; -} - -projectPlanPoint(p: PlanPoint, zUnits: number, cam: IsoCamera): ScenePoint; -unprojectFloorPoint(p: ScenePoint, cam: IsoCamera): PlanPoint; // только z=0 -clientToScenePoint(client: readonly [number, number], stageRect: DOMRectReadOnly, - view: ViewRect): ScenePoint; -projectedFrame(input: IsoFrameInput, cam: IsoCamera): ViewRect; -``` - -Нормативно определить: - -1. `projectPoint` возвращает **scene**, а не screen/client coordinates. -2. `unprojectFloorPoint` инвертирует только плоскость `z=0`; высотная грань не - имеет единственной plan-точки. -3. Pivot — одна фиксированная plan-space константа (рекомендуемо - `[NORM_W / 2, NORM_W / 2]`), а не центр viewport/content frame и не - положение курсора. Иначе появление far marker или переключение `_showFar` - сдвинет уже построенные стены без изменения их геометрии. -4. Wall height сначала переводится из общей константы в plan units, затем - применяется `zScale`; выбранные значения фиксируются ADR. -5. `fit`, pan clamp, home arrow, far-object hint и initial view используют - `projectedFrame`, включающий floor content и поднятые wall faces. -6. Projected frame не зависит от текущего zoom/pan и входит в geometry cache. - -Без этого нельзя проверить «не меняет фокус плана скачком» и «не обрезает -объекты». - -### B2 — Не задано преобразование viewport при flat ↔ iso и при входе в редактор - -**Где:** §7, §10, AC 7–9. -**Критичность:** blocker. - -Текущий `_view` хранит прямоугольник именно в координатах текущего SVG. Его -нельзя без преобразования перенести из flat scene в iso scene. Текущий -`_viewModeSnap` также хранит `cx/cy` в flat units. Требование «не менять zoom и -фокус» сейчас не имеет алгоритма. - -**Добавить нормативный алгоритм:** - -1. Перед сменой проекции получить логический центр пола: - - flat: центр `_view` уже является plan point; - - iso: центр `_view` пропустить через `unprojectFloorPoint`. -2. Построить target frame и target fit. -3. Сохранить тот же scalar zoom. -4. Спроецировать логический центр в target scene и вызвать `_applyView()` с - этим scene center. -5. Не переиспользовать raw `x/y/w/h` между видами. -6. Вход в editor выполняет тот же iso → flat переход; выход — flat → прежний - view kind. Смена пространства внутри editor сбрасывает старый snapshot по - существующему правилу. - -Предпочтение вида (`flat|iso`) и viewport — разные сущности. В localStorage -пишется только предпочтение и существующий scalar zoom; raw viewport остаётся -runtime/warm state. - -### B3 — ТЗ не совместимо с `docs/WARM-REMOUNT.md` - -**Где:** §7 «Непрерывность», §10. -**Критичность:** blocker. - -#73 переносит через `warmBoot` точный `_view`, `_viewModeSnap`, mode и -fingerprint кадра. После введения iso один и тот же `ViewRect` имеет два разных -смысла. Если новый экземпляр восстановит iso rectangle в flat mode (например, -флаг снят/истёк) либо наоборот, получится именно тот скачок/пустой кадр, который -#73 устраняет. - -**Добавить:** - -- warm viewport хранит `projection: 'flat'|'iso'` и `logicalCenter`; -- raw `_view` усыновляется только при совпадении space, projection и активного - Labs contract; -- при несовпадении восстанавливаются scalar zoom + logical center через - алгоритм B2, а не чужой rectangle; -- frame fingerprint включает effective projection и iso geometry fingerprint; -- выключение/expiry Labs никогда не может воскресить iso DOM из memo; -- отдельный smoke: iso → remount → тот же iso frame; iso → снять flag → - remount → корректный flat frame без veil/flash. - -### B4 — Правило «все попадания через unproject» технически неверно - -**Где:** §7 Pointer, §11 smoke Pointer. -**Критичность:** blocker. - -SVG сам hit-тестирует элементы внутри трансформированного ``. Room hover и -SVG opening symbols не нужно вручную unproject-ить: это даст двойное -преобразование. HTML marker также получает click как обычный DOM-элемент. -Кроме того, marker drag выполняется в Device editor, а по этому же ТЗ все -редакторы плоские; smoke «перетаскивание маркера в объёмном виде» противоречит -scope. - -**Заменить правило на:** - -- SVG/HTML interactive children используют нативный DOM/SVG hit-test; -- pan и zoom anchor работают в scene coordinates; -- `client → scene → unprojectFloor` применяется только там, где stage event - действительно должен получить plan coordinate; -- в stage 1 iso mode не создаёт/редактирует geometry и не перетаскивает - markers, поэтому editor `_svgPoint()` остаётся flat; -- тесты кликают реальные room/device/opening DOM targets и проверяют action; - отдельный unit проверяет `client → scene → floor` для будущего использования. - -### B5 — Kiosk UX противоречит фактическому DOM - -**Где:** §3, §7, AC 2/4. -**Критичность:** blocker. - -ТЗ обещает кнопку «в режиме просмотра (и в киоске) рядом с шапкой». В текущем -kiosk вся `.hdr.kioskhide` имеет `display:none`; такой кнопки физически не -будет. Одновременно §7 говорит, что скрытая панель не должна лишить пользователя -возврата в flat. - -Для скрытого stage 1 рекомендуется закрепить простой вариант: - -1. Кнопка существует только в обычном View под активным Labs. -2. Kiosk читает последнее per-space предпочтение этого браузера. -3. `hp-labs=-iso` или `hp-labs=off` — обязательный аварийный путь: kiosk сразу - становится flat и не может восстановить iso из warm memo. -4. В kiosk нет новой панели/диалога stage 1. -5. Smoke покрывает загрузку kiosk с сохранённым `iso` и возврат в flat через - URL operation. - -Если владельцу нужен переключатель прямо в kiosk, его надо отдельно поместить -в существующий long-press kiosk dialog; «рядом с шапкой» всё равно неверно. - -### B6 — Грамматика Labs содержит противоречия и ломает комбинированный hash - -**Где:** §2.2–2.4. -**Критичность:** blocker. - -Не определено: - -- кто сильнее при одновременных `?hp-labs=` и `#hp-labs=`; -- является URL полным replacement или операциями над storage; -- что делает `iso,-iso`, `off,iso`, повторный параметр; -- §2.2 требует не удалять параметр из URL, а §2.3 говорит, что `off` «очищает - и то, и другое»; -- как `#space=x&hp-labs=iso` сохраняет существующий deep link; -- что происходит при `history.back()`/`popstate`. - -**Предлагаемый точный контракт:** - -1. База — валидный набор из storage. -2. Query operations применяются слева направо, затем hash operations слева - направо; hash сильнее, потому что именно он реактивен внутри Lovelace. -3. `id` добавляет, `-id` удаляет, `off` очищает набор в этой позиции; следующие - токены снова могут добавлять. -4. Повторные `hp-labs` обрабатываются в порядке появления. -5. Если в URL был хотя бы один известный operation или `off`, итог пишется в - storage. Неизвестные значения сами по себе storage не переписывают. -6. URL никогда не переписывается механизмом Labs. Из §2.3 убрать слова об - очистке URL: `off` очищает **effective set и storage**, но остаётся видимым. -7. Hash разбирается общим helper вместе с `space`; оба порядка параметров и - percent-encoding тестируются. `_hashSpace()` не остаётся вторым regex parser. -8. `hashchange` реактивен; `popstate` перечитывает query/hash, если URL реально - сменился без reload. - -### B7 — Не определена топология side faces и смысл «нет торцов в проёме» - -**Где:** §5, AC 3/5/6. -**Критичность:** blocker. - -`wallBodiesGeometry().geom` — MultiPolygon с внешними и внутренними rings, уже -после union, junction patches и opening cuts. «Граничные рёбра» недостаточно: -нужно определить winding, outward normal, holes, culling и порядок отрисовки. -Фраза «без торцов внутри проёма» двусмысленна. При полном разрыве стены -вертикальные jamb faces по краям проёма являются корректной частью объёма; -запретить их — значит получить визуально обрезанную плёнку вместо стены. - -**Добавить:** - -- faces строятся непосредственно из rings канонического MultiPolygon после - union/cuts; исходные room edges для extrusion не используются; -- winding нормализуется один раз, outward normal учитывает outer/hole ring; -- face видима по знаку dot product normal и фиксированного view direction; -- для фиксированной камеры задаётся детерминированный stable depth order; -- opening slot создаёт две exposed jamb faces по концам разрыва — они нужны; -- запрещены face/полоса, пересекающая сам gap, и cap на floor тоннеля; -- на stage 1 дверь, окно и ворота являются full-height gaps осознанно, так как - модель не хранит высоту подоконника; -- opening никогда не вырезает coincident partition/column — сохраняется - текущий порядок union extras после room opening cuts; -- top face использует whole geometry с `fill-rule:evenodd`; -- unit fixtures включают outer ring, hole, multipolygon, T/X join, opening у - угла и coincident independent body. - -### B8 — Fallback может зациклить exception и оставить кнопку во лжи - -**Где:** §9, AC 10. -**Критичность:** blocker. - -«Вернуться в flat на этом кадре» не отвечает на вопросы: будет ли следующий -Lit render снова падать, что показывает `aria-pressed`, сохраняется ли `iso` в -localStorage и когда разрешён retry. - -**Добавить state machine:** - -- `desiredView` — сохранённое предпочтение; -- `effectiveView` — реально нарисованный `flat|iso`; -- исключение в pure geometry/iso template ловится на границе - `renderIsoScene()`, для `(space, geometryFingerprint)` ставится session latch; -- при latch effective view = flat, iso geometry больше не вызывается на каждом - HA state update; -- конфиг/layout и сохранённое предпочтение не меняются автоматически; -- кнопка отражает `effectiveView` (`aria-pressed=false`), явное повторное - нажатие или новый geometry fingerprint очищает latch и делает один retry; -- console error содержит issue, space, fingerprint и короткий reason, но без - config/entity payload; один раз на latch; -- ошибка HTML overlay projection также входит в эту границу, иначе получится - «стены flat, markers iso». - ---- - -## Существенные замечания - -### M1 — Spike ADR должен фиксировать больше, чем выбор renderer - -Сейчас D6 требует ADR, но его обязательные решения не перечислены. ADR должен -закрыть до основной реализации: - -- формулу и pivot проекции; -- camera constants, wall-height units и zScale; -- top/side fill, stroke, hatch и side shading в light/dark theme; -- ring normalization, face visibility и depth order; -- z-order floor → Glow/sun/decor → faces/top → screen-facing HTML overlays; -- осознанное правило stage 1: markers/room cards всегда выше wall faces и не - получают геометрическую occlusion; -- projected frame и flat↔iso viewport conversion; -- результат проверки SVG filter/clip/mix-blend на Chromium, Firefox, WebKit; -- причины отказа от проигравшего прототипа. - -До ADR issue остаётся в статусе spike/implementation-prep, не renderer-ready. - -### M2 — Fingerprint перечисляет не все входы iso geometry - -В §8 добавить как минимум: - -- `room_drafts` и их segment thickness; -- нормализованные `openCuts`/virtual intervals; -- canonical opening cuts; -- partitions и columns с shape/angle/diameter; -- `cell_cm`, grid pitch, coordinate scale/NORM; -- camera constants и wall-height constant; -- версию алгоритма projection/faces. - -Массивы должны сериализоваться детерминированно, числа — нормализоваться как в -существующих geometry fingerprints. Display state (`hover`, HA states, -`show_borders`) не должен инвалидировать geometry cache. `show_borders:false` -просто не рисует cached top/sides, но physics остаётся прежней. - -### M3 — `since`/`expires` требуют точной version semantics - -В проекте нет зависимости `semver`; строкового сравнения допускать нельзя. -Зафиксировать parser `major.minor.patch[-prerelease]`, fail-closed для -некорректной registry entry и инвариант `since < expires`. - -Рекомендуемое продуктовое правило: сравнивать numeric core, поэтому -`1.65.0-beta.1` уже достигает `expires: 1.65.0` и не тащит мёртвый флаг в новый -release cycle. Добавить тесты `1.64.9`, `1.65.0-beta.1`, `1.65.0`, malformed. - -### M4 — Не определён runtime owner механизма Labs - -Нужно указать, что availability flags глобальны для загруженного JS-модуля, а -effective `flat|iso` остаётся состоянием конкретной карточки/пространства. -Один module-level resolver/subscription не должен создавать по listener на -каждый render. - -`window.__hpLabs` должен иметь нормативную форму, например frozen sorted array: - -```ts -Object.freeze(['iso']) -``` - -При изменении URL property заменяется новым frozen array, все подключённые -карточки получают update. Нельзя отдавать внутренний mutable `Set`. - -### M5 — Scope `houseplan-space-card` не указан - -В репозитории есть второй renderer: `src/space-card.ts` + `src/space-render.ts`. -Текущий текст можно прочитать как требование объёмного вида для обеих карточек. - -Рекомендация для stage 1: явно записать, что `houseplan-space-card` остаётся -flat и Labs `iso` на него не влияет. Его поддержка — отдельный будущий scope. -Иначе придётся сразу заводить вторую композицию сцены, что противоречит цели -скрытого первого этапа. - -### M6 — Performance contract не совпадает с существующей инфраструктурой - -`compare.mjs` использует profile-specific budgets, noise allowance, -relative+absolute thresholds и exact same runner. Просто потребовать «≤20% по -трём полям» недостаточно; `longTask.maxSingleMs` особенно нестабилен около -нуля, а `modelReadyMs` почти не измеряет переключение renderer. - -Добавить отдельный профиль `large-house-isometric-v1`: - -- текущий benchmark harness запускает candidate bundle с `hp-labs=iso` и - переключает view; тот же harness запускает base bundle, который игнорирует - неизвестный flag и остаётся flat; -- profile id в обоих reports одинаков, runtime/browser/fingerprint проверяются - существующим fail-closed контрактом; -- отдельный reviewed budget JSON задаёт 20% relative allowance **плюс** - абсолютный noise allowance; -- обязательные метрики: first stable iso frame, view toggle, pan/zoom, - HA-state update, space switch, long-task count/total/max, heap growth, - iso-cache entries/growth, rendered devices; -- candidate-only prerelease smoke получает абсолютные ceilings; -- перед завершением этапа выполняется exact-SHA full performance workflow, а - не локальное сравнение с другой машиной. - -Фразу «flat не должен подорожать вообще» заменить на проверяемое: при -выключенном флаге iso geometry/cache/DOM отсутствуют и нет дополнительного -прохода по room/device collections; timing находится внутри noise allowance. - -### M7 — Golden coverage слишком мала для новой системы координат - -Две картинки не покрывают заявленный scope. Минимальная матрица stage 1: - -1. desktop dark: mixed walls + openings + Glow/sun + devices; -2. desktop light: theme/shading/filter parity; -3. mobile portrait или узкий kiosk: fit, marker/label alignment, no clipping; -4. `show_borders:false`: стены не нарисованы, room fill/Glow сохраняются; -5. remount/toggle sequence проверяется smoke, а финальный кадр — golden при - необходимости. - -Существующие flat baselines действительно не принимаются заново, если diff не -нулевой. Новые baselines принимаются только из полного Linux CI artifact по -действующему HP-QA-01 контракту. - -### M8 — A11y toggle contract неполон - -Для кнопки добавить: - -- `aria-pressed="true|false"` по `effectiveView`; -- стабильный accessible name «Объёмный вид» / `Volumetric view`; -- focus остаётся на той же кнопке после переключения; -- active visual state не кодируется только цветом; -- DOM/tab order устройств и room actions совпадает с flat; -- stage 1 не добавляет projection animation: swap атомарный. Если анимация - будет добавлена через #82, `prefers-reduced-motion` делает её мгновенной. - ---- - -## Замечания к тестам и формулировкам - -### T1 — «innerHTML до и после ветки» нужно сделать воспроизводимым - -Обычный тест не может сравнить текущий commit с кодом до ветки. Разделить -контракт: - -- в одном candidate build сравнить no-param и unknown-param: нет iso nodes, - нет дополнительных WS/HTTP и config/layout writes; -- golden гарантирует нулевой diff существующих flat scenes между revisions; -- unit spy подтверждает, что iso geometry builder не вызывался; -- чтение собственного Labs localStorage не считать сетевым/сторным изменением, - но при отсутствии URL оно не должно переписывать ключ. - -### T2 — Мутанты должны быть исполнимыми - -Пункт «отдельная формула проекции HTML» нельзя надёжно поймать текстовым -поиском. Нормативный mutant: внести controlled offset только в overlay mapping; -smoke должен увидеть расхождение anchor больше 1 CSS px. Для cache mutant тест -меняет geometry in-place без `_cfgEpoch`; iso faces обязаны обновиться. Для -layer-copy mutant тест проверяет upper bound DOM face count как `O(E)`. - -Для каждого из пяти mutants сохранить команду/patch id и имя краснеющего теста -в PR/issue evidence; ручной тезис «проверено» недостаточен. - -### T3 — Opening wording - -В AC 6 заменить «без швов и торцов внутри проёма» на: - -> Проём является full-height gap. Внутри gap нет wall top/side полосы; -> вертикальные jamb faces на двух границах masonry разрыва являются ожидаемыми. - -Это снимает конфликт с B7 и делает golden однозначным. - -### T4 — Первый запуск и сохранённое предпочтение - -В §9/§10 уточнить: - -- без записи `houseplan_card_view_v1[space]` effective view всегда flat, даже - при активном Labs; -- toggle в обычном View пишет `flat|iso` per space; -- при неактивном/expired flag сохранённое `iso` игнорируется, не меняет DOM и - не попадает в warm memo; -- вход/выход editor не перезаписывает предпочтение; -- fallback B8 не перезаписывает предпочтение автоматически. - -### T5 — Backlog — канонический источник - -Issue #89 сейчас говорит «черновик продуктового и технического решения», тогда -как файл говорит «готово к реализации». По `AGENTS.md` Issue/Project являются -каноническими. После принятия новой редакции: - -- добавить в body issue ссылку на stage 1 spec как нормативную; -- синхронизировать scope/acceptance criteria issue с утверждённой редакцией; -- оставить Project `Todo` до фактического начала, затем перевести в - `In progress`; -- не закрывать #89 после одного spike ADR: закрытие только после всех AC этапа. - ---- - -## Рекомендуемая новая структура нормативных разделов - -Чтобы не раздувать основной текст, достаточно добавить четыре подраздела: - -1. **§4.4 Coordinate spaces and viewport** — B1, B2. -2. **§5.1 Wall-face topology and visual tokens** — B7, M1. -3. **§7.1 Native hit testing and warm continuity** — B3, B4, B5. -4. **§2.2.1 Labs operation precedence and version lifecycle** — B6, M3, M4. - -Остальные замечания можно встроить в §8–§13. - -## Definition of Ready после следующей итерации - -ТЗ можно считать готовым к реализации, когда: - -- [ ] определены plan/scene/client spaces, camera pivot/zScale и projected frame; -- [ ] записан алгоритм flat↔iso viewport conversion; -- [ ] обновлён warm-remount contract; -- [ ] исправлено pointer rule и убран iso marker-drag smoke; -- [ ] выбран однозначный kiosk escape contract; -- [ ] полностью определена Labs grammar и expiry semantics; -- [ ] определены ring/face/jamb/depth rules; -- [ ] определена fallback state machine; -- [ ] ADR имеет обязательный список решений; -- [ ] fingerprint содержит все входы; -- [ ] указан scope `houseplan-space-card`; -- [ ] создан исполнимый performance profile/budget plan; -- [ ] расширена golden/a11y/mutant matrix; -- [ ] issue #89 ссылается на утверждённое ТЗ и не противоречит ему. - -После этого оценка stage 1 остаётся **L/XL с высоким риском**, но работа станет -декомпозируемой и проверяемой; менять выбранную продуктовую концепцию не нужно.