Files
houseplan-card/docs/reviews/SPEC-REVIEW-330-r2.md
T
2026-08-28 03:06:44 +03:00

17 KiB
Raw Blame History

SPEC-REVIEW-330-r2

Issue: #330 · Этап: spec (ТЗ на ревью, PROCESS.md §2.4) · Заход r2 · блокирующих циклов 1/4

Материал: docs/specs/330-junction-limits-performance.md на SHA 36a4aa70 (HEAD на момент ревью). Предыдущий вердикт: жёлтый, заход r1, документ docs/reviews/SPEC-REVIEW-330-r1.md, получен на SHA f1b7c237.

Скоуп

Ревизия 2 правит два High и один Medium из r1: пересчитывает перф-бюджеты §5 от профилированных чисел (H1), добавляет два новых среза решения — §4.5 (bucket-индекс для П4 в обеих реализациях) и §4.6 (документ текущей версии не мигрируется повторно), переписывает AC2–AC7 под новую доказательную базу и заменяет ссылку AC4 на новый бенч вместо нерелевантного benchmark_safe_resize (H2), добавляет §9 с обязательными разделами i18n/ touch/риски/release-артефакты (M1). Продуктовая рамка (§1: сценарий, что человек увидит) и граница «вердикты П1–П5 не меняются» (§3) — без изменений.

Дельта не локальна: она меняет состав самого технического решения (два новых среза, §4.5/§4.6), весь перф-контракт §5 и весь набор AC — то есть затрагивает больше половины документа и требует полной переверки этой части, а не только «строк, где стоят r1-H1/H2/M1». Разбор ниже — полный по §2–9; инвентаризация §7.1 и продуктовая рамка §1 унаследованы из r1 без повторной проверки (раздел «Унаследовано» ниже) — они делтой не задеты.

Как проверялось

Чтением, без исполнения — на этапе spec иное не предусмотрено. Против каждого нового утверждения решения проверялся код, который оно описывает, чтобы отделить профилированный факт от догадки:

  • custom_components/houseplan/wall_segment_model.py:674-684 (commit_wall_segment_model) — подтверждает предпосылку §4.6: функция прогоняет _migrate_space/_atomize для КАЖДОГО пространства безусловно, включая документ, чей model_version уже равен WALL_SEGMENT_MODEL_VERSION (флаг initial_migration меняет логику присвоения id внутри _migrate_space, но не пропускает атомизацию целиком). Утверждение «деньги не в deepcopy, а в _atomize даже для актуального документа» — не голословно.
  • custom_components/houseplan/junction_limits.py:150-245 (collinear_run_length_units, check_node_distances) — подтверждает, что П4 — все пары узлов (O(n²)) плюс узел×сегмент, отдельный от П3 дефект архитектуры, а не переиспользуемый индекс, как заявлено в §2 п.3 и §4.5.
  • custom_components/houseplan/websocket_api.py:1280-1341 (ws_config_set) — подтверждает, что validate_wall_model_transition (барьер модели) выполняется безусловно и валидирует документ целиком ДО validate_junction_limits — предпосылка §4.6 «барьер ниже по конвейеру по-прежнему валидирует документ целиком» верна, риск «клиент прислал неканонический v9» действительно закрыт независимым барьером, а не только AC5.
  • src/houseplan-card.ts:7418-7473, 12896, 13044 и src/wall-segment-model.ts:821-827 — подтверждают, что фронтенд сам выставляет model_version = 9 при клиентской миграции и различает _serverCfg.model_version >= 9: гипотеза «типичный кандидат уже v9 после первого сохранения» опирается на реальный код, а не на предположение.
  • Существующие файлы, на которые ссылаются AC/§7: test/junction-limits.test.mjs, tests_backend/test_junction_limits.py, demo/smoke_junction_limits.mjs, demo/smoke_island_rooms.mjs, demo/smoke_room_resize.mjs — существуют. demo/benchmark_junction_limits.mjs — не существует (и не должен: код ещё не написан, это предмет реализации).
  • scripts/mutation-gate.mjs:541-580 — три существующих мутанта junction-limit-* подтверждают, что новые имена мутантов в §7 (junction-limit-p3-quadratic-again, junction-limit-p4-bruteforce-again, junction-limit-baseline-cache-stale) следуют реальному соглашению именования, а не придуманы.
  • Арифметика §5 сверена построчно с числами §2 (корни) и с числами r1-H1 (754/376/105 мс) — см. «Что проверено и корректно».

