docs: spec #331 — boundary precision of the junction limits

Quantised node keys with -0 normalisation and node incidence at the quantum,
zero-degree wedges become visible, an iterative maximal-branch wall run, arc
collinearity measured against the chain base, fail-closed candidate checks,
and a narrow except in the python mirror. Every reproduction in §1 was
verified by execution on current dev after #330.

Issue: #331
User-Visible: no
This commit is contained in:
Codex
2026-08-28 04:03:14 +03:00
parent 2d1ca74a7d
commit 60e125e960
+142
View File
@@ -0,0 +1,142 @@
# Issue #331 — пограничная точность ограничений стыков (#329)
Статус: ревизия 1. Родители: #329 (правила), #330 (производительность —
слита, код проверок актуален по её итогу). Сёстры вне скоупа: #333
(optimize/import-лазейка), #339 (смешанные толщины у острия).
## 1. Сценарий и пользовательский результат
Класс симптома «легитимная запись отклонена / нелегитимная пропущена» уже
дважды бил по бетам (#316, #319). Репро на `dev` после #330 (все —
исполнением):
- пара узлов `−1e-8` и `0` (плавающий мусор ресайза до канонизации) — **два
ложных нарушения П4** с `actual = 0.000012 см`: ключи узлов `toFixed(6)`
дают `"-0.000000"` и `"0.000000"`;
- точный дубль стены (клин ровно 0°) не ловится ни П1, ни П4 — худший
вырожденный случай невидим, тогда как 0.5° блокируется;
- цепочка из ~9000 коллинеарных атомов — `RangeError` (обе реализации;
на бэке ушло бы клиенту как `unknown_error` с трейсбеком);
- при двух коллинеарных продолжениях в узле `.find` берёт первое — вторая
ветвь молча теряется, длина стены занижается → ложный отказ П3;
- исключение при проверке КАНДИДАТА пропускает запись (fail-open) — в
противоход установленному fail-closed гарду #278;
- `except Exception` в `_migrated_spaces` маскирует настоящие баги миграции;
- дуга из атомов по ~0.9°/шаг накапливает произвольный поворот, считаясь
одной «стеной» П3.
Результат: на границах вердикты честные и одинаковые в обоих зеркалах;
переполнений стека нет; отказ проверки — это отказ записи, не пропуск.
## 2. Решение (нормативно, оба зеркала симметрично)
### 2.1 Квантованный ключ узла и инцидентность
Ключ узла — координаты, квантованные к **1e-7** (`round(v·1e7)/1e7`) с
нормализацией `−0 → 0`; формат строки ключа одинаков в TS и python. Порог
выбран на два порядка грубее канонической сетки хранения (1e-9) и на
порядки тоньше любого осмысленного зазора плана (минимальный порог правил —
5 см ≈ 4e-4 в норм. координатах): плавающий мусор сливается, реальная
геометрия неразличимой не становится.
В П4 пары узлов с евклидовой дистанцией ≤ **1e-7** считаются ОДНИМ узлом
(инцидентность, не нарушение) — закрывает случай «через границу кванта»
(например `−5.1e-8` и `5.1e-8`).
### 2.2 П1: ноль градусов — честное нарушение
Фильтр `degrees > EPS` снимается: дельта ~0° между лучами узла — нарушение
угла с `actual ≈ 0`. Легальных 0°-клиньев не существует: коллинеарный стык
атомов одной прямой стены даёт в узле 180°-пару, а 0° — всегда дубль или
наложение стен. Точный дубль сегмента становится видим (два одинаковых
луча из одного узла).
### 2.3 Итеративный прогон П3 с максимальной ветвью
`collinearRunLengthUnits` / `collinear_run_length_units` переписываются
итеративно (явный стек, без рекурсии — глубина входа больше не
ограничивает). При развилке из нескольких коллинеарных продолжений одной
толщины берётся **максимальная** ветвь (DFS по вариантам, visited на рёбрах)
— вторая ветвь больше не теряется молча.
### 2.4 Коллинеарность — к базе цепочки
Допуск ±1° меряется к направлению ПЕРВОГО сегмента прогона, а не к
предыдущему атому — накопление поворота по дуге исчезает. Прямая стена с
легальными микро-изломами (<1° суммарно) остаётся одной стеной; дуга,
набравшая >1° от базы, — нет.
### 2.5 Fail-closed на кандидате
Исключение при вычислении нарушений КАНДИДАТА в `_junctionLimitsIntroduced`
— отказ записи с тостом (ключ `junction.limit_check_failed`, i18n en+ru), по
образцу fail-closed гарда #278. Baseline остаётся fail-open (недоказуемое
наследование не повод отклонять запись) — асимметрия сознательная и
комментируется в коде.
### 2.6 Узкий except в зеркале
`_migrated_spaces`: `except Exception` → `except (WallSegmentMigrationError,
ValueError)` + `_LOGGER.debug` с причиной. TypeError/RecursionError и прочие
баги миграции всплывают в общий обработчик WS честной ошибкой.
## 3. Границы
- Пороги правил П1–П5 (#329) не меняются; меняются вердикты только
пограничных классов: плавающий мусор (было: ложный отказ), 0°-дубль
(было: невидим), развилка/глубокая цепочка (было: занижение/краш), дуга
(было: одна «стена»).
- Спека #329 §2 дополняется абзацем о квантовании ключей и нуле градусов
(той же правкой, отдельной ревизией её файла).
- Вне скоупа: #333, #339, любые изменения производительности (#330 —
бюджеты бенча обязаны остаться зелёными).
## 4. Acceptance criteria
- **AC1 (ключи/инцидентность).** Пара `−1e-8`/`0` — ноль нарушений П4;
пара «через границу кванта» (`−5.1e-8`/`5.1e-8`) — ноль; узлы на
дистанции 4 см — по-прежнему нарушение; ровно 5 см — нет. Оба зеркала,
через паритет-набор.
- **AC2 (0°).** Точный дубль стены — нарушение П1 с `actual < 0.001°`;
прямая стена из двух атомов через узел (180°-пара) — по-прежнему чиста;
T-стык — чист. Оба зеркала.
- **AC3 (итеративность/ветви).** Цепочка 10 000 атомов — ответ без
переполнения (обе реализации); развилка двух коллинеарных ветвей — длина
= максимальной ветви (юниты с точным ожиданием), короткий доборный атом
перепада толщин остаётся законным (существующий AC3b #329 зелёный).
- **AC4 (дуга).** Дуга 30×0.9° не считается одной стеной (первый атом
короче 20 см в сумме с коллинеарной БАЗЕ частью — отказ П3); прямая с
одним изломом 0.5° — считается.
- **AC5 (fail-closed).** Синтетическое исключение в проверке кандидата
(мутант/подмена) — запись отклонена с тостом `junction.limit_check_failed`;
исключение на baseline — запись проходит. Смок/юнит.
- **AC6 (except).** Битая форма документа (TypeError в миграции) — честная
ошибка WS, не тихое «нарушений нет»; `WallSegmentMigrationError` —
прежний фолбэк «нет базы». Backend-юниты.
- **AC7 (паритет и регресс).** Паритет-набор расширен всеми классами выше;
все существующие юниты/смоки/мутанты #329+#330 и бенч #330 зелёные без
изменения ожиданий (кроме прямо описанных в §3 пограничных вердиктов).
## 5. План тестов
Юниты границ на каждый пункт §2 в обоих зеркалах + расширение паритета;
смок AC5 (тост отказа при сломанной проверке — через подмену в смоке);
мутанты: `junction-limit-zero-wedge-invisible` (возврат фильтра
`degrees > EPS`), `junction-limit-key-precision-lost` (возврат toFixed(6)),
`junction-limit-branch-dropped` (возврат `.find` первой ветви),
`junction-limit-candidate-fail-open` (возврат `catch { return []; }`).
Бенч #330 — без правок бюджетов.
## 6. Обязательные разделы
- **i18n**: один новый ключ `junction.limit_check_failed` (en+ru).
- **Touch**: не задет.
- **Риски**: (1) квантование ключей меняет вердикты классов входов —
закрыто явным перечнем §3 и паритетом; (2) выбор максимальной ветви может
легализовать ранее отклонявшиеся конфигурации — это исправление
занижения, фиксируется юнитом с точным ожиданием; (3) 0°-нарушение
может всплыть на легаси-планах с дублями — наследование #329 §3
по-прежнему пропускает несвязанные правки (инвариант не трогается).
- **Release**: обычная бета; CHANGELOG (en+ru) одной строкой user-visible
(«точность проверок стыков на границах: ложные отказы устранены, дубли
стен видимы»); откат — чистый revert.