18 KiB
SPEC-REVIEW-298-r1
- Issue: https://github.com/Matysh/houseplan-card/issues/298
- ТЗ:
docs/specs/298-resize-wall-thickness-carrier.md@ commit45066631aee4997ac1f033dbe75918b5b2d439d4 - Этап: ТЗ на ревью (PROCESS.md §2.4)
- Заход: r1 · блокирующих циклов израсходовано 0 из 4 (лимит лёгкого трека не действует — issue без метки
small) - Ревьюер: Claude (сессия ревью ТЗ), автор ≠ ревьюер
Скоуп ревью
Полный разбор — это первый заход, дельты нет. Читаю ТЗ состязательно: ищу, где оно невыполнимо, непроверяемо, либо выдаёт догадку за решение продукта.
Как проверялось
docs/SCOPE.md,AGENTS.md,PROCESS.md— целиком, перед тем как судить.- Тело issue #298 и три комментария (аналитика, занятие, передача ТЗ) — через
gh issue view 298 --json ...(MCP-инструменты GitHub были недоступны без подтверждения, поэтому использованghCLI). - Полный текст
docs/specs/298-resize-wall-thickness-carrier.md. - Канонические документы затронутой подсистемы:
docs/RESIZE.md,docs/WALL-THICKNESS.md, разделыdocs/TOUCH-SUPPORT.mdпро safety floor редакторов. - Существование артефактов, на которые ссылается ТЗ, проверено в дереве
репозитория, а не на слово автора:
test/fixtures/real-plan-second-floor.json,test/fixtures/real-plan-first-floor.json— существуют;LATTICE_NOISE_STEPS— существует,src/coordinate-canonicalization.ts:11, значение1e-4, используется вscripts/model-invariants.mjsи мутационном гейте;rekeyWallsAfterMove()— существует,src/wall-thickness.ts:533; прочитан код: помеченная в issue пропорциональная проекция (mapPoint, строки 574–582 —tсчитается от старого ребра и переносится на новое без учёта реального соответствия вершин) реально в проде — корневая причина подтверждена чтением, а не поверена на слово;off_lattice_coordinate/wall_carrier— существуют как виды нарушений вscripts/model-invariants.mjsи таблицеKNOWNdemo/smoke_edit_walk.mjs; текущая таблицаKNOWN(строки 73–87) уже регистрирует ровно те находки, которые AC6 требует убрать (off_lattice_coordinate,wall_carrierдля обеих фикстур), и отдельноmixed_role_record— долг #299, который ТЗ прямо исключает из скоупа и просит не трогать вKNOWN. Согласуется.
- Проверено существование и состояние всех связанных issue, на которые ссылается ТЗ: #253, #277, #289, #291, #293, #297 — закрыты; #299 — открыт, как и утверждает ТЗ (не дубликат, отдельный долг). Ни одна ссылка не битая и не искажена.
- Сверены формулировки с канонoм: раздел 3.3 (legacy fallback, запрет
proportional-midpoint) согласуется с
WALL-THICKNESS.md(«Legacy entries without endpoints keep the unambiguous whole-key/midpoint fallback and are never split by inventing a length»); раздел 8 (touch safety floor) согласуется с разделомTOUCH-SUPPORT.md«Safety floor that still applies to touch editors»; §5 (disabled handle до pointer capture) согласуется с существующим текстомRESIZE.mdпро eligibility и disabled-handle. Ни одного места, где автор выдаёт непроверяемую догадку за факт, не найдено — все нетривиальные технические решения (§12 «Принятые предположения») либо выводятся из уже принятого контракта #277, либо явно помечены как предположение.
Не проверялось (стадия ТЗ этого не требует): выполнение гейтов
typecheck/test/build — кода к задаче ещё нет; сам код ещё не написан.
Находки
Medium (в скоупе задачи) — отсутствуют обязательные продуктовые разделы §7.1
PROCESS.md §7.1 требует в ТЗ два продуктовых раздела первыми: «какая персона
встречает изменение, на какой поверхности, в какой момент» и «что человек
увидит до и после — одной фразой, без терминов реализации». AGENTS.md
повторяет это как обязательное дополнение к §7.1. Ни то, ни другое в тексте
ТЗ не выделено.
- §1 («Сценарий и подтверждённая причина») называет только техническую цепочку
(
rekeyWallsAfterMove(), endpoint, topology boundary) — персона, поверхность и момент («администратор дома, десктоп, Plan editor / Resize, после нескольких правок за дни») были прямо названы в комментарии аналитики к issue, но не перенесены в сам документ ТЗ. - §2 («Пользовательский результат») — единственный кандидат на «одну фразу без терминов реализации», но весь абзац написан в терминах реализации: «wall endpoints», «topology vertex», «lossless correspondence», «config write», «Undo entry». Читатель без контекста кода не поймёт по нему, что видит пользователь.
Дефект не про содержание решения — оно верное и подтверждено кодом — а про форму ТЗ, которая обязана доказывать, что это изменение продукта, а не работа над кодом (PROCESS.md §7.1: «ТЗ, которое не может ответить на эти два вопроса, описывает работу, а не изменение продукта»). Правка дешёвая: одна-две фразы в начале документа, без пересмотра контракта.
Предлагаемая правка (пример, автор волен сформулировать иначе):
Персона — администратор дома (
docs/SCOPE.md), поверхность — desktop Plan editor, инструмент Resize; момент — любой ресайз стены, эффект копится незаметно и проявляется через дни на другой стене. Видимо: сегодня после серии обычных ресайзов случайная стена в другом месте плана вдруг рисуется другой толщиной или теряет рабочую ручку ресайза, хотя её никто не трогал. После исправления обычный ресайз либо проходит как раньше, либо (в редком неоднозначном случае) заканчивается тем же самым сообщением об ошибке без изменения плана — но никогда не портит стену, которую пользователь не двигал.
Это Medium: без High-находок вердикт жёлтый, правка делается автором в этом же issue, повторного полного разбора не требует — фактическая архитектура решения ревью не оспаривает.
Low (снято ревьюером с записью) — способ доказательства не назван явно у двух AC
DoR-чеклист (PROCESS.md §2.5) требует у каждого AC явного указания, чем он
доказывается (unit/backend/smoke/golden/«ревью кода»). У AC1, AC2, AC4,
AC6, AC8, AC9 это явно есть (или очевидно из перечисленных команд). У AC5
(carrier preflight) и AC7 (preview/commit/Undo атомарны) отсутствует строка
«Доказательство: …» — способ проверки не назван словом, хотя из контекста
понятен: AC5 — чистая функция преflight из §4, доказывается unit-тестом; AC7 —
поведение pointer-жеста, доказывается production-bundle smoke по аналогии с уже
существующим demo/smoke_room_resize.mjs (см. RESIZE.md «Verification»).
Снимаю без возврата на цикл: неоднозначности в том, что проверяется, нет — их формулировки уже настолько конкретны (перечислены positive/negative кейсы, атомарность записи/Undo), что тип теста восстанавливается однозначно. Автору стоит добавить явную строку при реализации ради единообразия с остальными AC, но это не блокирует переход в «Готово к разработке».
Low (снято ревьюером с записью) — разделы «UX» и «i18n» не выделены явно
PROCESS.md §7.1 перечисляет UX и i18n как обязательные разделы. В документе
нет разделов с этими заголовками; содержание по факту размазано — UX-семантика
(disabled handle, единственное существующее сообщение об ошибке, отсутствие
нового UX) описана в §5 и подтверждена явным предположением №3 в §12; i18n
покрыт тем же предположением №3 («новый persisted reason или новый UX в этой
задаче не вводится» ⇒ новых ключей нет).
Снимаю: по существу оба вопроса закрыты (нет нового текста, нет новых ключей), и это прямо написано, просто не под ожидаемым заголовком. Chisto формальный момент, не влияющий на проверяемость AC.
Что проверено и корректно
- Корневая причина в §1 подтверждена чтением
rekeyWallsAfterMove()— реальный код делает ровно ту пропорциональную проекцию (mapPoint), о которой говорит issue и ТЗ; это не пересказ чужих слов. - Оба воспроизведения (AC1/AC2) содержат точные числа из issue
(
-85 → -82.457,59.538, вершины17/52/57/101) и совпадают с текстом issue буквально — не пересказаны с искажением. - Скоуп «не входит» (§6) корректно исключает #289/#299 (смешанные роли), ADR #282 (integer storage), eligibility/#277/#293 (topology) — все со ссылками на реально существующие issue в правильном статусе.
- AC6 корректно ссылается на реальную структуру
KNOWNвdemo/smoke_edit_walk.mjsи требует убрать только те два вида находок, которые там сейчас действительно зарегистрированы для этой пары фикстур, не трогая независимый долг #299 (mixed_role_record) — проверено построчно. - Ни одна ссылка на связанные issue (#253, #277, #289, #291, #293, #297, #299) не оказалась битой, дублирующей или искажающей состояние (закрыт/открыт).
- Технические решения раздела 3 (correspondence-таблица, конфликт нескольких
destinations, атомарное разбиение длинной записи) и раздела 4 (carrier
preflight по всему span, не только по концам/midpoint) не изобретают новое
поведение — они последовательно продолжают уже принятый контракт #277 и модель
данных
WALL-THICKNESS.md(«exact endpoints… independent of whichever room topology later happens to split the same straight line»), а не выдают догадку за факт. - Раздел 3.3 (legacy fallback, fail-closed при неоднозначном whole-edge
соответствии) прямо согласован с уже задокументированным в
WALL-THICKNESS.mdзапретом «изобретать длину» для legacy-записей безa/b. - Touch/safety floor в §8 корректно ссылается на существующий раздел
TOUCH-SUPPORT.md«Safety floor that still applies to touch editors», а не придумывает новое правило. - Откат (§10) реалистичен: чистый revert коммита, миграции нет, потому что схема не меняется — согласуется с §8.
- Продуктовых вопросов владельцу в этом ТЗ нет, и по факту разбора это
оправданно: спорные места (§5 п.7 про предсказуемый конфликт до pointer
capture) читаются как следствие уже принятого принципа fail-closed
(
RESIZE.md: «If neither step is safe, the handle remains… disabled»), а не как новое расширение eligibility — само ТЗ явно исключает изменение eligibility из скоупа (§6 «Не входит»), и текст §5 п.7 с этим не расходится при точном чтении («если конфликт можно доказать в eligibility» — то есть уже существующими проверками, а не новыми). - Ни один найденный ранее класс дефектов (#253, #258/#259, #287/#289) не игнорируется задним числом — ТЗ явно проговаривает отличие от каждого в разделе «Почему это дорого» issue и в §6/§12 ТЗ.
Чего не проверял
- Само исправление (кода ещё нет — задача в
S4-spec-review, реализация не началась). AC1–AC9 разбирались на выполнимость и проверяемость формулировки, а не на то, будущий код им действительно удовлетворит — это работа код-ревью. - Полный текст
docs/CANVAS.md— раздел на который ссылается ТЗ, но задача не меняет canvas-рендер напрямую (только данные, которые он потребляет); беглая проверка показала, что ссылка не противоречит модели данныхwalls[*]. - Все шесть комбинаций
smoke_edit_walkне прогонялись — на этой стадии нет кода для прогона; сверка ограничилась статическим соответствием таблицыKNOWNтому, что требует AC6. - Производительность (AC9) — числовые бюджеты
RESIZE.mdне пересчитывались, только сверено само требование «сохраняется p95 budget» с текстом канона.
Вывод
Единственная содержательная находка — Medium, отсутствие обязательных продуктовых разделов §7.1 (персона/поверхность/момент + «одна фраза без терминов реализации»). Остальное — два Low, снятых с запиской. High-находок нет: контракт технически выполним, каждый AC проверяем, ни одна ссылка не искажена, ни одна догадка не выдана за решённый факт. Вердикт жёлтый — формально из-за Medium, а не из-за сомнений в самом решении.