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

229 lines
20 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# SPEC-REVIEW-330-r1
Issue: #330 · Этап: spec (ТЗ на ревью, PROCESS.md §2.4) · Заход r1 · блокирующих циклов 0/4
Материал: `docs/specs/330-junction-limits-performance.md` на SHA `f1b7c237`
(HEAD ветки `issue/330-junction-limits-performance` на момент ревью), тело issue
#330 и оба комментария (S2-аналитика, объявление ТЗ).
## Скоуп
ТЗ описывает исключительно производительность существующей валидации
ограничений стыков (#329): вынос цепочки валидаторов `ws_config_set`/
`ws_plan_optimize` в executor, rev-кэш счётчиков `previous`, линеаризация П3
(`checkSegmentLengths`/`check_segment_lengths`) через переиспользуемый
byNode-индекс, и фронтовый кэш baseline на конфиг-эпоху. Явно заявлено: ни один
вердикт П1–П5 не меняется. Трек — полный (перф-влияние исключает `small`),
что верно по критериям §5 PROCESS.md.
## Как проверялось
Читкой, без исполнения. Против каждого пункта решения (§4) и каждого AC (§6)
проверялся код, который они описывают, чтобы отделить утверждение от факта:
- `custom_components/houseplan/websocket_api.py:1280-1398` (`ws_config_set`,
синхронная цепочка валидаторов под `write_lock`, MAX_CONFIG_BYTES) —
подтверждает описание проблемы дословно.
- `custom_components/houseplan/junction_limits.py:150-317`
(`collinear_run_length_units`, `check_segment_lengths`,
`check_node_distances`, `validate_junction_limits`) — корень П3
(индекс строится заново на каждый сегмент) подтверждён; заодно проверена
стоимость П4.
- `src/junction-limits.ts:121-169` и `src/houseplan-card.ts:7418-7473`
(`checkSegmentLengths`, `checkNodeDistances`, `_junctionLimitViolations`,
`_junctionLimitsIntroduced`) — та же проверка на фронте, плюс состав
«полной» функции, на которую ссылается перф-бюджет §5.
- `demo/benchmark_safe_resize.mjs` (полностью) — существующий бенч, на
который опирается AC4.
- `grep async_add_executor_job` по `custom_components/houseplan/*.py` —
подтверждение прецедента, на который ссылается §4.1.
- `docs/SCOPE.md`, `PROCESS.md` §2.4/§2.5/§4/§5/§7.1 — рамка ревью и
обязательные разделы.
Не проверялось (и не требовалось на этом этапе): исполнение кода, реальные
замеры производительности — числа взяты из S2-аналитики автора и приняты как
факт (это их роль на этом этапе; воспроизводить их — работа код-ревью).
## Находки
### H1 — Перф-бюджеты §5 недостижимы решением из §4: AC2 и AC6 непроверяемы как написаны
`docs/specs/330-junction-limits-performance.md` §5, AC2, AC6.
Собственные цифры автора (комментарий S2-аналитики и §1 ТЗ) на сетке
12×12 / 576 атомов:
| Компонент | Значение | Кем адресуется в §4? |
|---|---|---|
| Бэкенд, 1× миграция (`commit_wall_segment_model`) | 754 мс | rev-кэш (§4.2) убирает миграцию **только** стороны `previous`; сторона `candidate` мигрируется заново на КАЖДУЮ запись — это неустранимо текущей архитектурой и в §4 не тронуто. |
| Бэкенд, П4 (`check_node_distances`) | 376 мс | Не адресуется никем. Ни §2 (корни), ни §4 не называют П4 квадратичным дефектом — и по коду (`junction_limits.py:211-245`) это не «баг переиспользуемого индекса», как П3, а架构рно O(n²) все-пары-узлов плюс O(n·m) узел-к-стене — линеаризовать так же, как П3, нельзя без отдельного решения (spatial index), которого в ТЗ нет. |
| Фронт, П4 (`checkNodeDistances`) | 105 мс | Аналогично не адресуется. |
Уже из одной только суммы «миграция candidate (754 мс) + П4 бэкенд (376 мс)»
= **1130 мс** — при заявленном тёплом бюджете **≤ 150 мс** (§5) и AC6
«красный при возврате O(n²)». Это не «близко к порогу» — это расхождение на
порядок, и оно основано на цифрах, которые сам автор привёл как измеренные.
То же для фронта: П4 (105 мс) сам по себе почти вдвое превышает бюджет
«полный `_junctionLimitViolations` кандидата ≤ 60 мс», при том что П4 —
только один из компонентов внутри `_junctionLimitViolations`
(`src/houseplan-card.ts:7418-7441`, там же ещё `checkNodes` и цикл
`checkRoomClearance`/`innerContourForRoom` по комнатам, чья стоимость нигде
не измерена и не бюджетирована).
**Почему это находка, а не придирка к цифре.** §4.1–4.4 перечисляют РОВНО
четыре среза изменений (executor-вынос, rev-кэш previous, линейный П3,
фронт-кэш baseline). Ни один не снижает стоимость (а) обязательной миграции
`candidate` на каждую запись, (б) П4 в обеих реализациях. Значит бюджеты §5
либо взяты без пересчёта относительно уже собственноручно измеренных
компонентов (см. таблицу выше — цифры лежат в том же документе/треде), либо
подразумевают пятое, неописанное изменение (например, пространственный
индекс для П4, или отказ от полной миграции candidate). Ни то, ни другое не
названо явно — а значит **автор выдал число за решённое, не проверив его
арифметикой, которую сам же привёл строкой выше**. Это ровно то, что этап
spec-review обязан ловить: не «согласиться», а найти, где ТЗ не проверяемо.
Практическое следствие: реализовав буквально §4.1–4.4 и запустив
`benchmark_junction_limits.mjs` из §5, разработчик получит красный
перф-контракт независимо от качества работы — потому что AC2/AC6 требуют
результата, которого выбранное решение физически не даёт. Это либо
возвращает задачу в «В разработке» после несостоявшегося код-ревью (трата
цикла впустую), либо толкает разработчика тихо расширить скоуп без ТЗ
(находка П4 как quadratic bug, которую §3 явно не резервирует под #331 —
#331 про точность ключей и 0°-дубль, не про производительность П4).
**Что нужно поправить одним из двух способов:**
1. пересчитать бюджеты §5 от реальных достижимых значений (учитывая
неустранимую миграцию candidate и нетронутую стоимость П4), с явным
указанием, откуда цифра взята; или
2. добавить в §2/§4 пятый срез — оптимизацию П4 (обе реализации) и/или
устранение обязательной полной миграции candidate — со своим AC и
доказательством, раз она нужна для достижения заявленных 60/150 мс.
Оба варианта — правка внутри этого же issue (Medium по объёму работы,
но блокирует как High, потому что без неё AC2/AC6 недоказуемы в принципе,
а не «доказуемы с оговоркой»).
### H2 — AC4 ссылается на бенч, который не вызывает изменяемый код
`docs/specs/330-junction-limits-performance.md` AC4:
«…плюс существующий `benchmark_safe_resize` держит прежний pointer-бюджет
на large-house».
Прочитан `demo/benchmark_safe_resize.mjs` целиком: он меряет
`clampEdgeDrag`/`resolveSafeResize`/`clampSafeResize`/`applySafeResize` из
`test-build/resize.js` и `checkOptimizeGeometry` из
`test-build/plan-geometry-preflight.js`. Ни один из этих путей не вызывает
`_junctionLimitsIntroduced`, `checkSegmentLengths` или `checkNodeDistances` —
это отдельная реализация ресайза (низкоуровневый safe-resize примитив), не
тот код, который правит эта задача. Код, который реально меняется
(`_junctionLimitsIntroduced` в `src/houseplan-card.ts:7452-7473`, вызывается
из `_rszSpaceCandidateGeometry` на каждый шаг ресайза, `houseplan-card.ts:
~9041`), в `benchmark_safe_resize.mjs` не участвует вовсе.
Значит регресс именно в том коде, который §330 чинит, этим бенчем **не
будет пойман** — ни до фикса (он и сейчас не показывает проблему, потому что
не меряет её), ни после (он не докажет, что фикс сработал). AC4 как
написан создаёт видимость покрытия там, где его нет: «существующий бенч
держит бюджет» — верное, но нерелевантное утверждение, поданное как
доказательство продуктового обещания «ресайз на large-house держит кадровый
бюджет».
**Что нужно поправить:** либо расширить/создать бенч, который вызывает
`_junctionLimitsIntroduced`/`checkSegmentLengths` в контексте одного жеста
ресайза (это и есть предмет AC4 — «в одном жесте baseline считается один
раз»), либо снять ссылку на `benchmark_safe_resize` из доказательства AC4 и
оставить только юнит на счётчик вызовов (который в AC4 уже есть и сам по
себе корректен).
### M1 — Отсутствуют обязательные разделы ТЗ по §7.1: i18n, touch, риски, release-артефакты
`docs/specs/330-junction-limits-performance.md`, целиком.
§7.1 PROCESS.md перечисляет обязательные разделы ТЗ: сценарий · что человек
увидит до/после · проблема · скоуп/не-скоуп · контракт поведения · UX ·
модель данных и миграция · i18n · AC с доказательством · план автотестов ·
риски · откат · release-артефакты. В документе явно закрыты: сценарий (§1),
проблема (§1), скоуп/не-скоуп (§3), контракт поведения и модель данных
(§4 + §8 «откат» покрывает «формат данных не меняется»), AC (§6), план
автотестов (§7), откат (§8).
Полностью отсутствуют как отдельные утверждения:
- **i18n** — ни слова; для этой задачи корректный ответ тривиален («нет
новых строк»), но он должен быть написан, а не подразумеваться —
DoR (§2.5) требует «ключи en+ru перечислены» именно как явный пункт;
- **touch** — не сказано, что жест ресайза (в т.ч. на touch, best-effort по
`TOUCH-SUPPORT.md`) использует тот же код и получает тот же выигрыш; для
DoR это обязательный явный пункт, а не факт по умолчанию;
- **риски** — нет отдельного раздела (в §2 «Корни» перечислены причины
медленности, но не риски решения: например, что кэш `_junction_baseline`
на runtime-объекте — единственный слот, и конкурентная запись в другое
пространство того же конфига инвалидирует его чаще, чем ожидается —
это не баг, но риск, который стоит явно взвесить в ТЗ, а не оставлять
ревьюеру код-ревью находить постфактум);
- **release-артефакты** — не названы changelog RU+EN, документация,
golden/скриншоты (для чисто перф-задачи, вероятно, не нужны — но
правило §7.1 требует явного «нет», а не отсутствия раздела).
Ничего из этого не блокирует техническую состоятельность решения — все
четыре пункта, скорее всего, разрешаются одной строкой каждый («i18n: нет»,
«touch: тот же код, ускорение применяется одинаково», «риск: единственный
слот кэша инвалидируется по rev, race исключён write_lock», «release:
changelog не нужен — не user-visible поведенчески, только perf»). Но без
явной записи это дыра в DoR §2.5, которая иначе будет обнаружена только на
входе в очередь «Готово к разработке» и вернёт задачу обратно.
## Что проверено и корректно
- Корень П3 (`checkSegmentLengths`/`check_segment_lengths` строит byNode-
индекс заново на каждый сегмент → O(n²)) подтверждён чтением обеих
реализаций — не догадка, реальный дефект, фикс (передать индекс третьим
опциональным параметром) технически корректен и не меняет публичную
сигнатуру для прямых вызовов.
- Синхронность цепочки валидаторов `ws_config_set` под `write_lock` в event
loop подтверждена построчно (`websocket_api.py:1303-1334`); прецедент
`async_add_executor_job` в том же файле и соседних модулях (`__init__.py`,
`http_api.py`) подтверждён — решение §4.1 не изобретает новый паттерн.
- Граница §3 («вердикты П1–П5 не меняются») сформулирована однозначно и
проверяема существующим паритет-тестом и тремя мутантами #329 — это не
голословное заявление, у него есть механизм проверки.
- Откат (§8) корректен: кэши только в памяти процесса, формат данных не
меняется — ревертится действительно чисто.
- AC1, AC3, AC5 однозначны и имеют названный способ доказательства
(backend-тест инструментированного времени; тест счётчика вызовов
`commit_wall_segment_model` через monkeypatch; существующие юниты/паритет-
тест/смоки/мутанты). Догадок, выданных за факт, в этих трёх AC не найдено.
- Продуктовая рамка (§7.1 «сценарий» + «что человек увидит») по существу
присутствует и корректна: персона home admin на десктопе не названа явно,
но однозначно выводится из контекста (сохранение конфига и ресайз — только
редакторские действия) — это не отдельная находка, различие между
«выводится однозначно» и «отсутствует» здесь в пользу автора.
- Открытых продуктовых вопросов, вынесенных владельцу как технические
(§7.1, запрещённый класс), в ТЗ и комментариях не найдено — оба
комментария автора чисто аналитические/технические и решены им самим,
что соответствует правилу «владельцу — только продуктовые вопросы».
## Чего не проверял
- Не запускал `npm test`/`pytest`/бенчи — на этапе spec-review это не
предусмотрено (кода изменения ещё нет, есть только текст ТЗ и уже
существующий, не тронутый этой веткой код).
- Не проверял актуальность файла `AUDIT-2026-08-28.md`, упомянутого в теле
issue — файл не найден в дереве репозитория (`find` не дал результата);
это, вероятно, внешний документ или ещё не закоммитчен. Не блокирует
ревью ТЗ: цифры из него продублированы и подтверждены в S2-комментарии,
который я проверил по коду напрямую.
- Не оценивал точную конструкцию будущего `_junction_baseline` кэша
(структура `counts`, инвалидация на несколько пространств одного
конфига) глубже, чем нужно для проверки конкурентной безопасности под
`write_lock` — это техническая деталь реализации, которую §7.1 явно
оставляет на усмотрение авторов кода, а не ТЗ.
## Вывод
Решение технически обосновано и корни (кроме недостающего П4) описаны
доказательно, не как догадка. Но два High-находки делают документ
непроверяемым в буквальном смысле процесса: AC2/AC6 (H1) и AC4 (H2) не могут
быть подтверждены тем набором изменений и тем бенчем, которые ТЗ само же
описывает. Это возврат автору на правку числа/скоупа решения, не отклонение
идеи задачи.