20 KiB
SPEC-REVIEW-331-r2
Issue: #331 — «Ограничения стыков (#329): пограничная точность даёт ложные
отказы и невидимые дубли»
ТЗ: docs/specs/331-junction-limit-precision.md
Материал раунда: ревизия 2, SHA 2d703543 (issue-комментарий автора называет
этот SHA явно).
Предыдущий раунд: r1, вердикт «красный», документ
docs/reviews/SPEC-REVIEW-331-r1.md, материал на SHA 60e125e9.
Трек: полный (не small) — не изменился, дифф r1→r2 не расширяет и не
сужает трек: тот же файл, только правки нормативных пунктов и AC.
Заход: r2 · блокирующих циклов израсходовано 1 из 4 (израсходован r1, r2
жёлтый бюджет не тратит по правилу «зелёный не тратит», но зелёного нет —
см. вердикт).
Скоуп разбора (дельта, не заново)
Согласно инструкции по дельте: дифф git diff 60e125e9..2d703543 -- docs/specs/331-junction-limit-precision.md — предмет этого раунда. Дельта
не локальна в смысле «одна строка»: она переписывает нормативный текст трёх
из шести пунктов §2 (2.1, 2.3, 2.6) и правит §1, §3, AC1, AC3, §6/Release —
то есть ревизия закрывает пять из пяти находок (H1, M1–M4) плюс L1
содержательными правками, а не декларацией. Это оправдывает не полный
повторный разбор с нуля, но полную проверку каждой правки по существу
(арифметика, согласованность с остальным документом, согласованность
AC↔нормативный пункт) — именно так и сделано ниже. Не трогавшиеся куски
(§2.2, §2.4, §2.5, AC2, AC4, AC5, §5, i18n/touch) — унаследованы из r1 без
повторной проверки, см. раздел ниже.
Как проверялось
docs/SCOPE.md— не перечитывался заново (не задет дельтой); вывод r1 («соответствует J6») наследуется.PROCESS.md§2.4/§2.9/§7.1/§7.2 — перечитаны для формата документа и verdict-строки этого раунда.- Тело issue #331 и оба исходных комментария (уже читаны в r1) плюс два
новых комментария: вердикт r1 (
claude, красный) и хендофф автора о ревизии 2 — оба черезgh issue view 331 --comments. git diff 60e125e9..2d703543 -- docs/specs/331-junction-limit-precision.md— построчно, каждая правка сверена с текстом находки, которую она заявляет закрывающей.- Для проверки арифметики и терминологии H1/M1 — код:
src/ coordinate-canonicalization.tsиcustom_components/houseplan/ coordinate_canonicalization.py(формулаsign(v)·floor(|v|·factor+0.5)/ factorподтверждена побайтово в обоих файлах — не изобретена заново). - Для проверки M3 (семантика «сумма компоненты» вместо «максимальной
ветви») — текущая реализация
collinearRunLengthUnits(src/ junction-limits.ts:133-158) иcheckNodes/П1 (:63-96), чтобы оценить, действительно ли переопределение алгоритма меняет наблюдаемое поведение на легитимных (не патологических) входах. - Для M4 — существование
docs/USER-GUIDE.md(английское зеркало) рядом с.ru.md, и раздел «Ограничения стыков стен» (docs/USER-GUIDE.ru.md:420- 444) как целевое место правки. - i18n:
src/i18n/ru.json:590/en.json:590(junction.limit_distance) — сверка иллюстративной фразы §1 с реальным текстом тоста (не выдумана).
Гейты typecheck/test/build/check-docs/инварианты модели не гонялись:
дифф раунда — исключительно docs/specs/331-*.md (класс C, стадия spec).
Продуктового кода в диффе по-прежнему нет — §8/§10.2 PROCESS.md к спек-ревью
не применяются. Смоки/golden/backend-pytest/perf-профили — не применимы по
той же причине (нет исполняемого изменения).
Закрытие раунда r1
| Находка r1 | Чем закрыта в r2 | Где это видно |
|---|---|---|
H1 — квантование ключа через нативный round, риск разъезда TS/Python на .5-тике |
§2.1 переписан на явную формулу `sign(v)·floor( | v |
M1 — пример AC1 (−5.1e-8/5.1e-8) не проходит заявленный порог ≤1e-7 (реальная дистанция 1.02e-7) |
Порог инцидентности поднят до 2e-7 по сырым координатам; текст AC1 сам приводит арифметику 1.02e-7 ≤ 2e-7 |
:53-55, AC1 :125-127. Проверено арифметически: 1.02e-7 < 2e-7 — верно. Верхняя граница порога (2e-7) остаётся на четыре порядка меньше реального минимума правил (5 см ≈ 4e-4), запаса достаточно, чтобы не поглотить настоящие «слишком близко». Закрыто. |
M2 — §2.6 сужает except не различая previous/candidate; риск нового fail-closed на несвязанной стороне |
§2.6 переписан: кандидат — узкий except (fail-closed), previous — прежний широкий фолбэк (fail-open) явно, симметрично §2.5 |
:98-105. Нормативный текст закрыт. AC6 не тронут диффом (см. новую находку M-r2-1 ниже) — тестовый критерий не различает стороны, значит асимметрия, которую только что ввёл §2.6, не имеет однозначного доказательства. Закрыто частично. |
M3 — максимальная ветвь ищется DFS-перебором вариантов, сложность 2^K..3^K по числу развилок, не застрахована стресс-тестом |
§2.3 переписан: вместо перебора вариантов — обход по рёбрам с visited-множеством, каждый атом входит в прогон не более раза, O(E) независимо от числа развилок; AC3 добавляет кейс «100 развилок подряд — линейное время» |
:69-79, AC3 :132-137. Механизм действительно O(E) (посещение атома один раз гарантировано visited-набором) — линейность по числу развилок доказана конструкцией, а не тестом отдельно, но AC3 добавляет и прямую проверку. Проверено дополнительно: любой вход, где в одном узле есть ≥2 коллинеарных продолжения одной толщины («развилка» в терминах §2.3), по построению даёт две почти сонаправленные стены в одном узле — угол между ними < 15° и попадает под П1 независимо от версии этого пункта (порог 15° в MIN_JUNCTION_ANGLE_DEG, src/junction-limits.ts:15, не относится к правкам этой задачи). Значит запись с «развилкой» и так отклоняется П1 до того, как её увидит П3 — смена семантики «максимум пути» → «сумма компоненты связности» не открывает новый способ пропустить нелегитимную запись. Закрыто. (Остаточная стилистическая нестыковка — см. Low L2 ниже.) |
M4 — Release не называет docs/USER-GUIDE.ru.md |
Release-абзац дополнен: «docs/USER-GUIDE.ru.md и .md — раздел «Ограничения стыков стен» дополняется предложением о тосте junction.limit_check_failed» |
:171-174. Место правки (раздел «Ограничения стыков стен», docs/USER-GUIDE.ru.md:420-444) существует и является верным целевым разделом; английское зеркало docs/USER-GUIDE.md существует. Закрыто. |
| L1 — §1 не даёт открытой пользовательской фразы | §1 открывается сценарием от первого лица пользователя («заканчивает ресайз… получает отказ… хотя ничего не нарушал») до технического перечня | :12-15. Иллюстративная фраза тоста сверена с реальным junction.limit_distance (src/i18n/ru.json:590) — не выдуманный текст, обоснованный парафраз. Закрыто. |
Находки
Medium (в скоупе, чинится в этом же ТЗ)
M-r2-1 (наследник M2, закрыт частично). AC6 не различает сторону
(previous/candidate) — новая асимметрия §2.6 введена нормативно, но не
имеет однозначного теста.
docs/specs/331-junction-limit-precision.md — §2.6 (:96-105) в этой
редакции чётко разводит поведение _migrated_spaces по сторонам: для
кандидата TypeError/RecursionError теперь всплывают честной ошибкой WS
(fail-closed), для previous — по-прежнему тихий широкий фолбэк «нет базы»
(fail-open). Это ровно то различение, которое требовал r1-M2.
Но AC6 (:144-146) — байт-в-байт тот же текст, что был на SHA 60e125e9
(до правки; сверено git show 60e125e9:... против текущего файла): «Битая
форма документа (TypeError в миграции) — честная ошибка WS, не тихое
«нарушений нет»; WallSegmentMigrationError — прежний фолбэк «нет базы».
Backend-юниты.» Ни слова о том, к какой стороне относится «битая форма
документа» — а именно этот вопрос был явно поставлен в r1-M2 («AC6 не
говорит, к какой стороне… относится тестовый документ… — без этого AC не
проверяем однозначно») и остаётся без ответа.
Реализация по такому AC пройдёт формально, даже если асимметрия перепутана
местами (например, если TypeError на previous тоже станет честной ошибкой
WS — регресс именно того симптома, ради которого заведена вся задача,
P1, #316/#319) или не введена вовсе (оба случая ловятся широким фолбэком) —
единственный тест, который отличил бы эти реализации друг от друга, не
описан.
Правка: AC6 должен явно завести два случая — «TypeError при миграции
КАНДИДАТА → честная ошибка WS» и «TypeError при миграции previous →
фолбэк «нет базы», запись не заблокирована» — по образцу того, как AC5 уже
различает сторону («исключение в проверке кандидата… исключение на
baseline…»).
Low
L2. §6 «Риски» (2) описывает механизм, которого §2.3 больше не содержит («максимальная ветвь»), хотя §2.3 в этой же редакции заменён на «сумму компоненты связности».
:165-168 (не тронуто диффом r1→r2): «(2) выбор максимальной ветви может
легализовать ранее отклонявшиеся конфигурации…». После правки §2.3 (:69- 79) алгоритм больше не выбирает «максимальную ветвь» — он суммирует ВСЮ
коллинеарную компоненту связности. Общий вывод риска (2) («может
легализовать») остаётся верным (сумма ≥ максимум одной ветви, то есть ещё
менее строго), но конкретное описание механизма устарело и не совпадает с
текстом §2.3 в этой же ревизии — при повторном чтении документа это читается
как рассинхрон между разделами. Не блокирует (сам вывод риска верен, M3
закрыт по существу — см. таблицу выше); достаточно одной правки слова
«ветвь» → «компонента» при следующей правке файла.
Унаследовано из r1 (без повторной проверки)
Ниже — то, что r1 (docs/reviews/SPEC-REVIEW-331-r1.md, материал @
60e125e9) проверил и признал корректным, и что дельта r1→r2 не затрагивает
(ни один символ в этих разделах не изменился между 60e125e9 и 2d703543):
- Скоуп/не-скоуп: сёстры #333/#339 вне скоупа, §3 границы, П1–П5 пороги не
меняются — соответствие
docs/SCOPE.mdJ6. - §2.2 (снятие фильтра
degrees > EPS) и AC2: не создаёт ложных срабатываний на легитимной прямой стене/T-стыке. - §2.4 (коллинеарность к базе цепочки) и AC4: числа внутренне согласованы.
- §2.5 (fail-closed кандидата) и AC5: согласован с прецедентом #278, канал тоста и ключ i18n названы.
- Структура «один AC на нормативный пункт + AC7 на паритет/регресс».
- i18n: один новый ключ,
en+ru— формат совпадает с существующимиjunction.limit_*. - Touch: не задет, обоснованно.
- Обязательство обновить
docs/specs/329-junction-limits.md§2 — учтено, не заводит параллельного источника истины.
Что проверено и признано корректным (эта ревизия)
- H1, M1, M3, M4, L1 закрыты содержательно, не декларативно — арифметика, формулы и ссылки на код проверены, а не приняты на слово автора.
- Новый текст §1 не вводит поведения, которого нет ни в одном документе:
иллюстративная фраза тоста сверена с
src/i18n/ru.json:590. - M3: явно проверено, что смена семантики «максимальная ветвь» → «сумма компоненты» не открывает новый путь пропустить нелегитимную запись, потому что любая входная конфигурация с «развилкой» одновременно и обязательно нарушает П1 (угол между двумя почти сонаправленными продолжениями в одном узле физически меньше 15°) — этот вывод не назван в самом ТЗ, но проверяем по коду и не противоречит документу.
Чего не проверял
- Всё, что унаследовано из r1 (список выше) — не перепроверялось повторно.
- Производительность фактическая — по-прежнему нет кода, только текстовая гарантия §2.3/AC3 (линейность по построению правдоподобна, но не измерена).
- Backend-паритет численно — нет кода для запуска
test_parity_with_the_frontend_checks. docs/CONFIG-COMPATIBILITY.md— не тронут дельтой и не должен быть.- Собственно реализация AC6 с учётом M-r2-1 — по определению этой находки: пока сам критерий не различает стороны, доказать реализацию по нему нельзя.
Вердикт
Medium M-r2-1 — в скоупе задачи (наследник не полностью закрытого r1-M2), чинится точечной правкой AC6 без нового цикла продуктового анализа. High нет. Low L2 — на усмотрение автора, можно закрыть заодно с M-r2-1 или отдельной запиской.
Вердикт: жёлтый · заход r2 · блокирующих циклов 1/4 · High: 0 · Medium: 1 → в задаче · Документ: <публикуется шагом публикации>