Files
houseplan-card/legacy/specs/173-unified-wall-tool.md
Claudeandclaude[bot] df46fd1c3e docs(hygiene): ТЗ выпущенных задач без живых ссылок — в legacy/specs (#682)
Волна 5 эпика #674, перенос ТЗ (класс C). Из 240 файлов `docs/specs/` в
`legacy/specs/` уехали 219: на них не ссылается ни один живой файл (код,
тесты, скрипты, workflow, документы вне архива и ревью). Остались 21 ТЗ —
на которые ссылаются код, ADR, ISOMETRIC, SUN, RADAR, LIGHT (`docs/specs/067`),
DECOR-EDITOR, support-relay, и те, на которые ссылаются они сами; README
каталога объясняет, где искать остальное. Открытых issue с файлом ТЗ среди
перенесённых нет. Относительные ссылки перенесённых файлов переписаны
(`../X` → `../../docs/X`, соседние оставшиеся ТЗ → `../../docs/specs/…`) —
все 26 резолвятся. Попутно: битая ссылка в
`089-isometric-view-stage1.md:8` на удалённый `089-isometric-view.md` —
теперь команда `git show` по истории. Строка в `legacy/README.md`.

Issue: #682
User-Visible: no
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
2026-09-27 22:10:46 +00:00

48 KiB
Raw Permalink Blame History

Issue #173 — единый инструмент рисования стен и предложение комнаты по замыканию

Частично заменено #306. Единый инструмент «Стены» остаётся каноническим, но все положения этого документа об отдельном инструменте «Граница», virtual/open-wall UX и записи open_spans/open_to больше не действуют. Нулевая толщина создаётся и редактируется как обычная стена по docs/specs/306-zero-thickness-walls.md; модель v9 хранит только cm:0.

  • Issue: https://github.com/Matysh/houseplan-card/issues/173
  • Редакция: первая редакция для независимого ревью; статус определяется только метками issue
  • Тип / приоритет: feature / P1
  • Оценка: пользовательская ценность 9/10; ценность для разработки 6/10; сложность/риск 9/10
  • Область: desktop-first редактор Плана, рисование стен, planar topology, создание и разделение комнат, Undo/Redo, i18n и release-проверки
  • Модель данных: существующие rooms, room_drafts, partitions и walls; без новой schema, backend API и миграции
  • Связано: #137, #138, #141, #150, #172, docs/SCOPE.md, docs/UX-MODES.md, docs/CANVAS.md, docs/WALL-THICKNESS.md, docs/TOUCH-SUPPORT.md

1. Сценарий и персона

Персона: администратор дома, который рисует новый план либо достраивает существующий в desktop-редакторе Плана.

Момент: пользователь хочет провести стены. Сегодня перед первым кликом ему надо решить внутренний вопрос модели House Plan: это будущий контур комнаты или независимая перегородка. При ошибочном выборе приходится менять инструмент либо перерисовывать геометрию.

Задача поддерживает J4 и J6 из docs/SCOPE.md: встроенный GUI должен позволять нарисовать и поддерживать правдивый план без знания внутренней схемы данных и без внешнего редактора.

2. Что человек увидит до и после

До: в панели есть отдельные «Контур комнаты» и «Перегородка». Первый ждёт замыкания своего draft, второй заканчивается после одного сегмента. Замыкание по старым стенам, нескольким рёбрам, T- и X-пересечениям обычно не предлагает комнату.

После: в панели есть один инструмент «Стены». Пользователь ведёт одну ломаную столько, сколько нужно. Если она остаётся открытой и человек меняет инструмент или выходит из редактора, нарисованное остаётся обычными перегородками. Если новый сегмент замкнул одну или несколько допустимых областей, House Plan предлагает создать комнаты; отказ оставляет новые линии стенами.

Никакой отдельной кнопки «Завершить» и клавиши Enter не появляется.

3. Проблема и подтверждённое текущее состояние

  1. MarkupTool содержит разные draw и partition; toolbar показывает две кнопки, а warm viewport способен восстановить оба значения.
  2. draw сохраняет каждый завершённый сегмент в room_drafts; partition создаёт одну запись partitions после двух кликов и очищает _path.
  3. _markupClick() предлагает комнату при возврате к собственной первой точке, Ctrl/Cmd closure либо в узком контракте #138: начало и конец лежат на одном непрерывном room-owned отрезке.
  4. plan-snap-overlay.ts уже собирает стены комнат, сохранённые drafts и partitions, разрешает endpoint-first и line-snap. Он не строит planar faces.
  5. _keepClosedAsPartitions() умеет преобразовать только собственный замкнутый draft. Он не знает provenance рёбер, пришедших из старой стены, T/X-узла или нескольких новых граней.
  6. Отдельный Split уже сохраняет большую часть исходной комнаты с прежним id и метаданными, а меньшую открывает как новую комнату.
  7. #141 объединяет физические тела T/endpoint-стыков, но намеренно не создаёт persisted node для случайного X. В #173 X становится вычисляемым узлом только для face topology; persisted записи и физический join-контракт #141 от этого не меняются.

Следовательно, задача не сводится к удалению кнопки: нужен детерминированный поиск новых bounded faces и безопасная транзакция между draft, partitions и rooms.

4. Решения владельца

Решения зафиксированы 18.08.2026: https://github.com/Matysh/houseplan-card/issues/173#issuecomment-5330235454

  1. Специальных кнопок завершения и Enter нет. Смена инструмента, пространства либо выход из редактора завершает открытую цепочку; она становится обычными partitions и больше не возобновляется как draft.
  2. Esc/Ctrl+Z убирает последнюю точку. Pinch, pan и pointercancel никогда не завершают цепочку и не добавляют геометрию.
  3. Сохраняется действующий overlap-контракт: partial overlap запрещён, exact duplicate не предлагается, полное вложение комнат разрешено. Devices, decor, openings, columns и внутренние стены не блокируют предложение сами по себе.
  4. При разделении существующей комнаты большая часть сохраняет id, имя, HA area, настройки и позиции; диалог настраивает меньшую новую часть. Отказ не меняет исходную комнату.
  5. Если один сегмент замкнул несколько допустимых областей, они предлагаются последовательно от меньшей площади к большей; решение по каждой независимо.
  6. Endpoint, T и геометрический X являются вычисляемыми узлами face graph. Collinear/coincident edges дедуплицируются. Opening/open-span gap не замыкает область через отсутствующую стену.
  7. Детектор работает только после добавления пользователем нового завершённого сегмента. Пассивных предложений после загрузки, move/delete/Align/толщины нет; session/global «не предлагать» не вводится.
  8. Отдельный Partition удаляется из toolbar. Отдельный Split остаётся доступен; возможная дедупликация Split — отдельная будущая задача.

5. Scope

В #173 входят:

  1. один публичный инструмент «Стены» вместо кнопок «Контур комнаты» и «Перегородка»;
  2. сохранение активной открытой цепочки как crash-safe room_draft во время рисования и её явное завершение по смене контекста;
  3. преобразование завершённой незамкнутой цепочки в ordinary partitions без потери координат и толщин;
  4. planar graph из solid room-wall intervals, drafts, partitions и нового сегмента, включая endpoint, T, X и collinear atomization;
  5. поиск только новых bounded faces, возникших из последнего принятого сегмента;
  6. предложение standalone, adjacent, nested и clean-split комнат по принятому overlap-контракту;
  7. последовательная очередь нескольких кандидатов;
  8. действия «Создать комнату», «Оставить стенами» и Cancel без частично применённой конфигурации;
  9. сохранение provenance и толщины новых/существующих рёбер;
  10. совместимость старых drafts, partitions и warm session tool;
  11. Undo/Redo, optimistic save/conflict, limits, touch safety floor;
  12. unit, production-bundle smoke, golden и performance coverage;
  13. i18n RU/EN, пользовательские/архитектурные документы и оба changelog.

6. Non-scope

В задачу не входят:

  • удаление или изменение отдельного Split;
  • автоматическое предложение комнат при загрузке, импорте, reload, move, drag, delete, Resize, Align, изменении толщины, Undo/Redo или внешнем config update;
  • фоновая миграция всех старых room_drafts в partitions;
  • persisted node, junction, face либо новый storage/config field;
  • переписывание или дробление старых partitions/drafts ради вычисляемого X/T;
  • изменение snap tolerance, endpoint-first порядка и overlay-радиусов #137;
  • изменение bounded physical join, cap/mitre/bevel и renderer semantics #141;
  • создание комнаты через opening/open-span gap;
  • колонны, decor, backdrop, furniture и device markers как рёбра graph;
  • новая session/global настройка «не предлагать комнату»;
  • новый Finish control, Enter/double-click shortcut либо touch-hover parity;
  • удаление, перенос или автоматический rebinding устройств;
  • backend schema/API, HA service calls, новые зависимости и security permissions;
  • принятие golden baseline вне reviewed Linux artifact.

7. Термины и базовые инварианты

  • Активная цепочка — текущая полилиния инструмента «Стены», хранимая во время работы как один room_draft с per-segment cm.
  • Исходное ребро — room-wall solid interval, segment draft/partition либо новый segment до разрезания пересечениями.
  • Атомарное ребро — ненулевой отрезок между соседними вычисляемыми graph vertices после endpoint/T/X/collinear atomization.
  • Face — простой bounded polygon, найденный обходом half-edges. Внешняя бесконечная область face не является.
  • Новая face — face, отсутствовавшая до последнего принятого сегмента и содержащая хотя бы одно атомарное ребро этого сегмента.
  • Consumed atom — часть активной цепочки, которая после подтверждения стала границей созданной комнаты. Остальные части активной цепочки становятся independent partitions.

Инварианты:

  1. Topology вычисляется из координат и canonical cuts, не из SVG DOM, толщины stroke либо пикселей.
  2. Render/hover/topology не мутируют config.
  3. Существующая geometry не удаляется и не переписывается по inference.
  4. Одна пользовательская координата после snap имеет одну graph-вершину; record order, id и direction не меняют результат.
  5. Комната создаётся только после явного подтверждения в диалоге.
  6. Ошибка/Cancel сохраняет уже нарисованные стены как редактируемый draft, но не создаёт частичную комнату.

8. UX единого инструмента

8.1 Toolbar и начало цепочки

  1. Кнопка Partition удаляется из Plan toolbar.
  2. Текущая кнопка draw становится публичным инструментом «Стены» и остаётся neutral/default tool редактора.
  3. Первый click/tap ставит только стартовую точку; одиночная точка не пишется.
  4. Каждый следующий отличный resolved point создаёт segment с текущим cm, сохраняет его в активный room_draft и продолжает цепочку.
  5. Shift/45°, grid, endpoint/line snap, preview, size badge и limits сохраняют действующие контракты.
  6. Повторный клик по уже активной кнопке «Стены» ничего не завершает.
  7. Existing Reset не становится Finish: он очищает локальное продолжение по действующему reset-контракту, не конвертируя draft в partitions.

8.2 Завершение незамкнутой цепочки

Завершение запускают только:

  • переход с «Стены» на другой Plan tool;
  • переход в View, Devices или Background;
  • смена пространства/этажа, пока активна цепочка.

Reload, route remount/disconnect, external config adoption, pan, pinch, pointercancel, suppressed synthetic click, Escape, Ctrl/Cmd+Z и повторный выбор «Стены» завершением не являются.

Если есть только стартовая точка, она просто отбрасывается. Если есть один или больше segment, завершение одной geometry-транзакцией:

  1. создаёт ordinary partitions из сегментов с теми же endpoints и cm;
  2. удаляет только соответствующий активный room_draft;
  3. очищает _path, resume/session references и preview;
  4. после reload не предлагает эту цепочку для продолжения;
  5. записывается именованной Undo/Redo-командой и сохраняется до фактической смены контекста.

До добавления каждого segment проверяется, что итоговая конвертация не превысит MAX_PARTITIONS/общие limits. Поэтому смена инструмента не может оставить пользователя в ловушке «переключение невозможно из-за позднего лимита».

8.3 Замыкание и диалог

После каждого принятого ненулевого segment строится topology delta. Если новых eligible faces нет, рисование продолжается. Если они есть:

  1. terminal segment остаётся безопасно сохранённым в active draft;
  2. открывается стандартный диалог новой комнаты;
  3. при нескольких faces диалог показывает понятный прогресс i из N и идёт в стабильном порядке §9.5;
  4. Save/Create room запоминает настройки текущей face;
  5. Оставить стенами отклоняет только текущую face;
  6. после последнего решения весь batch применяется одной geometry-транзакцией;
  7. цепочка завершается: consumed atoms становятся room boundaries, остальные новые atoms/segments — partitions, active draft удаляется.

Нового предварительного confirm и silent-mode нет.

8.4 Cancel, Escape и ошибка

  • Cancel, X или Escape в room dialog отменяет весь ещё не применённый batch, включая ранее введённые в очереди ответы, и возвращает terminal active draft в открытое редактируемое состояние.
  • Уже сохранённый terminal segment не пропадает; комнаты/partitions из pending batch не создаются.
  • Validation/limit/boolean/save failure действует так же: no partial room, no partial conversion, draft остаётся восстанавливаемым.
  • Esc вне диалога и Ctrl/Cmd+Z сохраняют действующую семантику удаления последней точки/segment до shared history.
  • Conflict с удалённым config проходит текущий reload/conflict path без дубликатов rooms/partitions.

9. Контракт planar graph и новых faces

9.1 Входная геометрия

Graph получает immutable snapshot:

  • room boundary axes после canonical opening/open-span cuts #137/#138;
  • все валидные inactive room_drafts;
  • все валидные partitions;
  • active draft до candidate segment для before и с ним для after.

Active/saved draft и partition не получают room opening cuts. Columns, wall thickness offsets, physical body edges, decor, devices и paper не входят. Malformed/non-finite/zero-length sources игнорируются fail-safe и не ломают валидный connected component.

9.2 Atomization и vertices

  1. Endpoint каждого source segment — vertex.
  2. Endpoint, точно лежащий на другом solid segment, создаёт T-vertex.
  3. Proper X-intersection двух внутренних интервалов создаёт derived vertex в обоих рёбрах, даже если пользователь там не кликал.
  4. Collinear overlap режется по всем конечным точкам и дедуплицируется в undirected atomic edge; provenance всех источников сохраняется.
  5. Near-miss вне canonical epsilon не соединяется.
  6. Opening/open-span gap удалён до atomization и не получает искусственных bridge/endpoints.
  7. Persisted old records не дробятся и не переписываются. Atomization является вычисляемой projection; исключение — новый active segment при финальной принятой конвертации §10.4.

9.3 Face traversal и canonical identity

Outgoing half-edges у vertex сортируются по геометрическому углу со стабильным tie-break. Обход каждого ещё не посещённого half-edge даёт candidate ring.

Face допускается только если:

  • ring имеет минимум три разные vertices и положительную площадь выше canonical epsilon;
  • ring simple после удаления повторов;
  • все boundary atomic edges существуют в graph;
  • это bounded, а не внешняя face;
  • canonical ring identity одинакова при cyclic shift, reverse winding, перестановке records, id и направлении endpoints.

Точный алгоритм half-edge/DCEL свободен, но pairwise «нашли любой цикл» без выделения faces запрещён: он создаёт составные внешние циклы поверх внутренних.

9.4 Delta: только то, что замкнул пользователь

До append строится или берётся cached before component; после append — after. В очередь попадают только faces, которые:

  1. отсутствуют по canonical identity в before;
  2. присутствуют в after;
  3. содержат хотя бы один atomic interval нового segment;
  4. проходят room eligibility §10.

Поэтому уже существующий closed partition ring не открывает диалог при reload или при несвязанном клике. Detector вызывается только после commit candidate segment; pointermove/hover строят прежний локальный preview без face traversal.

9.5 Порядок и дедупликация кандидатов

Exact room polygon duplicate исключается. Остальные eligible faces дедуплицируются по canonical identity и сортируются:

  1. площадь по возрастанию;
  2. canonical ring key как стабильный tie-break.

При равной площади DOM order, room id, source record order и winding не меняют очередь. Отклонение одной face не удаляет из очереди другую.

10. Создание комнат и provenance стен

10.1 Standalone, adjacent и nested room

Для face, которая не является clean split:

  • partial overlap с любой существующей room polygon отклоняется текущей validation/toast семантикой;
  • exact duplicate не предлагается;
  • полное вложение в существующую комнату или охват существующей вложенной комнаты разрешены как действующий island-room контракт;
  • devices, decor, openings, columns и independent walls не блокируют face;
  • Save создаёт обычный room.poly с выбранными name/HA area/settings;
  • существующие room walls, drafts и partitions, использованные как часть boundary, остаются на месте и не удаляются по inference.

10.2 Clean split существующей комнаты

Если new wall path даёт валидное разбиение одной room polygon на две простые части по действующему splitRoomPath/area invariant:

  1. диалог относится к меньшей части;
  2. большая сохраняет исходные id, name, HA area, settings, label/device positions;
  3. при равной площади используется действующий детерминированный tie-break;
  4. Save применяет тот же materialize/normalize/open-span порядок, что Split;
  5. «Оставить стенами» не меняет room polygon или metadata;
  6. отдельный Split остаётся pixel/behavior-compatible.

Если graph face находится внутри комнаты, но не образует представимое clean split с одной простой remainder polygon, она рассматривается по разрешённому nested-room контракту либо отклоняется текущей partial-overlap validation; parent не переписывается эвристически.

10.3 Несколько faces

Room settings для каждого принятого кандидата буферизуются до конца очереди. Одна final transaction валидирует batch повторно на исходном snapshot и только затем одновременно:

  • создаёт принятые rooms;
  • применяет clean splits;
  • конвертирует не consumed новую геометрию в partitions;
  • удаляет active draft;
  • обновляет walls/open spans и rebuild устройств по действующим контрактам;
  • создаёт одну именованную Undo/Redo-команду и один config save.

Если комбинация независимых ответов образует partial overlap, disconnected remainder либо превышает limits, batch не применяется: пользователь получает validation error, а active draft остаётся. Частичное сохранение первых диалогов запрещено.

10.4 Новые рёбра, старые рёбра и толщина

  1. Existing room wall сохраняет свою authoritative thickness.
  2. Existing partition/draft сохраняет запись, id, selection/history identity и собственный cm; создание room по его оси не удаляет объект.
  3. Каждый active-draft segment сохраняет cm, выбранный в момент клика.
  4. Если только часть нового segment consumed room boundary, segment можно разрезать в рамках явной подтверждённой операции по graph vertices: consumed atoms переходят в room wall intervals, остальные становятся одной или несколькими partitions с исходным cm.
  5. Полностью consumed active atoms не дублируются independent partition.
  6. При полном отказе от faces исходные active segments становятся partitions без лишнего дробления только ради derived X/T.
  7. Room-wall normalization #150/#172, joined physical geometry #141, clean floor, Glow, sun, static и hidden Iso используют существующие canonical consumers; topology detection не вводит второй renderer.

11. Модель данных, compatibility и миграция

Новых persisted полей нет.

  • rooms[].poly, walls, room_drafts, partitions, openings и open_spans сохраняют текущую schema;
  • backend validation, storage version, import/export и Python model не меняются;
  • старые partitions остаются обычными selectable objects;
  • старые room drafts остаются видимыми и могут быть продолжены через единый инструмент; автоматически при чтении они не конвертируются;
  • legacy warm viewport tool partition нормализуется в draw/«Стены» без создания geometry;
  • reload/remount во время активного рисования восстанавливает безопасный draft, а не считает технический disconnect выходом из редактора;
  • новая версия пишет только уже валидные старой schema rooms/partitions, поэтому downgrade не требует data rollback;
  • topology, hover и dialog preview не пишут config/layout/localStorage.

12. UX, i18n, accessibility и touch

UX и i18n

RU/EN обязательно обновляют:

  • toolbar label Контур комнаты / Room outline → Стены / Walls;
  • title и hints: одна цепочка, Shift/45°, Esc/Ctrl+Z, предложение комнаты;
  • удаляется toolbar-only copy отдельного Partition, но слово «Перегородка» сохраняется как label существующего selectable object/properties;
  • при queue N > 1 диалог показывает локализованный Комната i из N / Room i of N либо эквивалентный понятный progress;
  • существующие «Оставить стенами», validation toasts, room fields и Save/Cancel переиспользуются, где их смысл совпадает.

Нового Finish/Enter control, setting или help toggle нет.

Accessibility

  • удалённая кнопка больше не остаётся в tab order или ARIA toolbar;
  • «Стены» получает корректные title/aria-label и pressed state;
  • room queue остаётся modal, сохраняет focus trap/return и объявляет i из N в title/status без отдельного плавающего слоя;
  • keyboard Escape/Undo не меняют смысл;
  • forced colours/reduced motion не получают новой анимационной зависимость.

Touch

Touch editor: best effort / intentionally degraded. Desktop — reference.

  • tap без pointermove повторно решает snap/topology после commit;
  • hover parity не обещается;
  • смена инструмента/режима завершает цепочку так же, как desktop;
  • pinch, pan, second pointer, pointercancel и suppressed synthetic click не завершают цепочку и не создают segment/room;
  • View и kiosk остаются fully supported и не получают новых interactions.

13. Архитектурный и performance-контракт

  1. Face detection выделяется в чистый модуль (wall-face-graph.ts или эквивалент), независимый от Lit/DOM и пригодный для exhaustive unit tests.
  2. plan-snap-overlay остаётся источником canonical architectural axes/cuts либо делит с detector общий pure collector; второй opening resolver запрещён.
  3. Graph строится для connected component нового segment либо использует bounded structural cache. На каждом pointermove full graph не строится.
  4. Structural key включает coordinates/provenance/cuts/space/grid epsilon, но не HA state, theme, hover, cursor, glow tick и animation.
  5. Atomization и half-edge traversal должны иметь документированную сложность; на валидном input ожидается не хуже O((E + I) log E) либо эквивалентного bounded spatial подхода, где I — реальные intersections component.
  6. Cache bounded и инвалидируется существующим geometry lifecycle.
  7. Нельзя ослаблять large-house-plan-snap-v1 budgets. Accepted-click/face traversal получает измерение в этом профиле либо отдельный профиль с теми же original timing/heap/DOM invariants.
  8. Boolean/graph failure fail-safe: draft остаётся, room не создаётся, config не мутирует. Исчезновение стены или частичная комната как fallback запрещены.
  9. Новых network calls, HA services, dynamic code и внешних зависимостей нет.

14. Acceptance criteria

  • AC1 (unit + smoke + golden; разработчик): Plan toolbar содержит одну кнопку Стены/Walls вместо Room outline + Partition; legacy warm tool partition безопасно открывает Walls, отдельный Split остаётся доступен и сохраняет прежний сценарий.
  • AC2 (unit + smoke; разработчик): open chain из 1…N segments сохраняет per-segment endpoints/cm как active draft; смена Plan tool, mode или space атомарно создаёт ordinary partitions, удаляет draft и исключает auto-resume. Одна стартовая точка не пишет config.
  • AC3 (smoke; разработчик): Esc/Ctrl+Z убирают последнюю точку, Reset не становится Finish, а pan/pinch/second pointer/pointercancel/suppressed click, reload и remount не завершают цепочку и не создают лишнюю geometry.
  • AC4 (unit; разработчик): graph atomizes endpoint, exact T, proper X и collinear overlaps; near-miss и opening/open-span gaps не соединяются; malformed/zero input не портит valid component. Persisted old records не мутируют.
  • AC5 (unit; разработчик): half-edge traversal возвращает только simple bounded faces с canonical identity; результат и order не зависят от ids, record order, endpoint direction, winding и cyclic start. Compound cycles и внешняя face исключены.
  • AC6 (unit + smoke; разработчик): dialog появляется только для face, добавленной последним committed segment и содержащей его atom. Existing closed ring, reload, move/delete/Align/Resize/thickness/Undo/Redo и unrelated segment не вызывают пассивное предложение.
  • AC7 (unit + smoke; разработчик): новая область замыкается собственной цепочкой, несколькими room edges, saved draft/partition, endpoint, T и X. Adjacent scenario #138 остаётся зелёным; gap через opening/open-span не предлагает room.
  • AC8 (unit + smoke; разработчик): exact duplicate room не предлагается, partial overlap отклоняется без mutation, full nesting в обе стороны сохраняет island-room contract; devices/decor/openings/columns внутри не блокируют valid candidate.
  • AC9 (unit + smoke; разработчик): clean wall-to-wall split сохраняет metadata/id/positions большей части и настраивает меньшую; отказ оставляет original room bit-equivalent. Отдельный Split проходит прежние regression tests.
  • AC10 (unit + smoke; разработчик): несколько faces идут area-ascending с stable tie-break и progress i/N; Create/Keep решения независимы, но final apply один. Cancel/error/conflict/invalid combination оставляют terminal draft без частичных rooms/partitions.
  • AC11 (unit + smoke; разработчик): accepted batch удаляет только active draft; existing walls/drafts/partitions не удаляются. Consumed active atoms получают правильные room-wall cm, unconsumed части становятся partitions с исходным cm; полный отказ сохраняет исходные segments без лишнего split.
  • AC12 (unit + smoke + golden; разработчик): Plan/View/static/hidden Iso, clean floor, Glow и sun используют существующую canonical physical geometry #141/#150/#172; dialog/topology/render не переписывают config и не создают визуального jump/double wall.
  • AC13 (unit + smoke; разработчик): conversion и accepted room batch являются именованными Undo/Redo-командами; partition/room limits проверяются до mutation; save conflict не оставляет duplicate/lost geometry.
  • AC14 (unit + backend/schema review; разработчик/ревьюер): schema, backend, import/export, старые plans/drafts/partitions и downgrade остаются совместимыми без migration/write-on-read; новых HA/network/security paths нет.
  • AC15 (performance + code review; разработчик/ревьюер): face traversal не запускается на pointermove/hover/HA state; large-house snap profile и новый accepted-click measurement проходят неизменные timing, heap, cache и DOM budgets.
  • AC16 (smoke + code review; разработчик/ревьюер): touch safety floor и View/kiosk не регрессируют; no-hover tap может выполнить тот же commit, но hover parity редактора не обещается.
  • AC17 (typecheck + unit + build + documentation review; разработчик): implementation-loop gates зелёные, три bundle-копии побайтно одинаковы, RU/EN i18n, оба changelog и перечисленные документы обновлены в одном user-visible implementation commit.

15. План автотестов и гейтов

15.1 Unit

Добавить отдельный pure suite, предпочтительно test/wall-face-graph.test.mjs, плюс lifecycle tests:

  1. endpoint/T/X/collinear atomization, reverse/order/permutation invariance;
  2. near-miss, gaps, zero/non-finite, duplicate axes;
  3. rectangle, concave face, nested faces, compound cycles и unbounded face;
  4. before/after delta и provenance последнего segment;
  5. several faces area order и equal-area tie;
  6. exact duplicate, partial overlap, inner/outer nesting;
  7. clean split metadata и bit-equivalent rejection;
  8. active draft → partitions, per-segment cm, limit reservation;
  9. consumed/unconsumed atom conversion без mutation старых records;
  10. pending batch Cancel/error/conflict/Undo/Redo;
  11. legacy partition tool normalization и old draft compatibility;
  12. structural cache invalidation и immutability.

Минимум один topology test должен краснеть на origin/dev без detector, а ревьюер обязан проверить его mutation discipline.

15.2 Targeted production-bundle smoke

Добавить demo/smoke_unified_wall_tool.mjs либо эквивалент, который через реальный UI:

  1. подтверждает одну toolbar-кнопку Walls и наличие Split;
  2. рисует open polyline, меняет tool/mode/space и проверяет partitions/no draft;
  3. проверяет Esc/Undo и pointer safety;
  4. создаёт room собственным loop, adjacent multi-edge path и older partition;
  5. создаёт T и X closure без persisted rewrite старых records;
  6. разделяет room wall-to-wall, проверяет original metadata и Keep walls;
  7. проводит segment, замыкающий две faces, проходит progress/order и mixed Create/Keep;
  8. отменяет queue и получает исходный active draft без partial room;
  9. выполняет reload/remount и safe resume;
  10. сравнивает Plan/View/static/hidden Iso signatures и no-write render.

Named smoke запускается локально перед S7-code-review по актуальному процессу.

15.3 Golden

Golden matrix получает минимум:

  • light/dark Plan toolbar без Partition;
  • active Walls chain и T/X snap topology;
  • room queue 1/N и N/N;
  • before/after open-chain conversion;
  • View/static/hidden Iso результата с adjacent/split room.

Existing unrelated baselines не принимаются автоматически. Любой baseline — только через golden:accept -- --reviewed по полному Linux artifact.

15.4 Performance и backend

  • расширить large-house-plan-snap-v1 accepted-click diagnostics либо добавить сопоставимый large-house-wall-face-v1 без ослабления исходных budgets;
  • измерить no-op segment, one-face и multi-intersection component;
  • подтвердить отсутствие traversal на серии pointermove и bounded cache growth;
  • backend не меняется; отдельный Python test не требуется, но Linux Validate остаётся release gate.

15.5 Implementation loop

Во время реализации:

npm run typecheck
npm test
npm run build

После build три bundle-копии синхронизируются побайтно. Named smoke выполняется перед передачей в code review. Полный golden/performance/backend suite — на предрелизном этапе и Linux CI по действующему процессу.

16. План реализации

  1. Выделить pure architectural source collector/face graph с provenance.
  2. Добавить before/after delta и room eligibility/classification.
  3. Свести toolbar/session state к одному Walls tool и legacy normalization.
  4. Реализовать безопасное finish active draft → partitions на смене контекста.
  5. Ввести pending room-face queue и atomic batch commit.
  6. Переиспользовать current room creation/Split/wall normalization для standalone/nested/split cases.
  7. Добавить unit, targeted smoke, golden fixtures и performance diagnostics.
  8. Обновить i18n, документы, changelog и три bundle-копии.

Точные имена helpers и раскладка файлов не являются продуктовым контрактом.

17. Release-артефакты

Изменение пользовательское. Implementation commit имеет User-Visible: yes и в том же коммите обновляет:

  • docs/CHANGELOG.md;
  • docs/CHANGELOG.ru.md;
  • docs/USER-GUIDE.ru.md — единый Walls flow, finish и room proposal;
  • docs/UX-MODES.md — Plan toolbar и independent walls;
  • docs/CANVAS.md — graph/snap/face topology;
  • docs/ARCHITECTURE.md — session draft, planar graph и batch transaction;
  • docs/WALL-THICKNESS.md — provenance consumed/unconsumed edges;
  • docs/TESTING.md — unit/smoke/golden/performance coverage;
  • docs/TOUCH-SUPPORT.md только если implementation уточняет documented degradation;
  • docs/STATUS.md после фактической реализации;
  • RU/EN i18n;
  • golden matrix/fixtures без непроверенного baseline;
  • три поставляемые bundle-копии.

Перед бетой: exact-SHA Linux Validate, полный smoke-suite, reviewed golden, performance smoke/Full Performance и зелёный code review. Изменение проходит beta до stable.

18. Риски и снижение

Риск Вероятность / ущерб Снижение
Cycle finder примет составной цикл вместо face высокая / высокий half-edge face traversal, compound/unbounded units
X/collinear epsilon создаст ложную комнату средняя / высокий canonical atomization, near-miss/gap matrix
Batch частично изменит несколько rooms средняя / высокий buffered decisions, one revalidation + one transaction
Split потеряет id/HA area/devices средняя / высокий reuse current Split path, metadata regression smoke
Active draft исчезнет при Cancel/remount средняя / высокий terminal segment persisted first, no partial commit
Consumed segment потеряет внешнюю часть средняя / высокий provenance atoms, consumed/unconsumed conversion tests
Existing partition удалится как «лишний» низкая / высокий standing no-delete-on-inference invariant
Finish превысит partition limit при смене tool средняя / высокий reserve/check capacity before accepting each segment
Full graph попадёт в pointermove средняя / высокий click-only API, benchmark diagnostics and cache assertions
Toolbar cleanup сломает old warm state средняя / средний normalize legacy partition → Walls unit/smoke
Touch gesture сохранит стену низкая / высокий pointercancel/pinch/suppressed-click smoke
Golden изменятся шире editor toolbar средняя / средний matrix audit, reviewed Linux acceptance only

19. Откат

Откат — revert одного user-visible implementation commit вместе с tests, docs, i18n, changelog и bundles. Новые rooms и partitions используют старую schema и останутся читаемыми; data migration/rollback не нужны. После отката вернутся две кнопки и прежний ограниченный closure flow.

Красный topology/smoke/golden/performance gate блокирует выпуск. Feature flag, ослабление budget или удаление теста как способ отката не используются.

20. Принятые технические предположения — можно менять без продуктового ревью

  1. Рекомендуется half-edge/DCEL-подобный pure representation; другой алгоритм допустим при выполнении canonical face AC.
  2. Graph epsilon переиспользует существующую geometry/grid precision и не становится настройкой пользователя.
  3. Before/after может пересчитывать только connected component candidate segment; exact cache structure свободна.
  4. Pending queue хранится только в session memory; persisted draft является crash-safe source of truth до final batch.
  5. Exact progress key, history names, helper/file/smoke/golden ids свободны при наличии RU/EN и выполнении AC.
  6. Reset может очистить только active session pointer либо оставить saved draft для явного endpoint resume, но не считается owner-approved Finish trigger.
  7. Active new segments разрешено разрезать в persisted partitions только когда пользователь подтвердил room и это нужно для сохранения unconsumed части; старые records не дробятся.
  8. Clean split classification может переиспользовать splitRoomPath; общий face detector не обязан заменять canonical polygon split helper.
  9. Full nested rooms сохраняют действующий smallest/geometry resolution других подсистем; #173 не вводит новый device-room arbitration.
  10. Открытых продуктовых вопросов нет: Q1 принят с правкой владельца, Q2–Q7 — defaults, 18.08.2026.