mirror of
https://github.com/Matysh/houseplan-card
synced 2026-10-07 23:19:14 +00:00
docs: specify atomic wall thickness lookup
Issue: #201 User-Visible: no
This commit is contained in:
@@ -0,0 +1,217 @@
|
||||
# Issue #201 — наследование толщины для атомарного участка стены
|
||||
|
||||
- Дата: 2026-08-19
|
||||
- Тип: bug · приоритет P2 · ценность 6/10 · сложность 4/10
|
||||
- Issue: [#201](https://github.com/Matysh/houseplan-card/issues/201)
|
||||
- Ветка: `issue/201-atomic-thickness-lookup`
|
||||
|
||||
Канонические документы: `docs/SCOPE.md`, `docs/UX-MODES.md`,
|
||||
`docs/WALL-THICKNESS.md`, `docs/TESTING.md`.
|
||||
|
||||
## 1. Сценарий и персона
|
||||
|
||||
Администратор редактирует план с частично совпадающими границами трёх комнат.
|
||||
На одном физическом пролёте уже задана толщина стены. Администратор закрывает
|
||||
виртуальный участок этой границы и ожидает, что восстановленная кладка получит
|
||||
толщину ближайшего сплошного продолжения, как и обещает текущий инструмент.
|
||||
|
||||
Это часть J4/J6: редактирование не должно незаметно заменять сохранённую
|
||||
физическую характеристику значением по умолчанию.
|
||||
|
||||
## 2. Что человек увидит до и после
|
||||
|
||||
**До:** если соседний сплошной участок является атомарной частью более длинной
|
||||
wall-записи, Close не узнаёт его толщину и создаёт восстановленный участок с
|
||||
default 15 см. На стене 20/22 см появляется непреднамеренная ступень.
|
||||
|
||||
**После:** Close наследует фактическую толщину покрывающего точного wall-run.
|
||||
Default 15 см используется только тогда, когда ни один подходящий сосед
|
||||
действительно не имеет толщины.
|
||||
|
||||
## 3. Подтверждённая проблема и причина
|
||||
|
||||
На production-scale fixture из #197 сохранённая запись 20 см точно покрывает
|
||||
пролёт `[620.83,550]–[887.5,550]`. Третья комната делит его на два атомарных
|
||||
ребёнка:
|
||||
|
||||
- `[620.83,550]–[691.67,550]`;
|
||||
- `[691.67,550]–[887.5,550]`.
|
||||
|
||||
На текущем `dev` `wallIntervals()` правильно разрешает обоих детей в 20 см:
|
||||
`cmsForPoly()` уже умеет искать покрывающий exact span. Но публичный pure-helper
|
||||
`thicknessCmAt()` вызывает только `lookupWall()`, рассчитанный на собственный
|
||||
key/midpoint одного stretch, и возвращает 0 для обоих детей. Полный parent-run
|
||||
тем же вызовом разрешается в 20 см.
|
||||
|
||||
Единственный продуктовый consumer `thicknessCmAt()` — `thicknessOnClose()` в
|
||||
`src/open-spans.ts`. Он перебирает соседние `solidEdges`; нулевой ответ исключает
|
||||
реального соседа и приводит к `DRAW_WALL_DEFAULT_CM`.
|
||||
|
||||
Исходное предположение issue о нулевой толщине в общем render-path устарело:
|
||||
render использует `wallIntervals()` и на текущем коде сохраняет 20 см. Баг
|
||||
ограничен чтением exact parent-run при закрытии виртуальной границы.
|
||||
|
||||
## 4. Scope
|
||||
|
||||
- научить `thicknessCmAt()` читать наиболее узкий exact stored span, который
|
||||
коллинеарен и полностью покрывает запрошенный дочерний сегмент;
|
||||
- сохранить прямой key/tolerant lookup как первый и совместимый путь;
|
||||
- обеспечить правильное наследование в `thicknessOnClose()` и
|
||||
`applyThicknessOnClose()`;
|
||||
- покрыть normalized и production coordinate scales;
|
||||
- добавить regression unit, browser smoke, mutation gate, тестовый контракт и
|
||||
пользовательские changelog.
|
||||
|
||||
## 5. Non-scope
|
||||
|
||||
- изменение `wallIntervals()`, `cmsForPoly()`, body geometry или рендера;
|
||||
- очистка уже сохранённых коротких интервалов (#198);
|
||||
- изменение выбора ближайшего коллинеарного соседа в `thicknessOnClose()`;
|
||||
- изменение default 15 см, UI Close, snapping, open-span ownership или Undo;
|
||||
- расширение legacy key-only записи на произвольный покрывающий parent;
|
||||
- schema/backend/API, миграция, Optimize и переписывание config при чтении;
|
||||
- общая замена midpoint/key модели wall entries.
|
||||
|
||||
## 6. Контракт разрешения толщины
|
||||
|
||||
### 6.1. Порядок поиска
|
||||
|
||||
Для query `[a,b]` функция `thicknessCmAt(walls,a,b,pitch,coordScale)`:
|
||||
|
||||
1. сохраняет текущий результат `lookupWall()` при direct key или его
|
||||
совместимом midpoint fallback;
|
||||
2. если direct hit отсутствует, рассматривает только записи с валидными
|
||||
конечными точками `a/b`, положительной толщиной и совпадающим направлением;
|
||||
3. принимает запись только тогда, когда обе query endpoints лежат на её
|
||||
конечном отрезке в действующем scale-relative tolerance, то есть stored span
|
||||
покрывает query целиком;
|
||||
4. при нескольких кандидатах выбирает наиболее узкий покрывающий span; tie
|
||||
разрешается детерминированно и независимо от порядка входного массива;
|
||||
5. если кандидата нет, возвращает 0 как сейчас.
|
||||
|
||||
### 6.2. Запрет утечки толщины
|
||||
|
||||
Exact запись, покрывающая только часть query, не подходит, даже если направления
|
||||
и midpoint близки. `[0..4]` не может дать толщину запросу `[0..10]`; тем самым
|
||||
сохраняется защита AUD-159B6-01. Параллельный сосед на другой линии,
|
||||
перпендикулярный участок, zero-length/malformed endpoints и `cm <= 0` также не
|
||||
подходят.
|
||||
|
||||
Legacy `{key,cm}` без exact endpoints продолжает работать только по нынешнему
|
||||
direct/midpoint контракту: новая логика не может доказать его покрытие.
|
||||
|
||||
### 6.3. Close
|
||||
|
||||
`thicknessOnClose()` не меняет сортировку кандидатов: среди коллинеарных
|
||||
`solidEdges` побеждает ближайший к закрываемому span участок с разрешённым
|
||||
`cm > 0`. Исправление меняет только ложный `0` на фактическую толщину exact
|
||||
parent-run. Default применяется лишь при отсутствии любого такого соседа.
|
||||
|
||||
`applyThicknessOnClose()` записывает выбранную толщину существующим
|
||||
`setWallThickness()`; последующая normalisation, preview/apply и Undo не
|
||||
получают отдельной ветки поведения.
|
||||
|
||||
## 7. Данные, миграция и совместимость
|
||||
|
||||
Формат `walls`, ключи и exact endpoints не меняются. Миграции нет. Существующие
|
||||
планы не переписываются до явного Close. Уже созданные ошибочные 15-см участки
|
||||
остаются данными пользователя и относятся к #198.
|
||||
|
||||
Downgrade возвращает неверное наследование только при следующем Close, не
|
||||
повреждая уже сохранённый план. Backend validation и старые key-only планы
|
||||
сохраняют текущую совместимость.
|
||||
|
||||
## 8. UX, i18n, accessibility и touch
|
||||
|
||||
Новых элементов, строк, уведомлений и жестов нет. Исправляется результат уже
|
||||
существующего действия. Keyboard/touch policy, focus и editor affordance не
|
||||
меняются.
|
||||
|
||||
## 9. Acceptance criteria и доказательства
|
||||
|
||||
| AC | Критерий | Обязательное доказательство |
|
||||
|---|---|---|
|
||||
| AC1 | Exact parent `[0..10]` толщиной 20 см даёт 20 для атомарных queries `[0..4]` и `[4..10]`. | Table-driven unit `wall-thickness.test.mjs`. |
|
||||
| AC2 | AC1 одинаков при `coordScale = 1` и `coordScale = NORM_W`; направление endpoints можно развернуть. | Unit scale/direction matrix. |
|
||||
| AC3 | Exact partial `[0..4]` не даёт толщину query `[0..10]`; параллельный offset, perpendicular и malformed rows не совпадают. | Negative unit matrix. |
|
||||
| AC4 | Из нескольких покрывающих exact spans выбирается наиболее узкий; перестановка rows не меняет ответ. | Unit permutation test. |
|
||||
| AC5 | Legacy key-only direct/midpoint lookup сохраняет текущий результат. | Existing + focused regression unit. |
|
||||
| AC6 | `thicknessOnClose()` на частично расщеплённом parent-run наследует 20/22 см, а без толстого соседа сохраняет default 15 см. | `open-spans.test.mjs`. |
|
||||
| AC7 | Реальное Close в Plan на трёхкомнатном partial-overlap fixture сохраняет восстановленный span с соседним cm, без console/page errors; Undo восстанавливает virtual span. | Расширенный targeted browser smoke. |
|
||||
| AC8 | Render-path полного fixture до/после не меняется вне закрываемого span. | Existing wall/open-span units и targeted smoke. |
|
||||
| AC9 | Пользовательский и тестовый контракты обновлены. | Оба changelog + `docs/TESTING.md`. |
|
||||
| AC10 | Regression доказана исполняемым мутантом. | `mutation-gate --check`, clean green / mutant red. |
|
||||
| AC11 | Рабочие gates зелёные. | typecheck, unit, build, targeted smoke. |
|
||||
|
||||
## 10. План автотестов
|
||||
|
||||
### 10.1. Unit
|
||||
|
||||
В `test/wall-thickness.test.mjs` добавить матрицу exact containment: child,
|
||||
reversed, scale 1/1000, nested candidates, row permutation и negative cases.
|
||||
Тест обязан быть красным на текущем `dev` именно потому, что child возвращает 0.
|
||||
|
||||
В `test/open-spans.test.mjs` построить parent-run с атомарными `solidEdges` и
|
||||
проверить, что Close выбирает parent cm. Отдельно сохранить default-case.
|
||||
|
||||
### 10.2. Browser smoke
|
||||
|
||||
Расширить ближайший по fixture smoke (`smoke_resize_virtual_thick.mjs` либо
|
||||
выделенный targeted smoke): три комнаты создают частичный breakpoint на одной
|
||||
линии, более длинный exact wall-run имеет не-default толщину, средний virtual
|
||||
span закрывается UI-действием. Проверить persisted cm, визуальное отсутствие
|
||||
15-см ступени, Undo и отсутствие ошибок. Выбор конкретного файла — техническое
|
||||
решение реализации.
|
||||
|
||||
Golden не обязателен: видимая форма уже покрывается числовой browser-проверкой,
|
||||
а общий wall renderer не меняется. Если реализация добавит новую визуальную
|
||||
сцену, её baseline принимается только перед бетой.
|
||||
|
||||
### 10.3. Mutation gate
|
||||
|
||||
Добавить entry `atomic-child-thickness-parent-fallback`. Мутант отключает новый
|
||||
exact-parent fallback в `thicknessCmAt()` и возвращает прежний direct-only
|
||||
результат. Guard — focused unit `wall-thickness` + `open-spans`; на чистом коде
|
||||
green, на мутанте non-zero.
|
||||
|
||||
## 11. Риски и меры
|
||||
|
||||
| Риск | Мера |
|
||||
|---|---|
|
||||
| Частичная запись снова протекает на весь parent. | Полное покрытие обеих query endpoints и AC3. |
|
||||
| Overlapping exact rows дают order-dependent cm. | Наиболее узкий span + stable tie, permutation AC4. |
|
||||
| Scale tolerance работает только в normalized units. | AC2 на `coordScale = 1/NORM_W`. |
|
||||
| Исправление меняет общий renderer. | Render не использует новый fallback; AC8 и non-scope. |
|
||||
| Поиск по walls дорог. | Он выполняется только при editor Close; линейный проход сопоставим с текущим lookup и не находится в render tick. |
|
||||
|
||||
Security/privacy boundary не меняется.
|
||||
|
||||
## 12. Rollback
|
||||
|
||||
Frontend pure-правка откатывается вместе с тестами и документацией одним
|
||||
коммитом. Данные и schema не требуют обратной миграции. Уже выполненный Close
|
||||
остаётся валидной wall-записью с выбранной толщиной.
|
||||
|
||||
## 13. Release-артефакты
|
||||
|
||||
- записи в `docs/CHANGELOG.md` и `docs/CHANGELOG.ru.md` в implementation-коммите
|
||||
с `User-Visible: yes`;
|
||||
- обновление `docs/TESTING.md`;
|
||||
- unit, targeted browser smoke и mutation entry;
|
||||
- синхронные bundle snapshots;
|
||||
- отдельные backend, i18n, user-guide, golden, performance и migration
|
||||
artifacts не нужны;
|
||||
- полные smoke/golden/performance остаются предрелизным гейтом.
|
||||
|
||||
## 14. Принятые предположения
|
||||
|
||||
Принято предположительно, поменять свободно при ревью:
|
||||
|
||||
1. Fallback реализуется внутри `thicknessCmAt()`, а `lookupWall()` сохраняет
|
||||
узкий контракт «один key — один stretch».
|
||||
2. Для containment используется уже действующий scale-relative wall lookup
|
||||
tolerance, без нового пользовательского порога.
|
||||
3. Stable tie не меняет persisted rows; он только делает чтение независимым от
|
||||
порядка.
|
||||
4. Targeted browser-сценарий расширяет ближайший существующий smoke, если это
|
||||
не ухудшит его читаемость; иначе создаётся отдельный файл.
|
||||
@@ -100,6 +100,7 @@ GitHub Issues и GitHub Projects (v2) остаются единственным
|
||||
| [#174](https://github.com/Matysh/houseplan-card/issues/174) Связанный виртуальный источник следует реальному контроллеру | [174-linked-virtual-light-controller.md](174-linked-virtual-light-controller.md) |
|
||||
| [#178](https://github.com/Matysh/houseplan-card/issues/178) Выбор сущности для действия «Переключить состояние» | [178-toggle-entity.md](178-toggle-entity.md) |
|
||||
| [#197](https://github.com/Matysh/houseplan-card/issues/197) Один junction-патч не гасит кладку всего плана | [197-junction-patch-fail-dark.md](197-junction-patch-fail-dark.md) |
|
||||
| [#201](https://github.com/Matysh/houseplan-card/issues/201) Наследование толщины для атомарного участка стены | [201-atomic-thickness-lookup.md](201-atomic-thickness-lookup.md) |
|
||||
| [#186](https://github.com/Matysh/houseplan-card/issues/186) Безопасный остаток стены у торцов партиционного проёма | [186-partition-opening-jamb-margin.md](186-partition-opening-jamb-margin.md) |
|
||||
|
||||
## P3
|
||||
|
||||
Reference in New Issue
Block a user