diff --git a/docs/specs/331-junction-limit-precision.md b/docs/specs/331-junction-limit-precision.md index f1e388d9..cba8b1fe 100644 --- a/docs/specs/331-junction-limit-precision.md +++ b/docs/specs/331-junction-limit-precision.md @@ -1,11 +1,19 @@ # Issue #331 — пограничная точность ограничений стыков (#329) -Статус: ревизия 1. Родители: #329 (правила), #330 (производительность — +Статус: ревизия 2 (spec-ревью r1: H1 — квантование по формуле канонизации +вместо нативного round, M1 — порог инцидентности 2e-7 и исправленный пример, +M2 — узкий except только для кандидата, M3 — обход по рёбрам вместо DFS, +M4 — USER-GUIDE, L1 — пользовательская фраза §1). Родители: #329 (правила), #330 (производительность — слита, код проверок актуален по её итогу). Сёстры вне скоупа: #333 (optimize/import-лазейка), #339 (смешанные толщины у острия). ## 1. Сценарий и пользовательский результат +Пользователь заканчивает ресайз или рисование на обычном плане — и получает +отказ «стены слишком близко: 0.00001 см», хотя ничего не нарушал; а сосед, +случайно нарисовавший стену дважды по одной линии, не получает отказа +вовсе. Задача делает пограничные вердикты честными. + Класс симптома «легитимная запись отклонена / нелегитимная пропущена» уже дважды бил по бетам (#316, #319). Репро на `dev` после #330 (все — исполнением): @@ -32,16 +40,22 @@ ### 2.1 Квантованный ключ узла и инцидентность -Ключ узла — координаты, квантованные к **1e-7** (`round(v·1e7)/1e7`) с -нормализацией `−0 → 0`; формат строки ключа одинаков в TS и python. Порог -выбран на два порядка грубее канонической сетки хранения (1e-9) и на -порядки тоньше любого осмысленного зазора плана (минимальный порог правил — -5 см ≈ 4e-4 в норм. координатах): плавающий мусор сливается, реальная -геометрия неразличимой не становится. +Ключ узла — координаты, квантованные к **1e-7** по формуле канонизации +этого репозитория: `sign(v) · floor(|v|·1e7 + 0.5) / 1e7` (r1-H1: нативные +`Math.round`/`round()` расходятся на .5-тиках — banker's rounding в python +против round-half-away в JS; `coordinate-canonicalization.ts/.py` уже +используют ровно эту формулу с комментарием о паритете, и ключи обязаны ей +следовать). `−0` нормализуется в `0`; формат строки ключа одинаков в TS и +python. Порог выбран на два порядка грубее канонической сетки хранения +(1e-9) и на порядки тоньше любого осмысленного зазора плана (минимальный +порог правил — 5 см ≈ 4e-4 в норм. координатах). -В П4 пары узлов с евклидовой дистанцией ≤ **1e-7** считаются ОДНИМ узлом -(инцидентность, не нарушение) — закрывает случай «через границу кванта» -(например `−5.1e-8` и `5.1e-8`). +В П4 пары узлов с евклидовой дистанцией ≤ **2e-7** (по сырым координатам) +считаются ОДНИМ узлом — инцидентность, не нарушение (r1-M1: порог покрывает +пару, севшую на соседние кванты, например `−5.1e-8` и `5.1e-8` с сырой +дистанцией 1.02e-7). Известное следствие для П2: мусорная пара на соседних +квантах остаётся двумя записями в карте валентности — недосчёт не хуже +текущего поведения и валентность никогда не ЗАВЫШАЕТСЯ; помечено в §3. ### 2.2 П1: ноль градусов — честное нарушение @@ -55,9 +69,14 @@ `collinearRunLengthUnits` / `collinear_run_length_units` переписываются итеративно (явный стек, без рекурсии — глубина входа больше не -ограничивает). При развилке из нескольких коллинеарных продолжений одной -толщины берётся **максимальная** ветвь (DFS по вариантам, visited на рёбрах) -— вторая ветвь больше не теряется молча. +ограничивает). При развилке коллинеарных продолжений одной толщины ветвь +больше не теряется: обход идёт **по рёбрам с visited-набором** — каждый +атом участвует в прогоне не более одного раза, суммарная работа O(E) на +вызов независимо от числа развилок (r1-M3: полный DFS по вариантам дал бы +комбинаторику; вместо «максимальной из всех путей» прогоном считается +суммарная длина коллинеарной КОМПОНЕНТЫ связности — это консервативнее к +пользователю, чем текущий `.find`, монотонно и дёшево). Бюджеты бенча #330 +остаются зелёными без правок — закреплено AC7. ### 2.4 Коллинеарность — к базе цепочки @@ -76,9 +95,14 @@ ### 2.6 Узкий except в зеркале -`_migrated_spaces`: `except Exception` → `except (WallSegmentMigrationError, -ValueError)` + `_LOGGER.debug` с причиной. TypeError/RecursionError и прочие -баги миграции всплывают в общий обработчик WS честной ошибкой. +`_migrated_spaces` получает флаг стороны (r1-M2 — симметрия с §2.5): + +- для **кандидата** — `except (WallSegmentMigrationError, ValueError)` + + `_LOGGER.debug`; TypeError/RecursionError и прочие баги миграции всплывают + в общий обработчик WS честной ошибкой (fail-closed, как §2.5); +- для **previous** — прежний широкий фолбэк «нет базы для наследования» + + `_LOGGER.debug`: недоказуемое наследование не повод блокировать + несвязанную запись (тот самый симптом П1, который задача чинит). ## 3. Границы @@ -88,22 +112,29 @@ ValueError)` + `_LOGGER.debug` с причиной. TypeError/RecursionError и (было: одна «стена»). - Спека #329 §2 дополняется абзацем о квантовании ключей и нуле градусов (той же правкой, отдельной ревизией её файла). +- Известное следствие §2.1: валентность узла (П2) на мусорной паре соседних + квантов может недосчитать единицу — не хуже текущего поведения и не + завышает (r1-M1). - Вне скоупа: #333, #339, любые изменения производительности (#330 — бюджеты бенча обязаны остаться зелёными). ## 4. Acceptance criteria - **AC1 (ключи/инцидентность).** Пара `−1e-8`/`0` — ноль нарушений П4; - пара «через границу кванта» (`−5.1e-8`/`5.1e-8`) — ноль; узлы на - дистанции 4 см — по-прежнему нарушение; ровно 5 см — нет. Оба зеркала, - через паритет-набор. + пара «через границу кванта» `−5.1e-8`/`5.1e-8` (сырая дистанция 1.02e-7 ≤ + порога 2e-7) — ноль; узлы на дистанции 4 см — по-прежнему нарушение; + ровно 5 см — нет. Квантование на .5-тике (`v = 2.5e-7`) даёт ОДИН и тот + же ключ в TS и python (формула канонизации, r1-H1) — прямой юнит в + паритет-наборе. Оба зеркала. - **AC2 (0°).** Точный дубль стены — нарушение П1 с `actual < 0.001°`; прямая стена из двух атомов через узел (180°-пара) — по-прежнему чиста; T-стык — чист. Оба зеркала. - **AC3 (итеративность/ветви).** Цепочка 10 000 атомов — ответ без - переполнения (обе реализации); развилка двух коллинеарных ветвей — длина - = максимальной ветви (юниты с точным ожиданием), короткий доборный атом - перепада толщин остаётся законным (существующий AC3b #329 зелёный). + переполнения (обе реализации); развилка коллинеарных ветвей — длина = + суммарной длине компоненты (юнит с точным ожиданием, обе ветви учтены); + цепочка со 100 развилками подряд — линейное время (входит в бенч-сетку + через юнит-таймаут, r1-M3); короткий доборный атом перепада толщин + остаётся законным (существующий AC3b #329 зелёный). - **AC4 (дуга).** Дуга 30×0.9° не считается одной стеной (первый атом короче 20 см в сумме с коллинеарной БАЗЕ частью — отказ П3); прямая с одним изломом 0.5° — считается. @@ -137,6 +168,9 @@ ValueError)` + `_LOGGER.debug` с причиной. TypeError/RecursionError и занижения, фиксируется юнитом с точным ожиданием; (3) 0°-нарушение может всплыть на легаси-планах с дублями — наследование #329 §3 по-прежнему пропускает несвязанные правки (инвариант не трогается). -- **Release**: обычная бета; CHANGELOG (en+ru) одной строкой user-visible +- **Release**: обычная бета; `docs/USER-GUIDE.ru.md` и `.md` — раздел + «Ограничения стыков стен» дополняется предложением о тосте + `junction.limit_check_failed` (отказ при невозможности проверить, r1-M4); + CHANGELOG (en+ru) одной строкой user-visible («точность проверок стыков на границах: ложные отказы устранены, дубли стен видимы»); откат — чистый revert.