Files
houseplan-card/docs/specs/331-junction-limit-precision.md
T
Codex 60e125e960 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
2026-08-28 04:03:14 +03:00

11 KiB
Raw Blame History

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.