Files
houseplan-card/docs/reviews/SPEC-REVIEW-258-r1.md
T
2026-08-23 14:18:10 +03:00

18 KiB
Raw Blame History

SPEC-REVIEW-258-r1

  • Issue: #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 коммита, миграции нет), открытых продуктовых вопросов нет и не должно было быть. Задача готова к статусу «Готово к разработке».