18 KiB
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, SHAa830184 - Этап: 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-представление одного и того же числа, лишённая пользовательского выбора), поэтому основная работа ревью — проверить, что утверждения ТЗ о коде верны, а не гадать о продуктовой неоднозначности, которой здесь почти нет.
- Прочитаны
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)»). - Прочитано тело issue #258 целиком и оба комментария (аналитика владельца + ссылка на ТЗ). Открытых продуктовых вопросов не оставлено — подтверждено и автором, и содержанием ТЗ (§15, шесть явных технических допущений).
- Вытянута ветка
issue/258-wall-key-storage-roundtrip(SHAa830184),git diffотdevпоказывает толькоdocs/specs/258-*.mdи одну строку вdocs/specs/README.md— продуктовый код не тронут, что ожидаемо для стадии spec. - Каждое техническое утверждение ТЗ сверено с реальным
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.jsonscripttest,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 не меняется» подтверждено.
- Численно пересчитан пример из issue в Node (
Math.round,GRID_STEP_N): середина стены 1 по точному узлу83/240даёt47.500000000 → 48 → 0.200000, по девятизнаковому0.345833333—47.499999960 → 47 → 0.195833. Совпадает с issue буквально до шестого знака. - Проверено, что предложенная в §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). Это не «предположение, которое звучит правдоподобно» — я исполнил формулу. - Проверен
docs/USER-GUIDE.ru.md:1374-1414(диалог Optimize) на предмет утверждения ТЗ §7.1 «исправление key считается технической канонизацией стен в существующем отчёте» — категории отчёта («обновлённое представление стен/связей» и «устранённый вычислительный шум») действительно существуют и правдоподобно покрывают этот случай; см. «Находки» Low-1 ниже. - Проверено структурное соответствие §7.1 PROCESS.md: сценарий/персона, «что человек увидит», проблема (роль играет §3 «Подтверждённая причина»), скоуп/не-скоуп, контракт поведения, UX/touch/perf/security, критерии приёмки с доказательством, план тестов (раздел 10 таблица + раздел 11 шаги 4-6), риски, откат, release-артефакты — все разделы присутствуют по содержанию.
- Проверено на «догадку, выданную за факт» (§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 коммита, миграции нет), открытых продуктовых вопросов нет и не должно было быть. Задача готова к статусу «Готово к разработке».