mirror of
https://github.com/Matysh/houseplan-card
synced 2026-10-03 13:18:58 +00:00
191 lines
18 KiB
Markdown
191 lines
18 KiB
Markdown
# SPEC-REVIEW-258-r1
|
||
|
||
- Issue: [#258](https://github.com/Matysh/houseplan-card/issues/258) — «На 1.67.0-beta.4 после «Оптимизировать» появились белые клинья в местах схода стен»
|
||
- ТЗ: `docs/specs/258-wall-key-storage-roundtrip.md`, ветка `issue/258-wall-key-storage-roundtrip`, SHA `a830184`
|
||
- Этап: spec (PROCESS.md §2.4), трек — обычный (не `small`/`trivial`, метки `bug`/`P1`/`S4-spec-review`)
|
||
- Заход: r1 · блокирующих циклов израсходовано 0 из 4
|
||
- Вердикт: **зелёный**
|
||
|
||
## Скоуп
|
||
|
||
Регресс после «Оптимизировать»: `wallKey()` квантует середину стены через
|
||
`Math.round`, и для стены нечётной длины в шагах сетки середина попадает точно
|
||
на границу округления. Точное узловое представление вершины и её
|
||
девятизнаковое persisted-представление (#224) дают два разных результата
|
||
округления → два разных строковых `key` для одного и того же физического
|
||
ребра. `lookupWall()` не находит запись по несовпавшему ключу и не спасается
|
||
терпимым запасом (`tol` = ровно полшага = ровно величина ошибки), тогда как
|
||
`thicknessCmAt()` находит её же по точным `a/b` через `exactCoveringWall()`.
|
||
Расхождение потребителей даёт белый клин в T-стыке. ТЗ фиксирует это как
|
||
регресс J1/J6 (`docs/SCOPE.md`) и предлагает: стабилизировать генерацию key
|
||
near-grid нормализацией endpoint'ов, добавить строгий same-span lookup по
|
||
точным `a/b` как средний шаг между exact-key и legacy midpoint fallback, и
|
||
новый точный (без допусков) модельный инвариант `wall_key_mismatch`.
|
||
|
||
## Как проверялось
|
||
|
||
Это исключительно техническая задача (persisted-представление одного и того
|
||
же числа, лишённая пользовательского выбора), поэтому основная работа ревью —
|
||
проверить, что утверждения ТЗ о коде верны, а не гадать о продуктовой
|
||
неоднозначности, которой здесь почти нет.
|
||
|
||
1. Прочитаны `docs/SCOPE.md`, `PROCESS.md` §2.4/§7.1/§7.2, `AGENTS.md`,
|
||
`docs/USER-GUIDE.ru.md` (раздел «Что делает оптимизация» и текст диалога
|
||
Optimize), канонический `docs/WALL-THICKNESS.md` и
|
||
`docs/CONFIG-COMPATIBILITY.md` (раздел «Каноническая геометрия на запись
|
||
(#224)»).
|
||
2. Прочитано тело issue #258 целиком и оба комментария (аналитика владельца +
|
||
ссылка на ТЗ). Открытых продуктовых вопросов не оставлено — подтверждено
|
||
и автором, и содержанием ТЗ (§15, шесть явных технических допущений).
|
||
3. Вытянута ветка `issue/258-wall-key-storage-roundtrip` (SHA `a830184`),
|
||
`git diff` от `dev` показывает только `docs/specs/258-*.md` и одну строку
|
||
в `docs/specs/README.md` — продуктовый код не тронут, что ожидаемо для
|
||
стадии spec.
|
||
4. Каждое техническое утверждение ТЗ сверено с реальным `src/wall-thickness.ts`
|
||
(3382 строки), `src/plan-optimizer.ts`, `src/coordinate-canonicalization.ts`,
|
||
`src/space-geometry.ts`, `scripts/model-invariants.mjs` на этой ветке:
|
||
- `wallKey()`/`q()`/`lookupWall()`/`exactCoveringWall()`/`cmsForPoly()`/
|
||
`edgeKinds()`/`rekeyWallsAfterMove()` существуют ровно с тем поведением,
|
||
которое им приписывает ТЗ (файл `wall-thickness.ts:147-489, 1299-1360`).
|
||
- `GRID_N = 240`, `GRID_STEP_N = 1/240` — `space-geometry.ts:201,205`.
|
||
- `canonicalizeNumber()` округляет до 9 знаков (`COORDINATE_DECIMALS = 9`) —
|
||
`coordinate-canonicalization.ts:8,23-28`, `canonicalizeConfigGeometry()`
|
||
существует и используется в `plan-optimizer.ts:541` и
|
||
`houseplan-card.ts:7042`.
|
||
- `rekeyWallsAfterMove()` уже использует `exactEps = pitch * scale * 1e-6`
|
||
(мин. `1e-9`) — это ровно та величина, которую ТЗ §6.1 предлагает как
|
||
key-epsilon, и ТЗ прямо говорит, что она совпадает с уже используемой
|
||
точностью. Подтверждено чтением (`wall-thickness.ts:449`).
|
||
- `scripts/model-invariants.mjs` не импортирует `src/**`, читает сырой JSON
|
||
— соответствует требованию §8 ТЗ. Существующий `checkReferences()`
|
||
проверяет только «конец записи лежит на ребре комнаты» с допуском
|
||
`EDGE_TOLERANCE = 0.004` (`model-invariants.mjs:23,74-135`) и не
|
||
сравнивает `key` с `wallKey(a,b)` — подтверждает, что предлагаемый
|
||
`wall_key_mismatch` действительно новая проверка, а не дубликат.
|
||
- `test-build/wall-thickness.js` — устоявшийся паттерн проекта
|
||
(`package.json` script `test`, `demo/benchmark_optimize_geometry_preflight.mjs`,
|
||
несколько прошлых `CODE-REVIEW-*.md`), так что AC4's «parity guard с
|
||
`test-build/wall-thickness.js`» — не выдумка, а существующий механизм.
|
||
- Партиции (`space.partitions`) хранят `{a,b,cm}` без производного
|
||
строкового `key` (`houseplan-card.ts:12945`, `partition-openings.ts`) —
|
||
этот класс дефекта их не касается, и ТЗ корректно не включает их в
|
||
scope/non-scope отдельной строкой.
|
||
- В backend (`custom_components/houseplan/**/*.py`) `wallKey`/`wall_key` не
|
||
встречается — допущение §15.6 «backend не меняется» подтверждено.
|
||
5. Численно пересчитан пример из issue в Node (`Math.round`, `GRID_STEP_N`):
|
||
середина стены 1 по точному узлу `83/240` даёt `47.500000000 → 48 →
|
||
0.200000`, по девятизнаковому `0.345833333` — `47.499999960 → 47 →
|
||
0.195833`. Совпадает с issue буквально до шестого знака.
|
||
6. Проверено, что предложенная в §6.1 near-grid нормализация endpoint'ов
|
||
(`eps = max(pitch·1e-6, 1e-9) ≈ 4.1667e-9`) действительно устраняет тай-брейк:
|
||
после снапа `0.345833333` к узлу `83/240` (расхождение `3.33e-10 < eps`)
|
||
середина обеих версий совпадает и даёт один и тот же `key` (`0.200000`).
|
||
Это не «предположение, которое звучит правдоподобно» — я исполнил формулу.
|
||
7. Проверен `docs/USER-GUIDE.ru.md:1374-1414` (диалог Optimize) на предмет
|
||
утверждения ТЗ §7.1 «исправление key считается технической канонизацией
|
||
стен в существующем отчёте» — категории отчёта («обновлённое представление
|
||
стен/связей» и «устранённый вычислительный шум») действительно существуют
|
||
и правдоподобно покрывают этот случай; см. «Находки» Low-1 ниже.
|
||
8. Проверено структурное соответствие §7.1 PROCESS.md: сценарий/персона,
|
||
«что человек увидит», проблема (роль играет §3 «Подтверждённая причина»),
|
||
скоуп/не-скоуп, контракт поведения, UX/touch/perf/security, критерии
|
||
приёмки с доказательством, план тестов (раздел 10 таблица + раздел 11
|
||
шаги 4-6), риски, откат, release-артефакты — все разделы присутствуют по
|
||
содержанию.
|
||
9. Проверено на «догадку, выданную за факт» (§7.1): раздел 15 явно выделяет
|
||
шесть принятых технических допущений вместо того, чтобы включить их
|
||
безадресно в контракт; ни одно из них не является скрытым продуктовым
|
||
решением — все технические (где резолвится repair, накопление в отчёте,
|
||
формат compatibility). Ни одно не требовало эскалации владельцу, что
|
||
совпадает с его собственным «Открытых продуктовых вопросов нет».
|
||
|
||
Гейты `typecheck`/`test`/`build`/`check-docs` не прогонялись: диапазон diff —
|
||
только `docs/specs/**`, продуктовый код (`src/**`) не тронут, стадия spec, а
|
||
не code review. Смоки/golden/invariants аналогично не прогонялись — они
|
||
проверяют поведение кода, которого в этой ветке ещё нет; они относятся к
|
||
будущему code-review циклу этой же задачи.
|
||
|
||
## Находки
|
||
|
||
### Low-1 — «Что человек увидит» смешивает продуктовую и техническую лексику
|
||
|
||
`docs/specs/258-wall-key-storage-roundtrip.md`, раздел 2. Формулировка «До»
|
||
начинается с «Optimize способен создать либо закрепить пару `key` и `a/b`,
|
||
полученную из разных floating-point представлений одного ребра» — это
|
||
описание механизма, а не то, что видит человек. PROCESS.md §7.1 требует эту
|
||
секцию «одной фразой, без терминов реализации»; здесь встречаются `key`,
|
||
`a/b`, «floating-point representations», «midpoint-key».
|
||
|
||
**Почему не блокирует:** несмотря на терминологию, ответ на оба обязательных
|
||
вопроса — какая персона, где, что видно до/после — читается однозначно из
|
||
того же раздела (белый клин в T-стыке, не лечится reload; после — непрерывная
|
||
кладка независимо от того, какая версия ключа была сохранена). Раздел 1
|
||
(«Сценарий») отдельно и чисто формулирует персону/поверхность/момент. AC5/AC6
|
||
проверяют ровно видимый результат, а не строковый key, так что и приёмка не
|
||
зависит от формулировки этого абзаца.
|
||
|
||
**Решение ревьюера:** снимаю как Low с записью, автор может (не обязан)
|
||
переформулировать первую фразу «До» в следующей редакции без блокировки
|
||
текущего захода.
|
||
|
||
## Что проверено и корректно
|
||
|
||
- **Причина бага и её локализация** — подтверждены и чтением кода, и
|
||
численным пересчётом; ни один из числовых примеров ТЗ не «на глаз».
|
||
- **Контракт key (§6.1–6.3)** — численно проверено, что предложенная
|
||
нормализация endpoint'ов действительно устраняет tie-break на нечётных
|
||
длинах, не трогая формат строки и не расширяя допуск (`tol` остаётся
|
||
`pitch/2`, новый epsilon на пять порядков меньше него).
|
||
- **AC1–AC8** — каждый однозначен, у каждого назван способ доказательства
|
||
(`unit`/`smoke`/`golden`/составной gate) в формате, достаточном для DoR
|
||
(§2.5); дисциплина «mutant, убирающий фикс» присутствует в AC1–AC4, что
|
||
соответствует требованию «тест умеет падать» уже на этапе постановки.
|
||
- **Scope/non-scope** — явно исключены смежные форматы/миграции (#224,
|
||
`GRID_N`, bevel-геометрия #249, partial-resize контракт #253, auto-Apply,
|
||
backend/schema/API), что не даёт задаче расползтись на соседние issue.
|
||
- **Совместимость** — legacy `{key, cm}` без `a/b` не трогается; несовпадающий
|
||
key не делает import невалидным и не позволяет удалить запись — это прямое
|
||
и корректное применение принципа `docs/SCOPE.md` «никогда не удалять
|
||
данные пользователя по догадке» к записям толщины.
|
||
- **Инвариант (§8)** — предлагаемая проверка `key === wallKey(a,b,1/240)`
|
||
действительно новая: существующий `checkReferences()` её не делает
|
||
(проверено чтением `model-invariants.mjs`), и она ловит оба случая из
|
||
issue без допусков, в отличие от уже существующей carrier-проверки,
|
||
которая, как верно замечено в issue, стоит ровно на той же границе допуска
|
||
и потому её не ловит.
|
||
- **Технические допущения (§15)** — все шесть действительно технические, не
|
||
подменяют продуктовое решение и не требовали вопроса владельцу; это
|
||
соответствует правилу «смешанный вопрос делится, а не эскалируется целиком»
|
||
и корректно отражено как «открытых продуктовых вопросов нет» в комментарии
|
||
автора.
|
||
- **Артефакты и трассируемость** — issue ↔ ТЗ ссылки в обе стороны на месте
|
||
(issue-комментарий → blob ТЗ; ТЗ → issue в шапке), `docs/specs/README.md`
|
||
обновлён той же веткой.
|
||
|
||
## Чего не проверял
|
||
|
||
- Продуктовый код не существует ещё на этой ветке — реализация, её тесты,
|
||
golden/smoke-сценарии и производительность будут предметом code-review
|
||
этой же задачи, не этого захода.
|
||
- Не проверял вручную UI диалога Optimize (нет кода, нечего запускать);
|
||
утверждение о категории отчёта (Low-1 контекст) проверено только по тексту
|
||
`USER-GUIDE.ru.md`, не по факту работы кода.
|
||
- Не проверял состояние второго пространства владельца (`smt2ntdrc`) — в
|
||
issue это числа без файла, экспортов у меня нет; для ревью ТЗ это не нужно,
|
||
для code-review стоит убедиться, что общий unit/mutation-набор AC1–AC2
|
||
покрывает обе формы (30/20/20/30 см из второго пространства), а не только
|
||
числа первого.
|
||
- Не проверял ссылки на `AUD-159B6-01` и `#201` в таблице рисков — они
|
||
использованы как исторический контекст, не как проверяемое утверждение
|
||
этого ТЗ, и не влияют на выполнимость или проверяемость AC.
|
||
|
||
## Вывод
|
||
|
||
ТЗ технически безупречно для такой узкой числовой задачи: все нетривиальные
|
||
утверждения о коде, формулах и константах подтверждаются чтением исходников
|
||
на этой же ветке и, где это осмысленно, прямым численным пересчётом. Единственная
|
||
находка — Low, стилистическая, снята с запиской. AC проверяемы и однозначны,
|
||
scope/non-scope чёткие, откат тривиален (только revert коммита, миграции нет),
|
||
открытых продуктовых вопросов нет и не должно было быть. Задача готова к
|
||
статусу «Готово к разработке».
|