Не проверялось: исполнение прототипов (44 мс/бакет-П4 и т.д. — числа автора, принятые как измеренные там, где так и помечено, и как «ожид.» там, где помечено оценкой). Это работа код-ревью, не этого этапа.

Закрытие раунда r1

Находка r1 Чем закрыта Где видно
H1 — бюджеты §5 недостижимы решением §4 (сумма «миграция candidate 754 мс + П4 бэкенд 376 мс» = 1130 мс против заявленных ≤150 мс) Добавлены два новых среза решения: §4.5 (bucket-индекс П4, прототип 372→44 мс на python, вердикты идентичны) и §4.6 (документ с актуальным model_version используется без повторной миграции — снимает 754/69 мс на типичном горячем пути). Бюджеты §5 пересчитаны от чисел этих прототипов, с явным разделением «тёплый путь» (v9-кандидат, rev-кэш previous) и «холодный» (легаси, единственный случай, укладывается в лимит только потому, что цепочка уже в executor согласно §4.1) docs/specs/330-junction-limits-performance.md §4.5, §4.6, §5 (таблица), AC1/AC2/AC5. Код-предпосылки подтверждены чтением: wall_segment_model.py:674-684, websocket_api.py:1325-1334
H2 — AC4 ссылался на benchmark_safe_resize, который не вызывает изменяемый код AC4 переписан: доказательство — юнит-счётчик на _junctionLimitsIntroduced (как и было) плюс НОВЫЙ бенч §5, который явно описан как измеряющий «TS-проверки П1–П5 напрямую и полный _junctionLimitViolations на смонтированной карточке»; benchmark_safe_resize в тексте AC4 прямо названо как не-доказательство («r1-H2») docs/specs/330-junction-limits-performance.md §5, AC4
M1 — отсутствуют i18n/touch/риски/release-артефакты (§7.1 DoR) Добавлен §9 с четырьмя явными пунктами: i18n «нет», touch «не задет», три названных риска (executor/write_lock, эквивалентность §4.6, геометрическая эквивалентность bucket-П4) с указанной для каждого проверкой, release-артефакты (обычная бета, одна строка CHANGELOG RU+EN, без миграции) docs/specs/330-junction-limits-performance.md §9

Находки

Нет находок уровня High или Medium.

Low-1 (снята решением ревьюера, без правки). Строка §5 «холодный (легаси-обе-стороны)» описывает синтетический худший случай для бенча, а не типичный сценарий «первая запись после обновления» (в котором, по коду фронтенда, candidate уже несёт model_version: 9, а легаси остаётся только previous). Название не искажает AC и не меняет бюджет — бенч просто держит более тяжёлый из двух легаси-случаев с запасом. Не блокирует: ни один AC не опирается на точное соответствие названия сценарию, и §2 отдельно описывает реальный случай смешанной версии.

Low-2 (снята решением ревьюера, без правки). Ряд §5 «TS checkNodeDistances (П4, 576) ~15 мс» помечен как «ожид.», но в тексте §4.5 измерен прототип только для python (372→44 мс); TS-число — экстраполяция по той же пропорции, не отдельный замер. Это не «догадка, выданная за факт»: колонка честно озаглавлена «После фикса (ожид.)», и AC2/AC7 всё равно требуют подтверждения бенчем и мутантом на этапе код-ревью — оценка не подменяет доказательство.

