mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-29 03:09:36 +00:00
500 lines
35 KiB
Markdown
500 lines
35 KiB
Markdown
# Issue #197 — один junction-патч не гасит кладку всего плана
|
||
|
||
- **Issue:** https://github.com/Matysh/houseplan-card/issues/197
|
||
- **Редакция:** r2 после `docs/reviews/SPEC-REVIEW-197-r1.md`; статус
|
||
определяется только метками issue
|
||
- **Тип / приоритет:** bug / P2
|
||
- **Оценка:** пользовательская ценность 9/10; ценность для разработки 9/10;
|
||
сложность 6/10; риск 8/10
|
||
- **Область:** каноническая геометрия стен, Plan, View/киоск, static card,
|
||
hidden Iso, clean floor/paper, Glow и солнце
|
||
- **Модель данных:** без изменений и миграции
|
||
- **Связано:** #141, #150, #198, #199, #201, `docs/WALL-THICKNESS.md`,
|
||
`docs/ARCHITECTURE.md`, `docs/TOUCH-SUPPORT.md`
|
||
|
||
## 0. Ответ на ревью r1
|
||
|
||
High-1 принят в части воспроизводимости: первая редакция не проговорила
|
||
критически важный контракт координат `WallEntry.a/b`, поэтому независимый
|
||
повтор оказался не тем production-вызовом, который выполнял автор.
|
||
|
||
В коде ревью комнаты, cuts **и `walls[].a/b`** были заранее умножены на 1000.
|
||
Это двойное масштабирование wall endpoints: `WallEntry.a/b` являются
|
||
persisted config coordinates, а `entrySpan(w, coordScale)` в
|
||
`src/wall-thickness.ts:124–129` сам умножает их на `coordScale`. Production
|
||
renderer масштабирует room polygons и open cuts, но передаёт `space.walls`
|
||
неизменными. При корректном вызове исходный fixture по-прежнему даёт один patch
|
||
и `wallBodiesGeometry() === null` на `19e92e0`.
|
||
|
||
R2 закрывает High не возражением на словах, а:
|
||
|
||
1. фиксирует координатный контракт явно;
|
||
2. добавляет ниже полный исполняемый reproducer, который сам извлекает fixture
|
||
из issue и не допускает ручного преобразования walls;
|
||
3. уточняет AC1/AC2 так, чтобы unit сначала доказывал правильную подготовку
|
||
fixture и наличие patch, а затем красный `null` исходного кода;
|
||
4. отделяет #201: прямой exact-key helper `thicknessCmAt()` действительно даёт
|
||
`0` на новом atomic child, но canonical `wallIntervals()`/`intervalCmAt()` на
|
||
production input дают `20 см`; поэтому #201 не подавляет patch и остаётся
|
||
отдельной задачей для собственной аналитики вызовов.
|
||
|
||
## 1. Сценарий и персона
|
||
|
||
**Персона:** домашний администратор, который уже построил подробный план и
|
||
периодически обслуживает его через «Общие настройки → Оптимизировать планы».
|
||
|
||
**Поверхность и момент:** после оптимизации или обычного открытия ранее
|
||
сохранённого много-комнатного плана человек переходит в View либо Plan. В одном
|
||
виртуальном T-стыке сходятся реальные стены соседних комнат с ненулевой
|
||
толщиной.
|
||
|
||
Задача поддерживает J1, J4 и J6 из `docs/SCOPE.md`: план должен правдиво
|
||
показывать пространственную структуру, штатно обслуживаться через GUI и не
|
||
терять физическую геометрию от одного локального численного отказа. View,
|
||
киоск и static card — блокирующие поверхности; Plan editor остаётся
|
||
desktop-first, но не может создавать или показывать иную физику.
|
||
|
||
## 2. Что человек увидит до и после
|
||
|
||
**До:** из-за одного стыка во всём пространстве исчезает штрихованная кладка;
|
||
между отступившими заливками комнат остаются белые полосы полной или половинной
|
||
толщины, как будто все стены одновременно стали прозрачными.
|
||
|
||
**После:** весь план продолжает показывать стены. Проблемный T-стык строится
|
||
обычно; даже если его дополнительный соединительный фрагмент численно
|
||
необрабатываем, отказ ограничивается этим фрагментом и не удаляет остальную
|
||
кладку, бумагу и физические препятствия.
|
||
|
||
## 3. Подтверждённая проблема и причина
|
||
|
||
Дефект воспроизведён на `origin/dev` `19e92e0` с анонимизированным fixture из
|
||
issue и production-параметрами:
|
||
|
||
- 8 комнат, 25 wall-записей, 3 `open_spans`;
|
||
- только `rooms[].poly` и `open_spans` переведены в render coordinates через
|
||
`NORM_W = 1000`; `walls[].a/b` оставлены в persisted config coordinates;
|
||
- `pitch = GRID_STEP_N`, `cell_cm = 5`, `gridPitch = GRID_PITCH`,
|
||
`coordScale = 1000`;
|
||
- `wallIntervals()` успешно разрешает значения `15/20/22/28/29/33 см`;
|
||
- `wallBodiesGeometry(...)` возвращает `null`.
|
||
|
||
`walls[].a/b` нельзя предварительно умножать на `NORM_W`: `entrySpan()` делает
|
||
это внутри по переданному `coordScale`. Следующий reproducer является
|
||
каноническим для проверки r2 после обычной сборки `test-build`:
|
||
|
||
```js
|
||
import { execFileSync } from 'node:child_process';
|
||
import { wallBodiesGeometry, wallIntervals } from './test-build/wall-thickness.js';
|
||
import { resolveOpenCuts } from './test-build/open-spans.js';
|
||
import { GRID_PITCH, GRID_STEP_N, NORM_W } from './test-build/space-geometry.js';
|
||
|
||
const issue = JSON.parse(execFileSync('gh', [
|
||
'issue', 'view', '197', '--repo', 'Matysh/houseplan-card', '--json', 'body',
|
||
], { encoding: 'utf8' }));
|
||
const raw = issue.body.match(/```json\s*([\s\S]*?)```/)[1];
|
||
const fixture = JSON.parse(raw);
|
||
const rooms = fixture.rooms.map((room) => ({
|
||
...room,
|
||
poly: room.poly.map(([x, y]) => [x * NORM_W, y * NORM_W]),
|
||
}));
|
||
const walls = structuredClone(fixture.walls); // config coords: НЕ умножать
|
||
const openCuts = resolveOpenCuts(
|
||
rooms, fixture.open_spans, NORM_W, GRID_PITCH * 0.02,
|
||
);
|
||
const intervals = wallIntervals(
|
||
rooms, walls, openCuts, GRID_STEP_N, fixture.cell_cm, GRID_PITCH, NORM_W,
|
||
);
|
||
const geometry = wallBodiesGeometry(
|
||
rooms, walls, openCuts, [], GRID_STEP_N, fixture.cell_cm, GRID_PITCH,
|
||
NORM_W, [],
|
||
);
|
||
console.log({
|
||
counts: [rooms.length, walls.length, openCuts.length], // [8, 25, 3]
|
||
nodeCm: intervals
|
||
.filter((iv) => Math.abs(iv.a[1] - 550) < 1e-6
|
||
&& Math.abs(iv.b[1] - 550) < 1e-6)
|
||
.map((iv) => iv.cm), // содержит 20
|
||
failed: geometry === null, // true на 19e92e0
|
||
});
|
||
```
|
||
|
||
Для прямого контроля patch-list автор исполнил ту же compiled module с временно
|
||
экспортированной без изменения тела `virtualJunctionPatches()`; результат — один
|
||
patch:
|
||
|
||
```json
|
||
[[[620.8333333333334,550],[612.5,550],
|
||
[612.5000000000001,541.6666666666665],
|
||
[620.8333333333334,541.6666666666666]]]
|
||
```
|
||
|
||
В реализации test seam должен сделать этот pre-union результат проверяемым без
|
||
source rewriting; точная публичность helper не является продуктовым контрактом.
|
||
|
||
Причинная цепочка:
|
||
|
||
1. `virtualJunctionPatches()` создаёт один прямоугольный patch около
|
||
`[620.8333…, 550]`.
|
||
2. Две математически совпадающие координаты приходят разными IEEE-754 числами:
|
||
`612.5` и `612.5000000000001`; аналогичный шум есть по Y.
|
||
3. `polyclip-ts` не может завершить output ring при `union(body, patch)` и
|
||
выбрасывает исключение.
|
||
4. Room rings и atomic wall-edge bodies имеют per-piece `try/catch`, а цикл
|
||
junction patches в `src/wall-thickness.ts:1760–1761` — нет.
|
||
5. Исключение достигает общего `catch`, `wallBodiesGeometry()` возвращает
|
||
`null`, и все потребители закономерно включают общий fail-dark.
|
||
|
||
Дополнительные исполняемые проверки отделяют причину от корреляций:
|
||
|
||
- удаление 15-см микро-интервала оставляет результат `null`;
|
||
- удаление open cuts, которое отключает virtual-junction pass, даёт валидную
|
||
geometry;
|
||
- округление только координат patch с технической точностью от `10⁻⁹` до
|
||
`10⁻¹²` render unit даёт валидную geometry; итоговая площадь после canonical
|
||
clipping совпадает с вариантом, где единственный отказавший patch локально
|
||
пропущен.
|
||
|
||
Проверка замечания r1 о толщине также выполнена на корректном production input:
|
||
`wallIntervals()` и `intervalCmAt()` возвращают `20 см` на обоих atomic children
|
||
горизонтальной стены узла. `thicknessCmAt()` — более узкий direct-key helper —
|
||
не наследует parent entry для child key и возвращает `0`, однако
|
||
`virtualJunctionPatches()` его не вызывает: она получает уже разрешённые
|
||
`wallIntervals()`. Поэтому Medium #201 не является причиной нулевого patch-list
|
||
в корректном воспроизведении #197.
|
||
|
||
Это не дефект #150: atomic thickness profile и exterior transition исправны.
|
||
Короткий вне-сеточный интервал — отдельная находка #198. Общая проверка результата
|
||
Optimize перед записью — отдельный защитный барьер #199.
|
||
|
||
## 4. Зафиксированное продуктовое решение
|
||
|
||
Открытых продуктовых вопросов нет. Действующий контракт уже задан
|
||
`docs/WALL-THICKNESS.md` и ТЗ #141:
|
||
|
||
1. локальный independent/virtual junction не может удалить валидную кладку
|
||
остальных комнат;
|
||
2. канонический результат един для рисунка, пола и световых препятствий;
|
||
3. при невозможности обработать дополнительный patch допустима локальная
|
||
консервативная деградация без записи config;
|
||
4. основной structural boolean failure по-прежнему отличим от успешной пустой
|
||
geometry и остаётся fail-dark; #197 не превращает любой сбой в оптимистичный
|
||
raw-ring fallback.
|
||
|
||
## 5. Скоуп
|
||
|
||
В задачу входят:
|
||
|
||
1. численная стабилизация координат, вычисленных для virtual junction patches,
|
||
перед передачей в boolean engine;
|
||
2. изоляция `union` каждого patch: один отказ не откатывает ранее построенные
|
||
room rings, edge bodies и успешные patches;
|
||
3. сохранение валидной основной geometry, `paperGeom`, `depthUnits` и
|
||
`openingIndex`, когда отказал только дополнительный patch;
|
||
4. одинаковый исправленный structural result для Plan, View/киоска,
|
||
`houseplan-space-card`, hidden Iso/floor footprint, clean floor/area, Glow,
|
||
source guard и солнца;
|
||
5. неизменность opening cuts, exterior shell, независимых partitions/columns и
|
||
порядка, в котором они входят в canonical body;
|
||
6. существующие сохранённые планы без миграции и фоновой записи;
|
||
7. unit regression на полном fixture, матрица численных вариантов, targeted
|
||
production-bundle browser smoke, visual regression и release-документация.
|
||
|
||
## 6. Не входит в задачу
|
||
|
||
- удаление, слияние или изменение минимальной длины wall interval — #198;
|
||
- geometry self-check в Optimize preview/apply/undo — #199;
|
||
- изменение алгоритма `normalizeWallIntervals()`, `degradeWalls()` или
|
||
`alignAllToGrid()`;
|
||
- общая замена `polyclip-ts` или настройка его глобальной точности;
|
||
- новый persisted junction/node, schema version или migration;
|
||
- привязка offset/mitre-вершин к пользовательской сетке;
|
||
- изменение толщины, `MITRE_LIMIT`, типов стыка или правил #141;
|
||
- исправление произвольного основного exterior/ring/opening boolean failure;
|
||
- новые controls, предупреждения, диагностический toast или настройка fallback;
|
||
- публикация скрытой изометрии.
|
||
|
||
## 7. Контракт поведения
|
||
|
||
### 7.1. Численная стабилизация patch
|
||
|
||
1. Координаты patch должны быть конечными; patch с не-конечным значением либо
|
||
площадью не больше действующего geometry tolerance не передаётся в union.
|
||
2. Математически совпадающие результаты операций с общей вершиной и offsets
|
||
приводятся к одной детерминированной координате с технической точностью,
|
||
многократно меньшей общего geometry epsilon.
|
||
3. Стабилизация не использует шаг пользовательской сетки: half-depth 15–33 см и
|
||
диагональные mitre вправе находиться между grid nodes.
|
||
4. Сдвиг любой вершины от исходного конечного значения не превышает выбранный
|
||
numeric tolerance и не меняет видимую толщину, bounded-mitre envelope или
|
||
association с исходным узлом.
|
||
5. Результат не зависит от представления `x` против `x ± ulp`, направления
|
||
segment и порядка эквивалентных wall records.
|
||
|
||
### 7.2. Изоляция локального отказа
|
||
|
||
1. Каждый junction patch объединяется независимо, как room-ring и edge-body
|
||
pieces в соседних canonical loops.
|
||
2. Перед попыткой сохраняется последняя успешная `body`. Если union patch
|
||
выбрасывает исключение, эта `body` остаётся результатом следующего шага.
|
||
3. Ошибка одного patch не пропускает последующие patches, exterior clipping,
|
||
shell union, opening cuts и independent extra-body union.
|
||
4. Если до patches существует валидная непустая structural body, функция не
|
||
возвращает `null` только из-за patch. `paperGeom` также не теряется.
|
||
5. Если основная exterior/ring/opening geometry не может быть построена либо
|
||
финальный обязательный pass падает, сохраняется действующий общий `null` и
|
||
fail-dark; raw per-room rings не воскрешаются.
|
||
6. Fallback ничего не записывает, не меняет входные массивы и не создаёт
|
||
различающуюся физику между render consumers.
|
||
|
||
### 7.3. Канонические потребители
|
||
|
||
Один возвращённый `wallBodiesGeometry()`/structural cache определяет:
|
||
|
||
- masonry path полного Plan/View и киоска;
|
||
- masonry path static card;
|
||
- hidden Iso footprint и wall faces;
|
||
- clean-floor subtraction, room fill и displayed area;
|
||
- paper footprint;
|
||
- Glow barriers, source-inside-body guard и spill;
|
||
- солнечные препятствия.
|
||
|
||
Ни один consumer не получает отдельный SVG-only обход либо собственную
|
||
нормализацию patch. Presentation-различия вроде hatch suppression при малой
|
||
экранной толщине остаются допустимыми; физическое множество совпадает.
|
||
|
||
### 7.4. Совместимость существующей геометрии
|
||
|
||
- Исправный virtual T продолжает получать прежний bounded patch.
|
||
- Room L/T/nested joins, corner Split #123, thickness transition #150 и
|
||
zero-divider #172 не меняют внешний outline.
|
||
- Openings по-прежнему режут room masonry после junction pass; совпавший
|
||
independent body не режется room opening.
|
||
- Partitions, drafts и columns не увеличивают Stage floor footprint.
|
||
- Перестановка комнат/стен может менять внутренний порядок boolean operations,
|
||
но не геометрическое множество и не способность функции завершиться.
|
||
|
||
## 8. Архитектурный контракт реализации
|
||
|
||
1. Исправление живёт в общей geometry-логике `src/wall-thickness.ts`, рядом с
|
||
построением/union virtual junction patches, а не в отдельном renderer.
|
||
2. Numeric normalization является чистым локальным helper либо эквивалентной
|
||
операцией и применяется только к вычисляемым patch vertices.
|
||
3. Tolerance масштабируется согласованно с `coordScale`, чтобы normalized и
|
||
render-space вызовы описывали одну физическую форму; жёсткое округление до
|
||
сантиметра, grid step или фиксированного числа видимых знаков запрещено.
|
||
4. Per-patch fallback сохраняет `body` транзакционно: присваивание происходит
|
||
только после успешного `union`.
|
||
5. Structural cache key не меняется: стабилизация является детерминированной
|
||
функцией уже входящих в fingerprint координат, cuts и толщин.
|
||
6. Логирование, если добавляется, не содержит config/названий комнат и не
|
||
спамит каждый HA state tick. Новый публичный diagnostic API не требуется.
|
||
|
||
Предполагаемые файлы:
|
||
|
||
- `src/wall-thickness.ts`;
|
||
- `test/wall-thickness.test.mjs`;
|
||
- `demo/smoke_junction_patch_resilience.mjs` либо узкое расширение существующего
|
||
wall-thickness smoke с однозначной связью с #197;
|
||
- visual fixture в golden matrix, если текущая wall-junction сцена не доказывает
|
||
сохранение полного много-комнатного плана;
|
||
- `docs/WALL-THICKNESS.md`, `docs/ARCHITECTURE.md`,
|
||
`docs/USER-GUIDE.ru.md`, `docs/TESTING.md`, `docs/STATUS.md`;
|
||
- `docs/CHANGELOG.md`, `docs/CHANGELOG.ru.md`;
|
||
- три синхронные поставляемые копии bundle.
|
||
|
||
## 9. Модель данных, compatibility и миграция
|
||
|
||
Форматы `RoomCfg`, `WallEntry`, `open_spans`, openings, partitions и columns не
|
||
меняются. Новых ключей, aliases и version markers нет.
|
||
|
||
- старые и уже оптимизированные планы исправляются вычисляемо при чтении;
|
||
- render, preview и cache warm-up не выполняют config/layout/storage write;
|
||
- Optimize не запускается автоматически и его отчёт не меняется;
|
||
- import/export и backend validation не меняются;
|
||
- откат возвращает прежнее поведение без преобразования данных;
|
||
- future/unknown fields не затрагиваются.
|
||
|
||
## 10. UX, i18n, accessibility и touch
|
||
|
||
Новых controls, текстов, focus/keyboard semantics, ARIA, animation и locale keys
|
||
нет. RU/EN i18n не меняется.
|
||
|
||
Plan editor остаётся desktop-first. Touch editor — best effort / intentionally
|
||
degraded, но один сохранённый plan не может получить другую masonry geometry из-за
|
||
pointer type. View, kiosk и static card полностью поддерживаются и обязаны
|
||
оставаться читаемыми.
|
||
|
||
Светлая/тёмная тема, forced colours, `prefers-reduced-motion`, hatch color и wall
|
||
opacity не меняют fallback. Цвет не используется как единственное доказательство:
|
||
unit проверяет геометрическое множество, browser smoke — реально отрисованный
|
||
path на двух темах.
|
||
|
||
## 11. Критерии приёмки
|
||
|
||
- **AC1 (`unit`):** полный анонимизированный fixture из issue готовится ровно по
|
||
reproducer §3: room/cut coordinates масштабированы, persisted `walls[].a/b`
|
||
остаются нормализованными; получаются 8 rooms, 25 walls, 3 cuts и 20-см
|
||
horizontal atomic intervals. На исходном `dev` этот вызов даёт `null`, после
|
||
исправления — ненулевой объект с непустыми `geom` и `paperGeom`.
|
||
- **AC2 (`unit`):** test seam подтверждает ровно один pre-union patch с четырьмя
|
||
вершинами из §3; варианты `x`, `x ± ulp` и стабилизированный эквивалент дают
|
||
одно geometric set в пределах numeric tolerance и не меняют bounded envelope.
|
||
- **AC3 (`unit`):** принудительный throw на первом из нескольких patch unions
|
||
сохраняет последнюю успешную body, позволяет обработать следующий patch и не
|
||
меняет `paperGeom`; основной обязательный boolean throw по-прежнему даёт
|
||
`null`.
|
||
- **AC4 (`unit`):** удаление и слияние микро-интервала не используются как
|
||
лечение: regression проходит с исходными 25 wall records; входные `rooms`,
|
||
`walls`, `openCuts`, `openings` и `extraBodies` после вызова побайтно
|
||
эквивалентны исходным.
|
||
- **AC5 (`unit`):** перестановка room/wall records, обратное направление
|
||
эквивалентных segments и повторный вызов дают одно множество без исключения;
|
||
geometry сравнивается symmetric difference/area, а не строковым порядком rings.
|
||
- **AC6 (`unit`):** существующие матрицы #123/#141/#150/#172, nested/courtyard,
|
||
openings, open spans и independent extras остаются зелёными; валидные обычные
|
||
junction fixtures геометрически не меняются за пределами numeric tolerance.
|
||
- **AC7 (`smoke`):** targeted production-bundle smoke загружает полный fixture и
|
||
подтверждает непустой masonry path в Plan, View, kiosk/static и hidden Iso,
|
||
одинаковый structural fingerprint и отсутствие write после render.
|
||
- **AC8 (`smoke`):** тот же smoke проверяет clean floor/paper и Glow/source/sun
|
||
consumers: кладка не исчезает при переключении HA state, светлой/тёмной темы и
|
||
режима отображения; topology не пересчитывается от обычного state tick.
|
||
- **AC9 (`golden`):** детерминированная сцена полного плана показывает кладку и
|
||
T-стык в Plan и View минимум в dark theme. Baseline принимается только из
|
||
просмотренного полного Linux artifact через `golden:accept -- --reviewed`.
|
||
- **AC10 (`ревью кода`):** отсутствуют renderer-specific fallback, grid snapping
|
||
физических offsets, schema/backend/i18n изменения и исправления #198/#199;
|
||
локальный catch не скрывает основной structural failure.
|
||
- **AC11 (`typecheck` + `unit` + `build` + `smoke`):** локальный gate зелёный,
|
||
целевой smoke выполнен до S7, три bundle-копии после build побайтно одинаковы.
|
||
|
||
## 12. План автотестов и гейтов
|
||
|
||
### Unit
|
||
|
||
В `test/wall-thickness.test.mjs` либо отдельном узком pure-geometry файле:
|
||
|
||
1. добавить полный issue fixture без ручного сокращения и вызвать production
|
||
signature `wallBodiesGeometry()`;
|
||
2. проверить patch count/finite vertices/bounds и numeric-equivalent variants;
|
||
3. внедрить контролируемый отказ одного patch union через узкий pure helper либо
|
||
другую тестируемую границу, не подменяя production algorithm;
|
||
4. проверить продолжение после отказа и отдельно общий structural fail-dark;
|
||
5. проверить input immutability, repeatability и permutation invariance;
|
||
6. прогнать существующие wall junction/thickness/opening/floor/light regressions.
|
||
|
||
Минимум один новый тест обязан уметь падать: возврат старого незащищённого цикла
|
||
должен снова дать `null` на полном fixture. Ревьюер проверяет эту мутацию или
|
||
эквивалентное доказательство.
|
||
|
||
### Targeted browser smoke
|
||
|
||
`node demo/smoke_junction_patch_resilience.mjs` либо эквивалентный явно названный
|
||
scenario:
|
||
|
||
1. загружает поставляемый production bundle и fixture issue;
|
||
2. снимает signatures masonry/paper/floor в Plan, View, static и hidden Iso;
|
||
3. проверяет dark/light theme и HA light-state update без исчезновения paths;
|
||
4. проверяет Glow/source guard и доступный sun-occluder contract;
|
||
5. подтверждает отсутствие config/layout write.
|
||
|
||
Этот targeted smoke выполняется локально перед `S7-code-review`. Полный набор
|
||
smoke не запускается в implementation loop.
|
||
|
||
### Golden и pre-release
|
||
|
||
Golden-сцена может расширить существующий wall-junction scenario, если полный
|
||
fixture и глобальное сохранение кладки читаются однозначно. Локальное принятие
|
||
baseline ради зелёной ветки запрещено. Full smoke, golden capture/verify,
|
||
performance и Linux Validate выполняются перед бетой на точном SHA по
|
||
`PROCESS.md`.
|
||
|
||
### Обязательный implementation loop
|
||
|
||
```text
|
||
npm run typecheck
|
||
npm test
|
||
npm run build
|
||
сравнение трёх bundle-копий
|
||
node demo/smoke_junction_patch_resilience.mjs
|
||
```
|
||
|
||
Python/HA harness не нужен для локального gate: backend не меняется; полный
|
||
Linux harness остаётся release gate.
|
||
|
||
## 13. Производительность и безопасность
|
||
|
||
Numeric normalization и один локальный `try/catch` не меняют асимптотику.
|
||
Количество patches, boolean passes и structural cache invalidation остаются
|
||
прежними. HA states, theme, hover и animation tick не входят в geometry key.
|
||
|
||
Отдельный budget не добавляется. Перед бетой существующие large-house и Full
|
||
Performance должны пройти без ослабления порогов; точный fixture #197 добавляется
|
||
в performance только если измерение покажет отдельный значимый профиль.
|
||
|
||
Security/privacy влияние отсутствует: нет HTML/CSS ввода, сетевых запросов, HA
|
||
services, новых permissions или пользовательских данных. Полный fixture уже
|
||
анонимизирован; diagnostic output не должен печатать исходный config.
|
||
|
||
## 14. Риски и снижение
|
||
|
||
| Риск | Вероятность / ущерб | Снижение |
|
||
|---|---|---|
|
||
| Слишком грубая нормализация изменит толщину или mitre | средняя / высокий | tolerance значительно меньше geometry epsilon; exact bounds и symmetric-difference tests |
|
||
| Grid rounding сломает диагонали и half-depth | средняя / высокий | явный запрет grid snapping; matrix разных `coordScale` и толщин |
|
||
| Catch скроет основной отказ и вернёт опасную geometry | средняя / высокий | catch только вокруг одного optional patch; отдельный core-failure test |
|
||
| Plan исправится, а light/Iso продолжат использовать другой body | низкая / высокий | один canonical result и consumer smoke AC7/AC8 |
|
||
| Перестановка records снова вызовет polyclip failure | средняя / высокий | permutation + ulp matrix на полном fixture |
|
||
| Golden примет массовый anti-aliasing diff | низкая / средний | узкая сцена, просмотр полного Linux artifact, без локального auto-accept |
|
||
| Попутно изменится optimizer | низкая / высокий | явный non-scope и отдельные #198/#199 |
|
||
|
||
## 15. Откат
|
||
|
||
Откат — revert одного user-visible implementation commit вместе с тестами,
|
||
документацией, changelog и bundle-копиями. Persisted schema и данные не меняются,
|
||
поэтому migration/cleanup не нужны. После отката возвращается риск полного
|
||
исчезновения кладки на исходном fixture.
|
||
|
||
Feature flag не добавляется: это восстановление обязательного fail-closed
|
||
контракта общей физической геометрии, а не экспериментальная функция.
|
||
|
||
## 16. Release-артефакты
|
||
|
||
Изменение пользовательское. Implementation-коммит имеет `User-Visible: yes` и
|
||
в том же коммите обновляет:
|
||
|
||
- `docs/CHANGELOG.md` и `docs/CHANGELOG.ru.md` со ссылкой на #197;
|
||
- `docs/WALL-THICKNESS.md` — per-piece junction failure и numeric normalization;
|
||
- `docs/ARCHITECTURE.md` — граница optional patch против core structural failure;
|
||
- `docs/USER-GUIDE.ru.md` — диагностика исчезнувшей кладки после Optimize;
|
||
- `docs/TESTING.md` — unit/smoke/golden coverage #197;
|
||
- `docs/STATUS.md` — фактическая реализованная release-линия;
|
||
- unit fixture, targeted smoke и golden scenario/candidate;
|
||
- `dist/houseplan-card.js`,
|
||
`custom_components/houseplan/frontend/houseplan-card.js`,
|
||
`demo/srv/assets/houseplan-card.js` после build.
|
||
|
||
Screenshots вне golden не требуются. Backend, migration, i18n, отдельный security
|
||
report и отдельный performance budget не требуются. Issue закрывается только
|
||
после включения в опубликованную бету.
|
||
|
||
## 17. Принятые технические предположения — можно менять без продуктового ревью
|
||
|
||
1. Рекомендуемый numeric step — величина порядка `coordScale × 10⁻¹²` либо
|
||
эквивалентная relative/ULP-нормализация, доказанная AC2; точное имя helper и
|
||
коэффициент не являются продуктовым контрактом.
|
||
2. Нормализуются только junction patch vertices непосредственно перед boolean
|
||
boundary; persisted rooms/walls/cuts и основные room profiles не округляются.
|
||
3. Patch с не-конечными координатами или ничтожной площадью считается локально
|
||
непригодным и проходит тот же isolated fallback.
|
||
4. Если нормализованный patch всё равно вызывает исключение, он пропускается;
|
||
последующие patches и обязательные passes продолжаются.
|
||
5. Для текущего полного fixture пропуск и успешный стабилизированный union после
|
||
final exterior clipping дают одинаковую площадь; тест всё равно проверяет
|
||
успешную нормализацию, чтобы catch не был единственным лечением.
|
||
6. Инъекция отказа в unit может быть реализована через небольшой pure helper,
|
||
test-only boolean adapter или эквивалент без публичного runtime API.
|
||
7. Имена smoke/golden scenarios и раскладка test fixture свободны, если связь с
|
||
AC и возможность красного прогона сохраняются.
|
||
8. Нет открытых продуктовых вопросов; смежные решения вынесены в #198 и #199.
|