mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-29 03:09:36 +00:00
@@ -0,0 +1,180 @@
|
||||
# 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 → в задаче · Документ: <публикуется шагом публикации>`
|
||||
Reference in New Issue
Block a user