Issue: #291 User-Visible: no
26 KiB
CODE-REVIEW-291-r2
- Issue: #291 «Барьер записи координат: убрать шум как явление»
- Этап: код-ревью (PROCESS.md §2.7), заход r2
- Диапазон:
52c74301..3c754729(merge-base сorigin/dev.. HEAD веткиissue/291-lattice-coordinate-barrier), 12 коммитов, 38 файлов, +2696/-327 - ТЗ:
docs/specs/291-lattice-coordinate-write-barrier.md, зелёное ревьюSPEC-REVIEW-291-r3.md - Ревьюер: свежая сессия, без контекста реализации; артефакты читаны из
ветки и через
gh, не со слов автора
Почему это ПОЛНЫЙ разбор, а не разбор по дельте
Вердикт r1 (CODE-REVIEW-291-r1.md) получен на диапазоне
10fa0dc7..81d2a801. Между r1 и этим заходом ветка была ребейзнута на
ушедший вперёд dev после слияния #290 (комментарий автора 14:17:38 UTC:
«Ребейз на актуальный dev после слияния #290 завершён»). Проверено прямо:
$ git cat-file -e 81d2a801^{commit}
fatal: Not a valid object name 81d2a801^{commit}
$ git cat-file -e 7de1f44^{commit} # коммит с фиксами M1/M2 из r1
fatal: Not a valid object name 7de1f44^{commit}
Оба SHA, на которые ссылался r1 (диапазон ревью и коммит с фиксами),
переписаны и не существуют в дереве. Это буквально случай PROCESS.md §2.10 /
AGENTS.md §7.2: «после ребейза на ушедший вперёд dev это другой код» —
разбор по дельте здесь неприменим, весь диф пройден заново.
Скоуп
Единый барьер записи, приводящий координаты, лежащие ближе 1e-4 шага сетки
к узлу k/240, к точному double этого узла — во frontend и backend, в layout
и config, с отдельным отчётом Optimize и защитой от обхода. Не входит: смена
схемы хранения, #288/#289/#290, union-алгоритм #278, снап авторской off-grid
геометрии.
Как проверялось
Гейты по PROCESS.md §8, соразмерно задаче (диапазон трогает src/**,
custom_components/**/*.py, геометрию и ссылки на неё):
| Гейт | Команда | Результат |
|---|---|---|
| Typecheck | npx tsc --noEmit |
зелёный |
| Unit | npm test |
1242 passed, 1 skipped, 0 failed (было 1232 на r1 — рост за счёт test/coordinate-write-barrier-guard.test.mjs и правок M1/M2) |
| Build + bundle parity | npm run build && npm run bundle:sync + cmp трёх копий |
dist/, custom_components/houseplan/frontend/, demo/srv/assets/ побайтово идентичны |
| Docs fingerprint | node scripts/check-docs.mjs |
«Documentation checks passed (7 files, 10 external links)» — обязателен, diff трогает src/** |
| Model invariants | node scripts/model-invariants.mjs --config test/fixtures/real-plan-first-floor.json --lattice (raw fixture) |
66.15% шума (127/192), худший 8.00e-8 шага — воспроизводит измеренную причину issue буквально; после барьера (через npm test, см. ниже) — 0 |
| Model invariants (после барьера) | npm test → test('барьер очищает клоны реальных планов, не переписывая source fixtures (#291)') |
зелёный: profile.noise===0 на клонах обоих реальных планов, checkWallKeys/checkMixedRoleRecords=0, raw-файл байт-в-байт неизменен |
| Mutation gate (структурная проверка) | node scripts/mutation-gate.mjs --check |
все патчи, включая 6 lattice-мутантов, применяются к текущему коду ровно один раз |
| Mutation gate (полный прогон, все 6 lattice-мутантов) | --id=lattice-round-truncates, --id=frontend-writes-raw-coords, --id=lattice-noise-threshold-too-small, --id=lattice-noise-threshold-too-large, --id=lattice-layout-allowlist-omitted, --id=lattice-unknown-fields-recursive |
все 6: мутант применён → именованный тест покраснел, гейт зафиксировал ловлю (python-lattice-round-truncates не прогнан — backend, см. «Не проверял») |
| Performance (AC11) | node demo/benchmark_coordinate_write_barrier.mjs |
ratio 0.608 (лимит 1.2), независимый прогон |
| Smoke — выбор | node scripts/smoke-select.mjs --base origin/dev --head HEAD |
3 «прямых совпадения» (cellCm) + 1 «зарегистрированная связь» (repairSpaceReferences) — те же, что на r1 |
| Smoke — по AC4/AC7 | smoke_lattice_write_barrier, smoke_optimize_coordinate_canonicalization |
оба OK, все под-проверки true |
| Smoke — композиционное доказательство AC4 (M1) | smoke_wall_chain_thickness, smoke_room_resize, smoke_opening_preview, smoke_free_walls, smoke_decor, smoke_drag_bounds |
все 6 OK — реальные production controller paths (wall chain, resize, opening/partition/column, decor, marker/label drag) |
| Smoke — по smoke-select | smoke_grid_scale_invariance, smoke_space_scale_defaults, smoke_orphan_space_references |
все OK |
| Backend | python -m pytest tests_backend -q |
не выполнено — среда без pytest/homeassistant (см. «Не проверял»); backend-.py не менялся ни одним коммитом после 4ccf4a3c (см. ниже), значит это тот же код, что был зелёным в CI |
| Golden | не прогонялся | сознательное решение, см. «Не проверял» |
| Process gate | node scripts/process-gate.mjs --github-range --issues локально на HEAD |
«гейт пройден, предупреждений 0» |
| Process gate (CI, точный SHA) | gh run view на run 32737681761 (HEAD 3c754729) |
success; docs/process-gate/provenance реально выполнены и зелёные |
Вывод smoke-select.mjs (идентичен r1 — диф по src/** не расширился сверх
трёх изменённых файлов):
Изменено файлов src/**: 3 · символов проекта на изменённых строках: 26
Прямое совпадение (3): smoke_decor.mjs ← cellCm; smoke_grid_scale_invariance.mjs ← cellCm;
smoke_space_scale_defaults.mjs ← cellCm
Зарегистрированная связь (1): smoke_orphan_space_references.mjs ← repairSpaceReferences
Итого прогнано лично 11 браузерных смоков (2 по AC + 6 по композиции AC4 + 3 по smoke-select), все зелёные.
CI на финальном SHA — что реально проверено автоматикой, а что нет
Проверил независимо от слов автора, через gh run view --json jobs:
3c754729(текущий HEAD): Validatesuccess, ноchanges-job отфильтровал диапазон последнего пуша (толькоdocs/images/screenshots.json) —frontend/backend/smoke/golden/performance_smokeskipped, не «зелёные», а не запускались вовсе;81d2a801(пуш со скриншотами, SHA которого больше не существует после ребейза): та же картина — все браузерные/backend job'ы skipped;9a42c85(SHA пре-ребейз реализации, тоже переписан): здесьfrontend/hacs/hassfest/backendреально success до отмены прогона следующим пушем;golden/smoke/performance_smoke—cancelled(штатное поведениеconcurrency.cancel-in-progress), а не «зелёные».
Вывод: ни один CI-прогон в истории этой задачи (ни до, ни после ребейза) не
довёл golden/smoke/performance_smoke до завершения. Это не искажение со
стороны автора (в хендоффах формулировка точная — «Linux backend … зелёный»,
без утверждений про golden/smoke), но практическая проверка ложится на
код-ревью, что и сделано выше (11 смоков лично, бенчмарк лично, invariants
лично). Backend — единственный пробел, не покрытый ни CI на актуальном коде,
ни локальным исполнением (см. «Не проверял»), закрыт чтением и тем, что файл
не менялся с последнего зелёного backend-прогона.
Закрытие раунда r1
| Находка r1 | Чем закрыта | Где это видно |
|---|---|---|
M1 (Medium) — AC4 не доказан ни автотестом, ни явной записью: smoke_lattice_write_barrier инъецировал шум напрямую в _serverCfg/_layout, минуя контроллеры |
ТЗ переписан (AC4, коммит 3f633888): явно зафиксирована композиционная схема доказательства — 6 production-bundle смоков реальных контроллеров + executable writer-inventory guard + smoke_lattice_write_barrier на уровне общей границы. docs/TESTING.md получил два новых пункта с той же формулировкой и списком [auto: …]. Комментарий в самом смоке (smoke_lattice_write_barrier.mjs:96-99) прямо называет разделение ответственности |
docs/specs/291-lattice-coordinate-write-barrier.md §AC4 (текст «Поведение доказывается композицией, а не вторым mega-smoke»); docs/TESTING.md два новых пункта [auto: ...]; прогнал лично все 6 controller-смоков + writer-inventory тест — все зелёные (таблица выше) |
M2 (Medium) — docs/USER-GUIDE.ru.md не обновлён при User-Visible: yes, хотя docs/USER-GUIDE.md (EN) получил абзац |
RU-раздел 19 получил абзац с тем же контрактом: общий счётчик, максимум с тремя значащими цифрами, scientific notation ниже 0.001 см, только затронутые пространства, неизменность off-grid значений | git diff origin/dev...HEAD -- docs/USER-GUIDE.ru.md — непустой, абзац на месте (см. «Что проверено» ниже за точный текст) |
L1 (Low) — хендофф цитировал cancelled прогон как «зелёный» для backend-job без упоминания отмены |
Не требовал правки (наблюдение о точности цитирования, не о коде); не блокирует, статус не менялся | — |
Обе Medium-находки закрыты по существу и проверены построчно (диффы выше,
не заявление автора), не сняты игнорированием. Third M2-related risk —
не перепутана ли RU-формулировка с устаревшей — проверена: абзац называет
3.30e-5 cm-подобную scientific-notation политику и «только затронутые
пространства», то есть терминологию именно #291, а не старый общий текст
раздела 19 (написанный до ветки задачи).
Унаследовано из r1
Поскольку разбор в этом заходе полный (см. «Почему это полный разбор»
выше), формально «наследовать без проверки» нечего — весь диф пройден заново
этим документом. Единственное, что взято как факт без повторной проверки:
зелёный вердикт ревью ТЗ (SPEC-REVIEW-291-r3.md, SHA 4ec43ffd+ и
23fb41d4+, три захода, зелёное на r3) — ревью кода не пересматривает
продуктовые решения владельца, зафиксированные на этапе ТЗ (Q1 про
детализацию Optimize-отчёта, threshold 1e-4, allow-list полей).
AC — построчно (повторная проверка, полный разбор)
| AC | Доказательство | Вердикт |
|---|---|---|
| AC1 Идемпотентность/точные биты | test/coordinate-canonicalization.test.mjs: 4801 узел, идемпотентность, -0→+0, 0.06/0.2875 не снапятся, граница 1e-4 — в npm test (зелёный) |
✅ доказано автотестом, выполнено |
| AC2 Shared allow-list parity | Общая fixture, deep-equal TS/Python; TS-часть выполнена (npm test); backend-часть проверена чтением (файл не менялся с последнего зелёного backend CI) |
✅ доказано автотестом (TS) + чтением (backend) |
| AC3 Живой профиль после migration = 0 | test/model-invariants.test.mjs: real-plan clone → noise=0, checkWallKeys/checkMixedRoleRecords=0, raw byte-equal — зелёный в npm test; плюс мой независимый CLI-прогон на сыром real-plan-first-floor.json (66.15% шума, воспроизводит измерение issue) |
✅ доказано автотестом + исполнением |
| AC4 Произвольная editing session не возвращает noise | Композиционное доказательство теперь явно задокументировано (закрытие M1 выше) и лично прогнано целиком: 6 production controller smokes + writer-inventory guard test + smoke_lattice_write_barrier (граница) — все зелёные |
✅ доказано композицией автотестов, все компоненты лично исполнены |
| AC5 Write barrier нельзя обойти | test/coordinate-write-barrier-guard.test.mjs (инвентарь всех frontend-writer'ов + backend Store boundary); мутанты lattice-round-truncates/frontend-writes-raw-coords лично прогнаны — красятся |
✅ доказано автотестом, выполнено |
| AC6 Source fixtures остаются шумными | Тест в npm test: raw-файл байт-в-байт равен до/после, profile.noise>=100 пин остаётся |
✅ доказано автотестом, выполнено |
| AC7 Optimize report и transaction | smoke_optimize_coordinate_canonicalization (лично, зелёный, 13/13); порядок операций в plan-optimizer.ts проверен чтением: lattice-отчёт измеряется один раз до мутаций (строка 427), финальный safety-pass канонизации (строки 649-650) явно прокомментирован как «internal work must not leak into the user-visible report» и гейтится тем же changed-флагом, что и остальные счётчики — не создаёт второго источника числа |
✅ доказано автотестом + smoke + чтением, отдельно проверено на предмет «одно число — один источник» после интеграции с #290 (near-axis repair/повторный align) |
| AC8 Compatibility paths | Существующие тесты не тронуты диффом сверх заявленного; no schema/version bump | ✅ проверено чтением + существующими тестами |
| AC9 Polyclip/#278 regression | npm test включает все 5 тестов #278 …, зелёные (видел в хвосте прогона: #278 anonymized regression…, #278 component set is deterministic…, #278 Optimize and model-invariants use the same strict structural result, #278 physical fingerprint…, #278 production source routes physical writers…) |
✅ доказано автотестом, выполнено |
| AC10 Мутанты | 6 lattice-мутантов — все лично прогнаны и красятся (таблица гейтов); backend-мутант не прогнан (среда, см. «Не проверял») | ✅ доказано исполнением (frontend), backend — чтением |
| AC11 Performance | demo/benchmark_coordinate_write_barrier.mjs, ratio 0.608 (я, независимый прогон), лимит 1.2 |
✅ доказано бенчмарком, выполнено |
| AC12 Локальные гейты | typecheck/test/build/bundle-parity/check-docs — выполнены и зелёные (таблица выше); backend — не выполнен (среда) | ✅/⚠️ частично, backend — см. «Не проверял» |
Находки
Находок уровня High или Medium в этом заходе нет. Обе находки r1 закрыты (таблица выше), нового Medium/High разбор не вскрыл.
Low — не блокируют
- L2 (унаследовано из r1/спек-ревью, не относится к коду): формулировка тела issue «Стадия 1 из ADR #282» технически неточна (по ADR #282 «Стадия 1» — stable wall ids, явно вынесенные в «Не входит» этого же ТЗ). Не влияет на код-ревью, отмечен ещё на этапе ТЗ, не блокирует.
- L4 (новое наблюдение этого захода) — ни один CI-прогон в истории ветки
не довёл
golden/smoke/performance_smokeдо завершения (см. раздел про CI выше): либоchanges-path-filter не включил их (докозаписи пуши), либо следующий пуш отменил незавершённый прогон. Не дефект процесса (штатное поведениеconcurrency.cancel-in-progressи путевого фильтра, задокументированное в r1), но фактическую нагрузку по этим трём категориям целиком несёт код-ревью — что и сделано (11 смоков + бенчмарк лично). Не блокирует, фиксирую как честное описание происхождения зелёных цифр.
Что проверено и корректно
- Ребейз не сломал заявленный порядок операций.
plan-optimizer.ts:427-432измеряет lattice-отчёт и применяет барьер доalignAllToGrid/repairNearAxisRoomWalls(новый шаг #290); финальный safety-pass вcanonicalizeConfigGeometryInPlace/canonicalizeLayoutGeometryInPlace(строки 649-650) идёт после второго align-прохода для выпрямленных стен и явно прокомментирован как «internal work… must not leak into the user-visible report» — то есть отчёт lattice не задваивается и не занижается интеграцией с #290. Автор описал это словами в хендоффе («canonicalization до Align → near-axis repair → повторный Align → финальная canonicalization»); я проверил это построчно по коду, а не поверил формулировке. - «Одно число — один источник» для нового placeholder
{cm}.formatLatticeShiftCm(использован в диалоге Optimize) иlatticeReport()/бенчмарк используют одну и ту же функцию форматирования с тремя значащими цифрами; не смешивается с обычной политикой округленияmoved/maxShiftCmgrid-align (разные ключи i18n, разные поля отчёта). - Backend не менялся с последнего зелёного backend-CI.
git log -- 'custom_components/houseplan/*.py'в диапазоне задачи показывает единственный коммит4ccf4a3c— тот же контент, что был протестирован в run32732620838(backend: success, до отмены прогона следующим пушем). Ни один последующий коммит backend-файлы не трогал — чтение актуального кода эквивалентно чтению того, что уже прошло CI. - i18n.
gs.optimize_lattice_summary/gs.optimize_lattice_spaceприсутствуют вen.json/ru.jsonбуквально как в ТЗ §7.1; проверил оба файла напрямую. - Трейлеры. Все 12 коммитов диапазона несут
Issue: #291иUser-Visible: yes|no; единственныйUser-Visible: yes(4ccf4a3c) правит обаdocs/CHANGELOG.md/docs/CHANGELOG.ru.mdв одном коммите (проверил дифф обоих файлов напрямую). - Process gate. И локальный прогон, и CI на точном финальном SHA
(
3c754729, run32737681761) зелёные. - Гвард
scripts/coordinate-write-barrier-guard.mjs— структурная проверка количества writer call-site'ов (1 config/set, 2 layout/update, 1 localStorage, 1 Optimize write во frontend; central Store writer'ы в backend) читана целиком: любой новый writer, обходящий единую границу, меняет счётчик occurrences и краситcheckCoordinateWriteBarriers.
Чего не проверял и почему
- Backend pytest (
tests_backend/test_coordinate_canonicalization.py): среда ревью не имеетpytest/homeassistant(.venv-backendотсутствует, это известное ограничение не-облачного окружения, AGENTS.md). Заменил чтением плюс фактом, что backend-.pyне менялся с последнего зелёного backend CI-прогона (32732620838, до отмены следующим пушем) — см. «Что проверено». npm run golden:verify: не прогонял. Тест'модели проекта не несут шума решётки (#282)'(зелёный вnpm test) доказывает, что golden-фикстуры не содержат lattice-шума — значит новые условные строки Optimize-диалога (${r.latticeCoordinatesCanonicalized ? …}) не рендерятся в golden-сценариях и визуального расхождения не должно быть. Golden — предрелизный гейт (PROCESS.md §8), не гейт код-ревью.- Полный набор из 183 браузерных смоков: не прогонял, кроме выбранных 11 (2 AC + 6 композиция AC4 + 3 smoke-select) — по правилу §8 «полные наборы — гейт предрелиза».
- Полный HA-харнесс / WSL: недоступен в среде ревью.
- Не проверял отдельно совместимость с #288/#289/#290 сверх того, что их
регрессионные тесты (в т.ч.
#278 …,#290 near-axis audit…) прошли вnpm testбез изменений в диффе, не относящихся к #291.
Итог
High-находок нет. Обе Medium-находки r1 (M1 — композиционное доказательство
AC4 не было названо явно; M2 — docs/USER-GUIDE.ru.md не обновлён) закрыты
по существу и проверены построчно, не заявлением автора. Разбор в этом
заходе полный, а не по дельте, потому что ребейз на ушедший вперёд dev
переписал оба SHA, на которые ссылался r1 (диапазон ревью и коммит с
фиксами) — код на ветке буквально другой. Ядро реализации — канонизация,
единый непроходимый write barrier, backend-зеркало, идемпотентность,
защита от обхода, производительность, интеграция с новым near-axis/align
пайплайном #290 — проверено заново и работает, включая независимое
исполнение 11 браузерных смоков, 6 мутантов, бенчмарка и invariants на
реальных данных.
Вердикт: зелёный · заход r2 · блокирующих циклов 1/4 · High: 0 · Medium: 0