Что проверено и корректно

  • Арифметика бюджетов §5 сходится с числами §2 и r1: тёплый путь (линейный П3 + bucket-П4 44 мс + отсутствие повторной миграции v9-кандидата) даёт порядок «около 100 мс» бэкенд / «около 40 мс» фронт — это прямо снимает расхождение на порядок, из-за которого r1 поставил H1. Холодный путь (~1.7 с) остаётся только в executor (AC1 требует loop-времени, а не общего CPU-времени), поэтому попадание в ≤50 мс loop-бюджета не зависит от того, насколько медленна миграция легаси-документа.
  • §4.6 не противоречит #329: барьер validate_wall_model_transition (websocket_api.py:1325) выполняется независимо от лимитов и валидирует документ целиком, так что пропуск повторной миграции в путях лимитов не открывает дыру для непроверенного «поддельного v9».
  • AC5 (эквивалентность §4.6) сформулирован проверяемо: конкретные фикстуры границ (14°/15°, 19/20 см, T-стык, доборный атом), существующий механизм — паритет-тест, который уже есть в test/junction-limits.test.mjs/ tests_backend/test_junction_limits.py.
  • AC4 больше не создаёт видимость покрытия: новый бенч явно вызывает именно изменяемый код, а не соседний примитив ресайза.
  • §9 закрывает все четыре пункта M1 явными и по существу корректными ответами (в частности, риск «неканонический v9» получает named mitigation, а не декларацию).
  • Имена новых мутантов и ссылки на существующие смоки/тесты в §7 соответствуют реальному дереву репозитория — не выдуманы.
  • Открытых продуктовых вопросов владельцу по-прежнему нет; ревизия 2 — целиком технический ответ на технические находки r1, что и требовалось (§7.1: технический спор автора и ревьюера решается вердиктом ревью, не эскалируется).

Чего не проверял

  • Не запускал npm test/pytest tests_backend/typecheck/бенчи — на этапе spec-review это не предусмотрено PROCESS.md §2.4 (кода этой задачи ещё нет; тронут только текст ТЗ).
  • Не выполнял прототипы автора (cProfile/bucket-индекс) — их числа приняты как измеренные там, где это прямо написано («по профилю», «прототип на python»), и как оценка там, где написано «ожид.» (см. Low-2). Воспроизвести эти числа под нагрузкой — работа код-ревью по AC1/AC2/AC7.
  • Не проверял детали будущей структуры bucket-индекса П4 (шаг сетки, 9-окрестность) глубже, чем нужно для проверки правдоподобия заявленного ускорения — §7.1 оставляет конструкцию реализации на усмотрение авторов кода.
  • Не проверял AUDIT-2026-08-28.md (упомянут в теле issue) — не найден в дереве репозитория; как и в r1, не блокирует: цифры продублированы и подтверждены в аналитике и в этом документе напрямую по коду.

Унаследовано из r1

Без повторной проверки в этом раунде — делта их не касается:

  • Продуктовая рамка §7.1 («сценарий», «что человек увидит», персона home admin на десктопе выводится из контекста) — принята в docs/reviews/SPEC-REVIEW-330-r1.md на SHA f1b7c237, текст §1 не менялся в ревизии 2 (сверено диффом f1b7c237..36a4aa70).
  • Корень П3 (collinear_run_length_units строит индекс заново на каждый сегмент) и синхронность цепочки ws_config_set под write_lock в event loop — подтверждены чтением кода в r1 (junction_limits.py, websocket_api.py:1303-1334), в этом раунде код не менялся, повторно не перечитывал построчно (кроме диапазона 1280-1341, проверенного заново для H1/§4.6 — см. «Как проверялось»).
  • Граница §3 «вердикты П1–П5 не меняются ни на бит» и механизм её проверки (паритет-тест + три мутанта #329) — принята в r1, текст границы в ревизии 2 расширен (добавлена одна строка про атомизацию вне скоупа), но сама граница и её проверяемость не изменились.
  • Откат §8 (чистый revert, кэши только в памяти) — не менялся, принят в r1.
  • Отсутствие открытых продуктовых вопросов владельцу — подтверждено в r1 и остаётся верным: оба цикла (r1→r2) были чисто техническими.

Вывод

Оба High и Medium из r1 закрыты по существу, с кодовым подтверждением ключевых новых предпосылок (§4.6 — барьер модели независим и валидирует документ целиком; премисса «типичный кандидат уже v9» опирается на реальный код фронтенда, а не на догадку). Новых High/Medium решение не вносит. Вердикт: зелёный.