Files
houseplan-card/legacy/reviews/v1.68.0/SPEC-REVIEW-282-r1.md
T
Claudeandclaude[bot] 0991c45374 fix(tools): архив переписывает относительные ссылки перенесённых документов (#682)
Ревью #682 r1, Medium: перенос добавляет документу уровень вложенности
(`docs/reviews/X.md` → `legacy/reviews/<тег>/X.md`, `docs/specs/` →
`legacy/specs/`), а относительные ссылки внутри перенесённых документов и в
соседях, ссылавшихся на них, никто не пересчитывал — на `97d19268` 53 битые
ссылки в 46 файлах (заявление «все 26 резолвятся» в `7feb6177` было верно
только до переноса документов ревью). Гейты архив не смотрят.

`reviews-archive.mjs`: `repairLinks` пересчитывает ссылку, если она не
резолвится от нового места, а цель находится от нового или старого места
через карту переносов; битая и до переноса ссылка не трогается. `--apply`
делает это само, `--repair-links=<rev>` — для всех переименований
`<rev>..HEAD`, `--check-links` печатает битые. Этим коммитом
`--repair-links=origin/dev` переписал ровно 53 ссылки в 46 файлах; остались
две прежние «...»-заглушки в CODE-REVIEW-448-r2 (битые и на dev). Тесты:
перенесённый документ, сосед со ссылкой в архив, ТЗ со ссылкой на позже
перенесённое ревью, битая-до-переноса не трогается, в `legacy/` битых нет;
мутант `reviews-archive-links-from-new-place-only`. PROCESS §2.10 и
DEVELOPMENT › Release называют переписывание и `--check-links`.

Issue: #682
User-Visible: no
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
2026-09-27 22:10:47 +00:00

20 KiB
Raw Blame History

SPEC-REVIEW-282-r1

Скоуп ревью

Issue #282 — зонтичная задача о смене модели геометрии стен, реализуемая стадиями (ADR). Текущий рабочий цикл поставляет только Stage 1: стабильный persisted id для атомарных contour-сегментов, ссылки комнат на эти id, миграция v7→v8, compatibility-проекция для старого рендерера и старых клиентов. Никакого визуального/UX-изменения не заявлено, кроме одного нового отказного состояния (не удалось безопасно смигрировать план).

Ревью не покрывает продуктовое качество ADR Stage 0-4 в целом (ADR принят владельцем 2026-08-24 как направление) — только ТЗ Stage 1: выполнимость, однозначность, доказуемость AC1…AC17 и соответствие docs/SCOPE.md.

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

  1. Прочитаны docs/SCOPE.md, AGENTS.md, PROCESS.md целиком.
  2. Прочитано тело issue #282 и все 4 комментария (включая измерение владельца по Stage 0: 65–79% координат его живой инсталляции — «шум» у узла решётки, и признание, что тестовые фикстуры проекта этот класс дефектов не воспроизводят).
  3. Прочитан ADR docs/adr/282-wall-geometry-representation.md целиком.
  4. Прочитан ТЗ docs/specs/282-stable-wall-segment-identity.md целиком (645 строк, §1–§19).
  5. Прочитаны канонические документы затронутой подсистемы — docs/WALL-THICKNESS.md, docs/CONFIG-COMPATIBILITY.md — целиком, для проверки, что ТЗ описывает существующую модель точно, а не по памяти/догадке.
  6. Факт-чек утверждений ТЗ по исходному коду (не запуская тесты — на этом этапе продуктового кода для #282 ещё нет, разбор чтением):
    • PLAN_MODEL_VERSION = 7 в src/plan-optimizer.ts:39 и custom_components/houseplan/const.py:54 — подтверждает заявленный переход v7→v8 (§0, §7.3, §10) как факт, а не предположение;
    • MAX_ROOMS = 400 (custom_components/houseplan/validation.py:909, src/houseplan-card.ts:648) и MAX_POLY_POINTS = 500 (validation.py:930) дают 200 000, что дословно совпадает с лимитом §13;
    • структура host?: { kind: 'partition'; id: string; t: number } (src/opening-placement.ts:46) подтверждает исходное состояние, которое §6.4 расширяет до tagged union 'partition' | 'wall';
    • wallKey, rekeyWallsAfterMove, exactCoveringWall, edgeKinds, sharedSegsOf, atomicPolyForRoom — все функции существуют в src/wall-thickness.ts под теми же именами, что и в §3/ADR;
    • src/coordinate-canonicalization.ts подтверждает, что write-barrier для lattice-координат (issue #291) уже реализован (canonicalizeConfigGeometry, LATTICE_NOISE_STEPS = 1e-4) — это отдельный от предлагаемого §9-барьера механизм (см. находку M1);
    • scripts/config-field-registry.mjs, scripts/model-invariants.mjs, scripts/mutation-gate.mjs, scripts/smoke-select.mjs существуют — гейты и инструменты, на которые ссылается ТЗ (§10.4, §17), реальны;
    • i18n-префиксы toast.* и gs.*, использованные в §12, — существующая конвенция (src/i18n/en.json).
  7. Сверены обязательные разделы ТЗ (PROCESS.md §7.1) против фактической структуры документа и против двух недавних крупных принятых ТЗ той же подсистемы (#291, #302) для калибровки, какой уровень детализации разделов принят как норма в проекте.

Находки

M1 (Medium, в скоупе). Не специфицирован порядок атомизации Stage 1 относительно уже существующего lattice-барьера #291 — именно там, где живёт заявленная причина задачи

Где: §7.1 «Breakpoints» и §7.2 «Толщина» (docs/specs/282-stable-wall-segment-identity.md:196–222), см. также §9 «Writers и единый identity barrier» (:314–344).

В чём проблема. §7.1 требует разбивать boundary «в точках… exact walls[].a/b», §7.2 — «exact matching walls[].a/b». Проблема, которую решает вся задача (см. ADR и подтверждающий комментарий владельца в issue: 65–79% координат его живой инсталляции лежат «почти на узле, но не на нём», худшее отклонение — 8·10⁻⁸ шага), — это именно то, что «exact» совпадение координат в хранимых данных ненадёжно до канонизации. В проекте уже есть отдельный, независимо реализованный барьер именно для этого (issue #291, src/coordinate-canonicalization.ts, canonicalizeConfigGeometry, порог LATTICE_NOISE_STEPS = 1e-4), а в docs/WALL-THICKNESS.md для сопоставления wallKey используется другой порог, max(pitch·10⁻⁶, 10⁻⁹) — то есть в кодовой базе уже сосуществуют два разных допуска для «эта же точка» в разных местах.

ТЗ Stage 1 нигде не говорит:

  • должна ли атомизация/breakpoint-matching запускаться после прогона существующей lattice-канонизации (#291) в той же транзакции миграции, или на сырых persisted-координатах;
  • если «exact» — это буквально бит в бит, атомизация того самого плана владельца, который стал поводом для задачи, будет либо создавать лишние микро-сегменты на каждом зашумлённом узле, либо (что хуже) детерминированно проваливать миграцию по «conflicting thickness»/«zero-length» блокировкам из §7.1, оставляя план в вечном «честном отказе» (§2);
  • какой из двух существующих допусков (1e-4 или max(pitch·10⁻⁶,10⁻⁹)) применяется к breakpoint-сопоставлению, либо что нужен третий.

Это не гипотетический краевой случай: это ровно тот датасет (собственная инсталляция владельца), который стал причиной задачи и явно процитирован в issue.

Почему это Medium, а не High. AC1–AC17 остаются осмысленными и не противоречат друг другу — вопрос решается одним предложением («атомизация переиспользует существующий lattice-канонизационный допуск #291 и запускается после него в той же commit-транзакции» или явным альтернативным решением) без изменения границ скоупа/не-скоупа и без продуктового вопроса владельцу: это техническое решение, которое ТЗ вправе принять само (PROCESS.md §7.1: «всё, чего пользователь не наблюдает, агенты решают сами»). Но без этого предложения AC1 («дважды дают byte-equivalent v8 candidate», «lossless») не имеет однозначного критерия выполнения на данных с реальным шумом, а разработчик и код-ревьюер не смогут договориться, что именно «exact» означает при коде-ревью.

Что делать: добавить в §7.1/§9 (или в §19 как явное принятое предположение) одно явное утверждение о порядке/допуске: атомизация запускается на уже канонизированных (через #291-барьер) координатах, использует тот же порог 1e-4/шаг, либо явно назвать другой порог и почему.

M2 (Medium, в скоупе). Заявленный UX отказа («предлагает Optimize либо исправление») не совпадает с фактическим текстом тоста в §12

Где: §2 «Что человек увидит до и после» (:30–34) против §12 «i18n» (:395–405) и §11 (:390–391).

В чём проблема. §2 обещает: «диалог/тост называет причину и предлагает Optimize либо исправление конфликтной геометрии». §11 повторяет контракт: «failure UI… только локализованный класс blocker и действие пользователя». Но единственный определённый в §12 ключ для этого случая —

toast.wall_model_migration_blocked → «Не удалось обновить модель стен: {reason}. План не изменён.» / «The wall model could not be updated: {reason}. The plan was not changed.»

— не содержит ни предложения запустить Optimize, ни какого-либо другого «действия пользователя». Для сравнения, второй ключ (toast.wall_model_client_outdated) действие называет прямо: «Обновите карточку и перезагрузите страницу…». Несоответствие между §2/§11 (обещан совет) и §12 (текста совета нет) — это ровно тот класс дефекта, который правило «одно число — один источник» (PROCESS.md §8) просит ловить для любой величины, видимой пользователю дважды: здесь дважды описан один и тот же UI-элемент двумя разными источниками текста, и они расходятся. Разработчик не может однозначно закрыть AC8 (атомарный отказ, «reason называет причину») по этому ключу — неясно, входит ли в её текст указание действия, или §2/§11 нужно привести в соответствие с §12.

Почему Medium, не High: решается либо правкой §2/§11 (убрать обещание конкретного совета — «называет причину» без «предлагает Optimize»), либо добавлением текста действия в i18n-таблицу; выбор — вкус автора ТЗ, не требует владельца.

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

  • Соответствие SCOPE.md/J6. Задача явно закрывает J6 «Keep the plan true as the home evolves» и не расширяется на новую функциональность — §5 «Не-скоуп» корректно исключает Stage 2–4, новый UI, cm:0-функциональность #306 (который приостановлен и явно не открывается этим ТЗ).
  • Продуктовые разделы (§7.1 первых два пункта). §1 «Сценарий» называет персону из SCOPE.md (администратор дома) и момент; §2 формулирует «до/после» в одном абзаце без терминов реализации — обе секции присутствуют и различимы, как того требует PROCESS.md §7.1.
  • Отсутствие незаявленных догадок. Ключевые фактические утверждения ТЗ (текущая модель v7, MAX_ROOMS/MAX_POLY_POINTS, существующая структура opening host, существование wallKey/rekeyWallsAfterMove/exactCoveringWall и т. д.) проверены построчно против кода — расхождений не найдено. Технические решения (алгоритм детерминированного ID §7.3, tie-break правила split/merge §8.2–8.3, single-writer barrier module) корректно помечены как «assumed, change freely» в §19 и не эскалированы владельцу, хотя они не банальны — соответствует PROCESS.md («смешанный вопрос делится», «владельцу — только продуктовые»).
  • AC1–AC17 в целом однозначны и проверяемы. Каждый AC называет конкретный наблюдаемый инвариант (unique ID/owner count 1-2, идемпотентность миграции, barrier bypass mutant, read-only не пишет model_version, fail-closed старого клиента, лимиты и performance-бюджеты с конкретными числами) и способ доказательства (unit/backend/smoke/golden/source-guard), что удовлетворяет требованию §2.5 «у каждого AC указано, чем он доказывается» — форма («Доказательство: …» под каждым AC, без отдельного заголовка «План автотестов») совпадает с недавно принятой практикой того же поднабора ТЗ (#291, #302), так что это не дефект структуры.
  • Не-скоуп корректно ограничивает риск. Явно исключены integer lattice (Stage 2), отказ от rooms[].poly (Stage 3), closed-form junction (Stage 4), удаление legacy reader — держит риск задачи (10/10 по аналитике) в границах, которые действительно можно проверить одним циклом ревью.
  • Откат и release-артефакты (§17–18) реалистичны: до первой v8-записи откат — чистый revert кода; после — только штатный backup/Undo, без автоматического downgrade v8→v7 (обоснованно запрещён, т.к. теряет hosted identity).
  • Ссылки на существующий тулинг и гейты (§10.4, §17) верны: все названные скрипты (config-field-registry.mjs, model-invariants.mjs, mutation-gate.mjs, smoke-select.mjs) существуют в scripts/.

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

  • Не читал построчно docs/ARCHITECTURE.md, docs/CANVAS.md, docs/UX-MODES.md, docs/TOUCH-SUPPORT.md целиком — ограничился тем, что уже известно из docs/SCOPE.md/AGENTS.md и прямыми grep-проверками (renderer/junction-код в ТЗ явно не переписывается, что подтверждается прочитанным WALL-THICKNESS.md). Для Stage 1, который сам заявляет «renderer не меняется», это достаточно; при код-ревью реализации у ревьюера кода будет диапазон коммитов, где эти документы стоит перечитать целиком, если изменения затронут рендер сильнее, чем заявлено.
  • Не проверял историю issues #244/#248/#249/#252/#253/#258/#261/#271–#280/#288–#302, упомянутых как регрессионный пол AC12/AC1, на предмет того, что у каждого из них действительно есть тест в текущем npm test — на этапе ТЗ формулировка AC («все тесты … остаются зелёными») достаточна; фактическую зелёность гоняет код-ревью.
  • Не запускал npm run typecheck/npm test/npm run build — на этапе spec-review для #282 продуктового кода ещё нет (только текстовый ТЗ-файл), гонять гейты нечего.
  • Не оценивал реалистичность SHA-хэш-коллизионной схемы §7.3 математически (вероятность коллизии base32(sha256)[0:20]) — оценил как явно избыточно низкую для практических объёмов (MAX_ROOMS×MAX_POLY_POINTS = 200 000) и не требующую отдельного доказательства на этапе ТЗ.

Вердикт

Документ методологически сильный: почти каждое утверждение о существующей модели подтверждено кодом, AC однозначны и проверяемы, скоуп/не-скоуп жёстко держат границу Stage 1. Две находки M1 и M2 — обе в скоупе задачи, обе решаются правкой текста ТЗ без изменения архитектуры или обращения к владельцу. Без High-находок это жёлтый вердикт.

Вердикт: жёлтый · заход r1 · блокирующих циклов 1/4 · High: 0 · Medium: 2 → в задаче · Документ: docs/reviews/SPEC-REVIEW-282-r1.md