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

87 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-282-r1
- **Issue:** [#282 — Геометрия стен: сменить представление, а не чинить последствия](https://github.com/Matysh/houseplan-card/issues/282)
- **Этап:** spec (PROCESS.md §2.4)
- **Заход:** r1 (первый; дельта-режим §2.10 не применяется)
- **Артефакт ТЗ:** [`docs/specs/282-stable-wall-segment-identity.md`](../../specs/282-stable-wall-segment-identity.md)
- **SHA ТЗ на момент ревью:** `8856fbda` (`docs: specify stable wall segment identity`, единственный коммит в ветке поверх `dev`)
- **Нормативный документ:** [`docs/adr/282-wall-geometry-representation.md`](../../../docs/adr/282-wall-geometry-representation.md), статус: Stage 0 принят и реализован (#283), Stage 1 — предмет этого ТЗ
- **Поставляемый этап:** ADR Stage 1 — stored identity сегментов contour walls
## Скоуп ревью
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`