Files
houseplan-card/docs/reviews/SPEC-REVIEW-113-r1.md
T
2026-08-19 02:47:50 +03:00

329 lines
29 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-113-r1
- **Issue:** https://github.com/Matysh/houseplan-card/issues/113
- **ТЗ под ревью:** `docs/specs/113-optional-space-model.md` (коммит
`5f02dd38a1b60fa5064ea1291559b32c502fa1f0`, ветка
`issue/113-optional-space-model`)
- **Роль:** ревьюер ТЗ (не автор), этап `S4-spec-review`
- **Трек:** обычный (не `small`/`trivial`) — аналитика оценила сложность и
риск 6/10 и 7/10, что превышает порог лёгкого трека (§5 PROCESS.md, ≤3);
ТЗ корректно лежит файлом в `docs/specs/`, зарегистрировано в
`docs/specs/README.md:85`.
- **Цикл:** r1/4
## Скоуп ревью
Проверялось соответствие ТЗ:
- `docs/SCOPE.md` — попадание задачи в J6 («план остаётся правдивым по мере
развития», системная защита от ложного/пустого плана), отсутствие
расширения скоупа за пределы честного optional-контракта;
- `PROCESS.md` §2.4, §2.5 (DoR), §7.1 (обязательные разделы ТЗ), §3/§12
(запреты, включая «догадка вместо решения»);
- `AGENTS.md` — классы файлов (класс C для этого коммита), ветка, трейлеры;
- фактическому состоянию `src/houseplan-card.ts` — на предмет того, что
диагноз ТЗ и цифры issue (45 полевых обращений `_spaceModel().<поле>`,
недостижимость до render-гейта, факт того, что #111 закрыл один вызов) не
являются непроверенной догадкой;
- прецедентам house-style в уже принятых `docs/reviews/SPEC-REVIEW-*.md`
(107, 122, 123, 131, 137, 138, 141, 146) — как единообразно оформлены
обязательные разделы §7.1 и как в этом репозитории калибруется
High/Medium/Low для находок такого типа (структурная полнота ТЗ против
дефекта контракта).
## Как проверялось
1. Прочитан весь тред issue #113: тело (находка M3 из ревью #111), комментарий
аналитики от 2026-08-14 (ценность 7/10 пользователю, 9/10 разработке,
сложность/риск 6/10, 7/10, P2, tech-debt, обычный трек, «продуктовых
вопросов нет — технические guard patterns решаются в ТЗ без вопроса
владельцу») и комментарий автора ТЗ от 2026-08-15 («продуктовых вопросов
нет; выбран честный optional API»). Продуктовых вопросов владельцу
корректно не задавалось — вся задача технического характера, что
соответствует правилу «владельцу задаются только продуктовые вопросы».
2. Построчно сверены обязательные разделы ТЗ (§7.1 PROCESS.md) — таблица
ниже.
3. Прочитан код, на который опирается диагноз ТЗ, чтобы отличить проверенный
факт от догадки:
- `_spaceModel(id?): SpaceModel` (`src/houseplan-card.ts:2984`) —
сигнатура и тело (`m.find(...) || m[0]`) подтверждают проблему буквально
как в §1 ТЗ.
- Подсчитано число полевых обращений `_spaceModel([^)]*)\.<поле>`:
`rooms` — 35, `wall_columns` — 6, `room_drafts` — 2, `bg` — 2, плюс один
дополнительный сайт `_spaceModel(spaceId).vb` (`:15897`), не учтённый в
таблице тела issue (45 vs фактических 46). Это неточность в теле самого
issue, не в ТЗ (ТЗ не повторяет число 45), и не влияет на контракт ТЗ —
не выношу отдельной находкой.
- `render()` (`:14281`) подтверждён как ранний empty-state gate
(`if (!model.length) return html\`...\`;`), `willUpdate`/`updated`
(`:3089`, `:3116`) подтверждены как выполняющиеся **до** и **независимо**
от результата `render()` (Lit вызывает их для каждого прохода жизненного
цикла), что подтверждает центральный тезис ТЗ и issue — недостижимость
держится на порядке вызовов, а не на типе.
- `_continuityAssetsReady()` (`:2763-2769`) подтверждён как фикс #111:
явный `if (!this._model.length) return false;` **до** вызова
`_spaceModel()` — соответствует описанию issue «#111 закрыл один вызов
из render snapshot».
- Default-parameter паттерн `space = this._spaceModel()`
(`_physicalBodiesR`, `_rawPhysicalBodiesR`, ещё один метод — `:11668`,
`:11679`, `:11688`) подтверждён как реально существующий — именно тот
паттерн, который §5.3 ТЗ явно требует убрать.
- Проверено отсутствие текущих non-null assertions на `_spaceModel()`
(`grep "_spaceModel([^)]*)!"` — пусто) — согласуется с ТЗ §8, который
запрещает их вводить, а не убирает существующие.
- Найдены реальные call sites с явным `id`, показательные для §6 ТЗ:
`_livePos(d)` (`:4535` → `this._spaceModel(d.space)`), `_vacStartFit`
(`:15376` → `this._spaceModel(d.space)`), `_vacPlanRoomAnchors`
(`:15302`), сайт создания/перемещения устройства (`:12043` →
`this._spaceModel(space || undefined)`) — все передают «стабильный»,
потенциально устаревающий `spaceId`, а не всегда `this._space`. Это
именно тот класс вызовов, который §6 ТЗ описывает как «команды с
stable spaceId».
- Существующие тесты вида `test/*-contract.test.mjs`
(`isometric-contract.test.mjs`, `release-contract.test.mjs`,
`performance-contract.test.mjs`, `native-select-contract.test.mjs`)
подтверждают, что «source-contract test» из §8/§10 ТЗ — не изобретённый
механизм, а продолжение существующего паттерна репозитория.
- `docs/ARCHITECTURE.md` и `docs/TESTING.md` существуют — release-артефакты
§13 ТЗ ссылаются на реальные файлы, не выдуманные.
4. Сопоставлены между собой §4 (нормативный API), §6 (active id и fallback),
§10 AC8 и §14 (риски) на непротиворечивость — обнаружено расхождение,
см. Medium-1.
5. Проверены трейлеры и class-принадлежность: `git show --stat 5f02dd3`
показывает только `docs/specs/113-optional-space-model.md` и
`docs/specs/README.md` (класс C, ни одного файла класса A — продуктовый
код не тронут до `S5-ready`, Rule #1 `AGENTS.md` соблюдено); коммит несёт
`Issue: #113` и `User-Visible: no` — корректно для документа ТЗ, который
сам не меняет поведение.
## Обязательные разделы (§7.1 PROCESS.md)
| Раздел | Есть | Комментарий |
|---|---|---|
| Сценарий (персона/поверхность/момент) | ⚠️ | Нет отдельного заголовка; содержание фактически распределено по §1 (кто и когда встречает пустой план) — см. Low-1 |
| Что человек увидит до/после | ⚠️ | Нет отдельного заголовка; ответ («ничего видимо не меняется, это профилактика класса #111») восстановим из §1+§9, но не сформулирован явно одной фразой — см. Low-1 |
| Проблема (с подтверждённой причиной) | ✅ | §1, факты подтверждены чтением кода (см. «Как проверялось» п.3) |
| Скоуп / не-скоуп | ✅ | §2 (цели) / §3 (не входит в задачу), границы чёткие (без глобального `noUncheckedIndexedAccess`, без миграции, без empty-state UX) |
| Контракт поведения | ✅ | §4–§8, классификация call sites по 4 категориям, конкретные примеры кода |
| Модель данных и миграция | ✅ | §9 — явно «config/layout schema и revisions не меняются» |
| UX, i18n, accessibility, touch | ⚠️ | §9 содержательно утверждает «editor touch safety floor не меняется», но не использует ни одну из трёх канонических формулировок `docs/TOUCH-SUPPORT.md` («Touch editor: supported/best effort/not exposed») — см. Low-3 |
| AC1…ACn с доказательством | ⚠️ | §10, 10 штук, пронумерованы и в целом проверяемы, но ни один не несёт явной пометки способа доказательства (`unit`/`smoke`/…), в отличие от всех сверенных прецедентов (107, 122, 123, 131, 137, 141, 146) — см. Low-2. Отдельно для AC8 отсутствие явного доказательства — не косметика, а реальный пробел, см. Medium-1 |
| План автотестов | ✅ | §11, разбит на unit/smoke/регрессию, включает mutation-тест («вернуть required signature — тест красный») |
| Риски | ✅ | §14, таблица риск/мера, 5 строк |
| Откат | ✅ | §14 (последний абзац) — без миграции данных |
| Release-артефакты | ✅ | §13, конкретный список (`ARCHITECTURE.md`, `TESTING.md`), оба файла существуют, `User-Visible: no` корректно снимает требование changelog |
Присутствует также раздел «Принятые технические предположения» (§15, 4
пункта) — соответствует требуемому PROCESS.md §7.1 блоку «принято
предположительно, поменять свободно», хотя заголовок не содержит буквально
эту оговорку (косметика, не отдельная находка).
## Находки
### Medium-1 — AC8 не имеет предъявленного механизма доказательства; риск и контракт расходятся
**Файл:** `docs/specs/113-optional-space-model.md:110-130` (§6),
`:180` (AC8), `:186-201` (§11, unit-план), `:234-241` (§14, таблица рисков)
§14 называет риск буквально: «Stale id редактирует первый space» и указывает
единственную меру — «exact lookup для commands». Это ровно тот самый риск,
который AC8 обязан закрывать: «Missing explicit stale space id не мутирует
первый space».
Но нормативный API §4 —
```ts
private _spaceModel(id?: string): SpaceModel | undefined {
const requested = id ?? this._space;
return this._model.find((space) => space.id === requested) ?? this._model[0];
}
```
— **всегда** возвращает `_model[0]`, когда `requested` не найден, независимо
от того, был ли `id` передан явно или нет. Для непустой модели эта функция
никогда не возвращает `undefined`: она возвращает объект, только не тот,
который просили. Проверено на реальных call sites: `_livePos(d)`
(`houseplan-card.ts:4535`) вызывает `this._spaceModel(d.space)` для позиции
устройства, `_vacStartFit` (`:15376`) — аналогично. Если пространство,
которому принадлежит устройство (`d.space`), было удалено (что штатно для
задачи #113 — «удаление последнего/произвольного space»), а другие
пространства остались, `_spaceModel(d.space)` молча вернёт **первое
оставшееся** пространство — тип этого не ловит, потому что результат
определён (не `undefined`).
Единственная названная в ТЗ мера — вынести отдельный
`_spaceModelById(id)` без fallback (§6) — сформулирована как **опция**:
«Чтобы исключить опасную двусмысленность, **допустимо** разделить API...
Это техническое разделение **рекомендуется** для drag/history/dialog
commands». Формулировка не обязывает автора реализации ввести этот метод
и не обязывает каждый call site с явным id использовать его вместо
`_spaceModel(id)`. Единственная альтернатива, которую ТЗ предлагает
взамен, — ручная проверка identity на каждом call site («callers... должны
проверять identity отдельно и abort-ить»), но:
- ни §8 (source-contract test), ни §11 (план тестов) не называют проверку,
которая убедилась бы, что *каждый* call site с явным id либо использует
no-fallback lookup, либо содержит ручной identity-guard;
- unit-пункт §11 «exact lookup не падает в first-space fallback» тестирует
сам вспомогательный selector в изоляции, а не то, что производственные
call sites (`_livePos`, `_vacStartFit`, сайт создания устройства `:12043`
и другие, использующие явный `id`/`spaceId`) действительно его
применяют.
**Почему это Medium, а не High:** контракт не производит немедленную,
гарантированную регрессию (в отличие от `SPEC-REVIEW-138-r1` High-1, где
буквальное прочтение ТЗ ломает работающий сегодня клик) — сегодняшний код
уже имеет такое же молчаливое поведение при stale `id`, задача #113 его не
ухудшает. Риск в том, что заявленная в AC8 защита от этого класса дефектов
не гарантирована структурно (типом или тестом), а оставлена на
дисциплину конкретных call sites без перечня, что именно нужно проверить —
то есть тот же класс проблемы, из-за которого появился сам #113
(«недостижимость держится на порядке вызовов, а не на типе»), может
частично воспроизвестись для explicit-id веток, если реализация не
проявит собственную дисциплину сверх того, что явно требует контракт.
**Что нужно поправить:** заменить «допустимо»/«рекомендуется» в §6 на
обязательное правило — любой call site, передающий явный/стабильный
`id`/`spaceId` в мутирующем или persist-контексте, обязан использовать
no-fallback lookup (или эквивалентный identity-guard), и добавить в §8/§11
конкретную проверку (source-contract grep по списку известных
call sites, либо unit-тест, дергающий каждую публичную мутирующую точку со
stale id и проверяющий отсутствие записи/side effect). Это техническое
уточнение, не продуктовый вопрос — решается автором и ревьюером кода без
эскалации владельцу.
**Решение ревьюера:** Medium, заведён отдельный issue
[#184](https://github.com/Matysh/houseplan-card/issues/184) со ссылкой на
#113 и на этот документ. Не блокирует переход ТЗ в `S5-ready`: остальной
контракт (9 из 10 AC, вся lifecycle/render/cleanup часть) самодостаточен,
проверяем и корректен, а сама эта находка — уточнение контракта, которое
разумно донести до автора кода явно, а не через возврат ТЗ на цикл.
### Low-1 — нет отдельных заголовков «Сценарий» и «Что человек увидит до/после»
**Файл:** `docs/specs/113-optional-space-model.md:9-27` (§1)
PROCESS.md §7.1 требует эти два раздела первыми, отдельно от «Проблема».
Все восемь других сверенных ТЗ репозитория (`107`, `122`, `123`, `131`,
`137`, `138`, `141`, `146`) оформляют их отдельными заголовками. В
`113-optional-space-model.md` содержание фактически присутствует —
§1 называет причину и предшествующий инцидент (#111), §9 фиксирует, что
видимое поведение не меняется, — но не сформулировано как явный ответ на
«кто/где/когда» и «одна фраза до/после». Прецедент `SPEC-REVIEW-107-r1`
(Low-1: «раздел «Проблема» не выделен отдельным заголовком») фиксирует тот
же класс находки как некритичный, если содержание по существу
присутствует.
**Решение ревьюера:** Low, не блокирует. Можно поправить в следующей
редакции (например, короткая явная фраза: «Персона — любой пользователь
View/editors при удалении пространства или холодном старте с `spaces: []`;
до — риск исключения при будущей правке порядка вызовов (класс #111),
после — тот же надёжный empty-state, без видимых изменений»), либо снять
записью в этом документе, если автор сочтёт §1 достаточным.
### Low-2 — AC1…AC10 не несут явной пометки способа доказательства
**Файл:** `docs/specs/113-optional-space-model.md:170-182` (§10)
DoR (`PROCESS.md` §2.5) и §7.1 требуют «у каждого [AC] указано, чем он
доказывается: unit / backend / smoke / golden / «ревью кода»». Ни один из
десяти пунктов §10 такой пометки не несёт — способ доказательства
приходится реконструировать по §11 (план автотестов), что для девяти из
десяти AC делается однозначно (например, AC3/AC4/AC5/AC6 — browser smoke,
AC1/AC2/AC10 — typecheck + source-contract test, AC7 — golden/существующий
скриншот, AC9 — unit), но для AC8 реконструкция не удаётся структурно (см.
Medium-1). Прецедент `SPEC-REVIEW-141-r1` (Low-3) относился к похожему, но
более мягкому случаю (формулировка доказательства вне буквального
перечня, а не полное отсутствие) и не блокировал.
**Решение ревьюера:** Low, не блокирует. При следующей правке ТЗ
рекомендуется приписать к каждому пункту §10 короткую пометку в скобках
(`(smoke)`, `(unit)`, `(typecheck+source-contract)` и т.п.) — механическая
правка без изменения контракта.
### Low-3 — нет буквальной метки touch-контракта по `docs/TOUCH-SUPPORT.md`
**Файл:** `docs/specs/113-optional-space-model.md:161-168` (§9)
`docs/TOUCH-SUPPORT.md` требует одну из трёх формулировок (`Touch editor:
supported` / `best effort / intentionally degraded` / `not exposed`) для
спецификаций, затрагивающих поведение редакторов. §9 по существу описывает
best-effort/unchanged-контракт («editor touch safety floor не меняется»),
но не использует канонический ярлык. Тот же класс находки зафиксирован как
Low и не блокировал в `SPEC-REVIEW-141-r1` (Low-1).
**Решение ревьюера:** Low, не блокирует. Косметическая правка на усмотрение
автора (добавить строку `Touch editor: best effort / intentionally
degraded (unchanged)`).
## Что проверено и корректно
- **Соответствие `docs/SCOPE.md`:** задача закрывает J6 («план остаётся
правдивым по мере развития» — плановые правки не должны ронять карточку
при пустом плане) как профилактика, не расширяя продуктовую поверхность;
User-Visible: no подтверждён и содержанием ТЗ (§13), и отсутствием любых
UX/i18n/визуальных изменений в контракте.
- **Легитимность полного трека:** сложность/риск 6/10 и 7/10 превышают
порог `small` (≤3) — полный трек и файл `docs/specs/` выбраны верно,
issue корректно НЕ помечен `small`.
- **Технический диагноз не голословен.** Число полевых обращений
`_spaceModel().<поле>`, факт единственного гейта в `render()`, порядок
`willUpdate`/`updated` относительно `render()`, факт фикса #111 через
явный ранний `return false` — всё подтверждено чтением
`src/houseplan-card.ts`, а не пересказом issue.
- **Классификация call sites (§5) полна и специфична для этой кодовой
базы:** default-parameter паттерн (`space = this._spaceModel()`) назван
и подтверждён существующим в трёх методах; onboarding-гейт `updated()`
(`this._model.length === 0`) как пример уже существующей ручной
дисциплины, которую ТЗ формализует типом.
- **Запреты (§4) конкретны и адресуют реальный анти-паттерн:** явный запрет
на synthetic empty `SpaceModel`, на `!` у каждого consumer, на скрытый
fallback через `this._serverCfg.spaces[0]` — не общие слова, а прямая
реакция на то, как решались подобные проблемы в других частях кодовой
базы.
- **Продуктовые вопросы закрыты по процессу:** задача целиком техническая
(tech-debt, «guard patterns»), аналитик и автор корректно не эскалировали
ничего владельцу; ни один вопрос, вынесенный бы во владельцу, на самом
деле не был продуктовым — сверено построчно, эскалаций нет вообще.
- **Трейлеры и класс файлов:** коммит `5f02dd3` — только `docs/specs/**`
(класс C), `Issue: #113`, `User-Visible: no` — корректно для документа
ТЗ; ссылка issue ↔ ТЗ двусторонняя (issue → комментарий со ссылкой на
файл; `docs/specs/README.md:85` → issue).
- **Release-артефакты не выдуманы:** `docs/ARCHITECTURE.md` и
`docs/TESTING.md`, которые §13 обязывает дополнить, существуют в
репозитории.
## Чего не проверял
- Не проверял реализуемость предложенного разделения `_spaceModel()` /
`_spaceModelById()` как факта работающего TypeScript-кода — на этапе ТЗ
реализации ещё нет, это предмет код-ревью.
- Не запускал автотесты, не собирал бандл и не проверял backend — на этапе
`spec` продуктовый код не менялся (подтверждено `git show --stat`), гейты
из §8 PROCESS.md здесь неприменимы.
- Не проверял полноту всех ~46 call sites `_spaceModel()` по отдельности —
проверена корректность классификации (§5 ТЗ) на представительной выборке
(lifecycle-хуки, default-parameter паттерн, explicit-id команды), а не
построчный аудит всех вхождений; это ожидаемо станет предметом код-ревью,
когда появится diff.
- Не проверял `docs/TESTING.md` на предмет того, легко ли туда встроить
«empty-plan lifecycle matrix» — детали документации оставлены на
усмотрение автора реализации (класс C, не влияет на продуктовый контракт).
- Не оценивал производительность предложенных изменений — ТЗ (§13) явно и
обоснованно откладывает performance-гейт до случая, когда diff
действительно заденет горячие render-пути; на этапе спецификации это не
проверяется.
## Вердикт
Зелёный. High: 0, Medium: 1 (заведён отдельным issue, не блокирует), Low: 3
(отсутствие отдельных заголовков «Сценарий»/«Что человек увидит»; отсутствие
явной пометки доказательства у AC1…AC10; отсутствие буквальной touch-метки
— все три косметические, содержание по существу присутствует, не блокируют
приёмку). ТЗ решает заявленную проблему (#111-класс дефектов) для
lifecycle/render/cleanup путей полно и проверяемо; единственный содержательный
пробел (Medium-1, explicit-id command paths) не отменяет ценность и
корректность контракта для основного объёма из ~46 call sites, но должен
быть закрыт до или во время реализации — что и обеспечивает заведённый
issue.