Files
houseplan-card/docs/reviews/SPEC-REVIEW-331-r2.md
T
2026-08-28 01:27:57 +00:00

181 lines
20 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 без
повторной проверки, см. раздел ниже.
## Как проверялось
1. `docs/SCOPE.md` — не перечитывался заново (не задет дельтой); вывод r1
(«соответствует J6») наследуется.
2. `PROCESS.md` §2.4/§2.9/§7.1/§7.2 — перечитаны для формата документа и
verdict-строки этого раунда.
3. Тело issue #331 и оба исходных комментария (уже читаны в r1) плюс два
новых комментария: вердикт r1 (`claude`, красный) и хендофф автора о
ревизии 2 — оба через `gh issue view 331 --comments`.
4. `git diff 60e125e9..2d703543 -- docs/specs/331-junction-limit-precision.md`
— построчно, каждая правка сверена с текстом находки, которую она
заявляет закрывающей.
5. Для проверки арифметики и терминологии H1/M1 — код: `src/
coordinate-canonicalization.ts` и `custom_components/houseplan/
coordinate_canonicalization.py` (формула `sign(v)·floor(|v|·factor+0.5)/
factor` подтверждена побайтово в обоих файлах — не изобретена заново).
6. Для проверки M3 (семантика «сумма компоненты» вместо «максимальной
ветви») — текущая реализация `collinearRunLengthUnits` (`src/
junction-limits.ts:133-158`) и `checkNodes`/П1 (`:63-96`), чтобы оценить,
действительно ли переопределение алгоритма меняет наблюдаемое поведение
на легитимных (не патологических) входах.
7. Для M4 — существование `docs/USER-GUIDE.md` (английское зеркало) рядом с
`.ru.md`, и раздел «Ограничения стыков стен» (`docs/USER-GUIDE.ru.md:420-
444`) как целевое место правки.
8. 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|·1e7+0.5)/1e7` с ссылкой на `coordinate-canonicalization`; AC1 добавляет прямой юнит на `v=2.5e-7` | `docs/specs/331-…md:43-51`, AC1 `:127-128`. Формула сверена побайтово с `src/coordinate-canonicalization.ts:47-50` и `.py:20-31` — совпадает. Для `v=2.5e-7`: `floor(2.5+0.5)=3` в обеих реализациях (не `Math.round`/банковское округление) → один ключ `3e-7`. **Закрыто.** |
| **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.md` J6.
- §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 → в задаче · Документ: <публикуется шагом публикации>`