feat: backend mirror of the junction limits (#329 §5, AC9)

custom_components/houseplan/junction_limits.py repeats П1-П4 for the write
barrier in websocket_api, counting per rule so an inherited violation still
round-trips, and raises JunctionLimitError with the stable code
junction_limit_<rule>.

П5 is deliberately not mirrored — it judges the rendered wall bodies, and a
second mitre/inset pipeline in Python would drift more dangerously than the
rule it guards. Optimize stays outside the check for the same reason migration
and import do: it repairs existing geometry.

test_parity_with_the_frontend_checks feeds identical fixtures to the TS
functions and to this module and demands the same verdict, so the two
implementations cannot silently diverge.

Issue: #329
User-Visible: no
This commit is contained in:
Codex
2026-08-27 23:32:07 +03:00
parent 7a74a37c6e
commit 8945e04fe4
6 changed files with 522 additions and 24 deletions
+11 -11
View File
@@ -2,7 +2,7 @@
"version": 1,
"fixture": "synthetic-only",
"chromium": "151.0.7922.34",
"sourceFingerprint": "d7dbfe2be5a7f7d20a59f281ad796428717854c7fcf8972568826c79f206cb99",
"sourceFingerprint": "eb7e35d0b47e9c3c3b60b0ddbca2e20a62e5eb1e9b516c4394b7617d72495502",
"captureScriptSha256": "ce2e9542fed9dade3085be87d16f69adb2ac8262893ad78ad966b1b9673f2983",
"command": "npm run build && node demo/docs/capture.mjs",
"scenarios": {
@@ -14,7 +14,7 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "d7dbfe2be5a7f7d20a59f281ad796428717854c7fcf8972568826c79f206cb99",
"sourceSha256": "eb7e35d0b47e9c3c3b60b0ddbca2e20a62e5eb1e9b516c4394b7617d72495502",
"imageSha256": "0ca982495a7749907c48ebbddfd089c54385a4347f79f64e94af2c5a8d5c03eb"
},
"view-touch": {
@@ -25,7 +25,7 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "d7dbfe2be5a7f7d20a59f281ad796428717854c7fcf8972568826c79f206cb99",
"sourceSha256": "eb7e35d0b47e9c3c3b60b0ddbca2e20a62e5eb1e9b516c4394b7617d72495502",
"imageSha256": "1c8e28ff34736b936ba7c9ec047d1da27aaf6fbb01411155f2d5c05282c02c41"
},
"space-create": {
@@ -36,7 +36,7 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "d7dbfe2be5a7f7d20a59f281ad796428717854c7fcf8972568826c79f206cb99",
"sourceSha256": "eb7e35d0b47e9c3c3b60b0ddbca2e20a62e5eb1e9b516c4394b7617d72495502",
"imageSha256": "a55b463055a565892ca139014e314a8f63fb5301c87fff2e48d7e69210fe9b6c"
},
"room-contour-close": {
@@ -47,7 +47,7 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "d7dbfe2be5a7f7d20a59f281ad796428717854c7fcf8972568826c79f206cb99",
"sourceSha256": "eb7e35d0b47e9c3c3b60b0ddbca2e20a62e5eb1e9b516c4394b7617d72495502",
"imageSha256": "dfad9a27371cfa397a3fd8dce7005f42d24649743f5322105ee894067d3a9f16"
},
"plan-context-tray": {
@@ -58,7 +58,7 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "d7dbfe2be5a7f7d20a59f281ad796428717854c7fcf8972568826c79f206cb99",
"sourceSha256": "eb7e35d0b47e9c3c3b60b0ddbca2e20a62e5eb1e9b516c4394b7617d72495502",
"imageSha256": "b75f348c632296da5c2fc142d53df613f757e1516acac315be50f5b4557729cc"
},
"device-editor": {
@@ -69,7 +69,7 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "d7dbfe2be5a7f7d20a59f281ad796428717854c7fcf8972568826c79f206cb99",
"sourceSha256": "eb7e35d0b47e9c3c3b60b0ddbca2e20a62e5eb1e9b516c4394b7617d72495502",
"imageSha256": "6b9cd281450e7782cff292f11efc58d227bedd0df86afc3be987feb2edec9dc3"
},
"device-display-preview": {
@@ -80,7 +80,7 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "d7dbfe2be5a7f7d20a59f281ad796428717854c7fcf8972568826c79f206cb99",
"sourceSha256": "eb7e35d0b47e9c3c3b60b0ddbca2e20a62e5eb1e9b516c4394b7617d72495502",
"imageSha256": "6f628abe6b89d7c4b352e6d73ac0b126526f6fdba5927bb774c5b884e484ca19"
},
"background-editor": {
@@ -91,7 +91,7 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "d7dbfe2be5a7f7d20a59f281ad796428717854c7fcf8972568826c79f206cb99",
"sourceSha256": "eb7e35d0b47e9c3c3b60b0ddbca2e20a62e5eb1e9b516c4394b7617d72495502",
"imageSha256": "a1d1a905f1410f73231d2ea5d1dfbd8ae79c2fe8b0a79cff87ebb977ac7a3103"
},
"room-card": {
@@ -102,7 +102,7 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "d7dbfe2be5a7f7d20a59f281ad796428717854c7fcf8972568826c79f206cb99",
"sourceSha256": "eb7e35d0b47e9c3c3b60b0ddbca2e20a62e5eb1e9b516c4394b7617d72495502",
"imageSha256": "48a5685039a5e0de2a28190d857b193c86e2e63606e3c7a8c40fc9d2b190da4e"
},
"device-info": {
@@ -113,7 +113,7 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "d7dbfe2be5a7f7d20a59f281ad796428717854c7fcf8972568826c79f206cb99",
"sourceSha256": "eb7e35d0b47e9c3c3b60b0ddbca2e20a62e5eb1e9b516c4394b7617d72495502",
"imageSha256": "29a342811aaedaebc54ba337792d60cb454a7cf4c3063b210f6c4bafe0a4d87b"
}
}
+26 -8
View File
@@ -97,11 +97,25 @@
## 5. Бэкенд
Семантическая дельта-валидация по образцу существующих (например, jamb
margin): правила П1–П5 применяются к элементам, которых касается дельта
записи (новые/изменённые сегменты, узлы, комнаты); унаследованные нарушения
нетронутых элементов проходят. Ошибки — стабильные коды
`junction_limit_<rule>` в общем формате `vol.Invalid`/семантических ошибок.
Питоновское зеркало обязано давать решения, идентичные фронту.
margin), модуль `custom_components/houseplan/junction_limits.py`. Считается
так же, как на фронте: нарушения по ПРАВИЛУ, а не по носителю (структурная
запись переатомизирует сегменты и меняет их id), унаследованные проходят,
новое — отказ. Ошибка — `JunctionLimitError` со стабильным кодом
`junction_limit_<rule>`, отдаётся тем же `connection.send_error`, что и
остальные семантические ошибки записи конфига.
Зеркалятся П1–П4. П5 (просвет комнаты) сознательно оставлен только на
клиенте: это утверждение о ПОСТРОЕННЫХ телах стен, и повторение конвейера
mitre/inset на Python было бы второй реализацией геометрии, расхождение
которой опаснее самого правила. Документ, нарушающий только П5, некрасив, но
не повреждён. Паритет П1–П4 закреплён тестом
`tests_backend/test_junction_limits.py::test_parity_with_the_frontend_checks`:
одни и те же фикстуры прогоняются через TS-функции и через питоновские, и
вердикт обязан совпасть.
Оптимизация планов проверку не проходит — это ремонтный путь того же класса,
что миграция и импорт (§3): он приводит существующую геометрию в порядок и не
должен упираться в правила, написанные для новых записей.
## 6. Затронутые поверхности и артефакты
@@ -112,8 +126,10 @@ margin): правила П1–П5 применяются к элементам,
`junction.limit_angle`, `junction.limit_valence`, `junction.limit_length`,
`junction.limit_distance`, `junction.limit_clearance` (с параметрами
значений) + заголовок отказа.
- `custom_components/houseplan/validation.py` — дельта-правила §5;
`tests_backend` — кейсы принятия унаследованного и отказа нового.
- `custom_components/houseplan/junction_limits.py` — дельта-правила §5,
вызов в `websocket_api.py` рядом с остальными семантическими валидаторами;
`tests_backend/test_junction_limits.py` — границы правил, принятие
унаследованного, отказ нового и паритет с фронтом.
- Тесты фронта: юниты на каждую формулу П1–П5 (границы: 14.9°/15°, 6/7 стен,
19/20 см, 4/5 см, пустой/непустой просвет), мутанты в гейте на П1 и §4;
смоки каналов отказа: текст объяснения ручки Resize и текст тоста
@@ -190,7 +206,9 @@ margin): правила П1–П5 применяются к элементам,
#313). План байт-неизменен. Смок проверяет текст тоста.
- **AC8.** Миграция/импорт/restore фикстуры с нарушениями проходят.
- **AC9 (§5).** Бэкенд: эхо-запись унаследованного нарушения проходит;
запись, добавляющая новое нарушение, отклоняется стабильным кодом.
запись, добавляющая новое нарушение, отклоняется кодом
`junction_limit_<rule>`; вердикты П1–П4 совпадают с фронтом на общих
фикстурах (тест паритета).
- **AC10.** Идемпотентность: повторный commit валидного конфига байт-иден-
тичен; Optimize на легаси-фикстуре не создаёт новых нарушений (AC-пост-
условие §3).