# SPEC-REVIEW-176-r1 - **Issue:** https://github.com/Matysh/houseplan-card/issues/176 - **ТЗ под ревью:** тело issue #176 (лёгкий трек, файл `docs/specs/` не создаётся — метка `small` подтверждена аналитиком и не оспорена владельцем) - **Роль:** ревьюер ТЗ (не автор), этап `S4-spec-review` - **Трек:** лёгкий (`small`), лимит циклов ревью ТЗ — 2 (§4 PROCESS.md) - **Цикл:** r1/2 ## Скоуп ревью ТЗ #176 удаляет мёртвый внутренний tool-state `'partition'` из `src/houseplan-card.ts` (значение `MarkupTool`, `MARKUP_TOOLS`, метод `_partitionClick`, ~15 условных веток `this._tool === 'partition'`) и три осиротевших i18n-ключа, оставленных после #173 (замена one-shot Partition единым Walls-инструментом). Persisted-модель независимых стен (`space.partitions`, `PartitionCfg`, `kind: 'partition'` у selection/opening host, весь UI работы с уже созданными стенами) не меняется. `User-Visible: no`; changelog не требуется. Находка изначально заведена самим ревьюером кода на #173 (`docs/reviews/CODE-REVIEW-173-r1.md`, Medium-1) и подтверждена владельцем как issue — проверка «issue создан явным решением владельца» выполнена (первая запись в issue — команда владельца на вход в `S2-analysis`). Не в скоупе ревью: продуктовый код не написан (issue в `S4-spec-review`; ветки `issue/176-*` в репозитории нет — проверено `git branch -a`), поэтому реализация, тесты и гейты — предмет будущего код-ревью (PROCESS.md §2.7). ## Как проверялось 1. Прочитаны `docs/SCOPE.md`, `AGENTS.md`, `PROCESS.md` целиком (актуальная редакция, включая §2.4/§2.5/§5/§7.1/§8) и тело issue #176 со всеми комментариями (аналитика владельца, передача на ревью, отметка о сбое автоматического прогона). 2. Построчно сверены технические утверждения ТЗ с текущим `dev` (`src/houseplan-card.ts`, `grep -in partition`, полный список ~140 вхождений): - `type MarkupTool = ... | 'partition' | ...` — строка 516, подтверждено; - `'partition'` в `MARKUP_TOOLS` — строка 547, подтверждено; - `private _partitionClick(...)` — строка 7143, реализует «два клика → одна перегородка» через `_recordGeometry('history.partition_add', …)`; подтверждено; - диспетчер клика: `if (this._tool === 'partition') { this._partitionClick(...); }` — строка 6834-6835, единственная точка вызова метода — подтверждено, что `_partitionClick` не достижим никаким иным путём; - ветки `this._tool === 'draw' || this._tool === 'partition'` (лимиты, snap, hints, Undo/Escape, рендер превью) — найдены на строках 2241, 5362, 5429, 5450, 5487, 6364, 6601, 17202, 17923, 18039, 18041, 18091, 9597; одиночные `this._tool === 'partition'` — строки 2270, 2323, 6834, 9559, 9598, 11908 — количество и характер веток соответствуют заявленным в issue «порядка 15». 3. Проверена причина недостижимости `this._tool === 'partition'` — ключевое техническое утверждение issue, а не декларация: - `normalizeMarkupTool` (`houseplan-card.ts:551-562`) — единственное место, присваивающее `this._tool` из внешнего/сохранённого значения (`this._tool = normalizeMarkupTool(vp.tool)` при восстановлении warm viewport, строка 2656); - `normalizeMarkupTool` безусловно вызывает `value = normalizeUnifiedWallTool(value)` **до** проверки `MARKUP_TOOLS.has(...)` (строка 555); - `normalizeUnifiedWallTool` (`src/wall-face-graph.ts:53-55`): `return value === 'partition' ? 'draw' : value;` — литерал `'partition'` маппится в `'draw'` безусловно. - Других обработчиков, присваивающих `this._tool = 'partition'` из пользовательского ввода, не найдено (единственное текстовое совпадение — сравнения `===`, не присваивания). Вывод issue «код недостижим, а не редко используем» подтверждён чтением, не декларацией автора. 4. Проверено, что тип `MarkupTool` и `MARKUP_TOOLS` не используются больше нигде в `src/` (`grep -rn "MarkupTool\|MARKUP_TOOLS" src/`) — оба контейнера частные для `houseplan-card.ts`; удаление литерала не пробивает границу модуля. 5. Проверена граница «мёртвый код инструмента» vs «живая persisted-модель» построчно: `kind: 'partition'` в типах selection/drag/dialog (строки 1225, 1228, 1234, 1243, 7196, 7257, 7283, 7374, 7401, 7415, 7434, 7462, 7505, 11485, 12005, 17840, 17863, 17881, 17981), `space.partitions`/`PartitionCfg`, `_partitionDeleteDialog`/`_confirmPartitionDelete`, `_partitionOpeningCuts`/`resolvePartitionOpeningCompat` и весь модуль `partition-openings.ts` — ни один из них не завязан на `this._tool`, все работают с уже существующими объектами независимо от активного инструмента. Граница, которую ТЗ объявляет неприкосновенной (контракт п.3), реально проходит там, где заявлено. 6. Проверены i18n-ключи (`src/i18n/en.json`, `src/i18n/ru.json`): `title.markup_partition`, `markup.hint_partition`, `physical.partition_size_title` — все три существуют в обоих locale и (`grep -rn` по `src/`) используются только через `this._tool === 'partition'` (строки 9559, 9598) либо нигде не используются вне собственного определения (`title.markup_partition`) — подтверждено, что это ровно осиротевший набор, ни ключом больше, ни меньше. Ключи `markup.partition`, `physical.partition_properties`, `history.partition_add`, `opening.host_partition`, `confirm.delete_partition_openings_*`, `opening.rebind_partition` — заняты persisted-объектом и диалогами, ТЗ верно не включает их в список удаления. 7. Проверен `test/golden-matrix.test.mjs:72`: `assert.equal(['draw', 'partition'].includes(scenario.planSnap.tool), true, …)` — сейчас допускает оба значения. Проверены реальные сценарии `demo/golden/matrix.mjs` (`planSnap: {...}`, строки 73-80) — оба существующих `planSnap`-сценария уже используют `tool: 'draw'`; ни один golden-сценарий не задаёт `'partition'`. Сужение assert до одного `'draw'` не меняет ни одного изображения — заявление ТЗ «визуальные эталоны не меняются» подтверждено чтением фикстур, а не предположением. 8. Проверены названные в плане автотестов файлы: `demo/smoke_free_walls.mjs` (существует; строки 57-60, 177-181 напрямую ставят `c._tool = 'partition'` и зовут `c._partitionClick(...)` в обход публичного dispatch — ровно то приватное использование, которое ТЗ обязуется перевести на публичный flow), `demo/smoke_plan_snap_overlay.mjs` (существует; строка 185 `card._tool = 'partition'` перед `_markupClick`, что действительно проходит через диспетчер строки 6834-6835), и `demo/smoke_unified_wall_tool.mjs` (существует; не использует приватный tool-state — уже написан против публичного `_tool = 'draw'` + `_keepClosedAsPartitions()`, строка 128 — служит рабочим образцом того, как должны выглядеть переписанные сценарии). 9. Проверено существование паттерна «source-contract test», на который ссылается AC1 как способ доказательства: `test/isometric-contract.test.mjs`, `open-passage-contract.test.mjs`, `optional-space-model-contract.test.mjs`, `release-contract.test.mjs` и другие — это существующая практика репозитория (в т.ч. для внутренних инвариантов), а не изобретённый под это ТЗ механизм. 10. Проверено `docs/USER-GUIDE.ru.md:343` — таблица инструментов Плана уже фиксирует «отдельного инструмента «Перегородка» нет» на пользовательском уровне; терминология ТЗ («persisted-объект остаётся «Перегородка», внутренний tool-token удаляется») не расходится и не переизобретает то, что задокументировано. 11. Проверено `docs/TESTING.md:486-497` («Room markup editor») и `docs/STATUS.md` (`grep -in partition` — без совпадений) — ни один текущий пункт не описывает приватный `_tool='partition'`; изменение кода не требует правки формулировок этих файлов по существу, перечисление их в «Затрагиваемые поверхности» — корректная страховка на случай появления новых заметок в реализации, не декларация обязательной правки. 12. Проверено, что issue #176 не оспорил `small`: сложность/риск 3/10, одна поверхность (`src/houseplan-card.ts` + производные тесты/i18n), нет миграции, нового UX-контракта, влияния на perf/touch — все критерии §5 выполнены одновременно; `trivial` корректно не применён (тип `tech-debt`, не `bug`). 13. Проверено отсутствие ветки `issue/176-*` (`git branch -a | grep 176` — пусто) и отсутствие более раннего `docs/reviews/SPEC-REVIEW-176-*.md` (`ls docs/reviews | grep 176` — пусто) — это первый цикл, r1/2. Гейты (`typecheck`/`test`/`build`) не прогонялись — на этапе ревью ТЗ продуктового кода нет; прогон гейтов не относится к этому этапу (PROCESS.md §2.7/§8). ## Обязательные разделы (§7.1 PROCESS.md, лёгкий трек — тело issue) | Раздел | Есть | Комментарий | |---|---|---| | Сценарий (персона/поверхность/момент) | N/A, обоснованно | задача не создаёт наблюдаемого изменения продукта (`User-Visible: no`, контракт п.6); §7.1 формулирует эти два раздела как продуктовые именно для отделения «работы» от «изменения продукта» — здесь по существу нет стороны-наблюдателя, это подтверждено чтением кода (см. п.3), а не заявлено голословно | | Что человек увидит до/после | N/A, обоснованно | то же: «до» и «после» с точки зрения пользователя идентичны — задача про внутреннюю модель, не про интерфейс | | Проблема | ✅ | подтверждена чтением `houseplan-card.ts` и `wall-face-graph.ts` (см. «Как проверялось» п.2-3), а не декларацией автора | | Контракт (скоуп/не-скоуп внутри пунктов 1-3) | ✅ | граница мёртвый-код/persisted-модель проверена построчно (п.5) и проходит ровно там, где заявлено | | Модель данных и миграция | ✅ | «persisted-модель не меняется» — подтверждено (п.5); миграции нет, потому что нет изменения хранимой схемы | | i18n | ✅ | ровно три ключа, ни больше ни меньше — подтверждено построчной сверкой (п.6) | | AC1…ACn с доказательством | ✅ | 3 штуки, у каждого назван реалистичный и проверяемый способ доказательства (typecheck+source-contract test / unit+targeted smokes / locale unit+build diff+golden:verify) | | План автотестов | ✅ | ссылается на реально существующие файлы (п.7-8), включая точную строку смока, которая сейчас использует приватный API и должна быть переписана | | Риски | частично | явного раздела «Риски» нет, но риск («будущий контрибьютор вернёт мёртвую ветку по аналогии») назван в теле issue и закрывается самим фактом удаления кода — для лёгкого трека §5 отдельного раздела «Риски» не требует (шаблон: проблема · контракт · AC · откат) | | Откат | ✅ | один revert, без миграции данных — обоснованно, так как persisted-схема не тронута | | Release-артефакты | ✅ | явно и корректно: `User-Visible: no` → changelog не требуется; три bundle snapshot синхронизируются по стандартному процессу сборки | Раздел «Принятые предположения» присутствует и корректно отделяет то, что уже прочно установлено чтением кода (`partition` в persisted-типах — не мёртвый код; legacy-нормализатор остаётся намеренно), от того, что не является продуктовым вопросом вовсе (нет UI, нет changelog) — ни одно утверждение раздела не выдаёт техническую догадку за факт. ## Находки Отсутствуют. Ниже — то, что специально проверено на предмет типичных для этого класса дефектов (пропущенный call site, недосказанная граница скоупа, домысленное поведение) и не подтвердилось. - **Домыслы вместо решения не найдены.** Единственное потенциально спорное техническое утверждение — «код недостижим, а не редко используем» — не принято на веру, а прослежено до кода нормализации (`normalizeUnifiedWallTool`, «Как проверялось» п.3): это не догадка автора, а проверяемый факт. - **Скоуп i18n не занижен и не завышен.** Отдельно проверено, что среди трёх удаляемых ключей есть `physical.partition_size_title`, которого не было в исходной находке код-ревью #173 (там названы только `title.markup_partition` и `markup.hint_partition`) — авторская аналитика нашла его самостоятельно и подтвердила чтением (`this._tool === 'partition' ? 'physical.partition_size_title'`, строка 9559); ключ действительно используется только приватным tool-state и нигде в persisted-UI. - **Golden-эталоны не сузятся ошибочно.** Проверено, что сужение `test/golden-matrix.test.mjs:72` до одного `'draw'` не затрагивает ни один реальный сценарий `demo/golden/matrix.mjs` — оба `planSnap`-сценария уже на `'draw'`. ## Что проверено и корректно - **Соответствие `docs/SCOPE.md`:** задача не создаёт нового Core user job и не расширяет ни один из них напрямую — это ожидаемо для `tech-debt`. Косвенная связь с J4/J6 (поддержание единственного тестируемого пути Plan editor, без альтернативного нетестируемого API) не притянута: старый `_partitionClick` действительно обходит crash-safe/finish/limit-контракт, установленный #173 для «unified Walls chain» (J6 — «keep the plan true as the home evolves»). - **Легитимность лёгкого трека:** сложность 3/10, одна поверхность, нет миграции/нового UX-контракта/влияния на perf или touch — критерии §5 выполнены все одновременно; аналитик верно не предложил `trivial` (тип `tech-debt`, критерий §5.1 требует `bug`). - **Продуктовых вопросов действительно нет.** Единственный класс вопросов, который вообще уместен на этом этапе — объём видимого изменения — здесь вырожден: видимого изменения нет вовсе (`User-Visible: no`), и это не недосмотр автора, а прямое следствие того, что удаляется недостижимый код. Технические решения (source-contract test, перевод смоков на публичный flow) корректно оставлены на усмотрение разработчика записью в разделе «Принятые предположения», как и предписывает §7.1 PROCESS.md. - **Граница мёртвый-код / persisted-модель проведена точно и без пропусков** — построчная проверка (см. «Как проверялось» п.5) не нашла ни одного места, где persisted `kind: 'partition'` зависел бы от `this._tool`, и ни одного места, где `this._tool === 'partition'` относился бы к чему-то, кроме активного инструмента разметки. - **Ссылки на существующие файлы не выдуманы** — все три названных в плане автотестов смока (`smoke_free_walls.mjs`, `smoke_plan_snap_overlay.mjs`, `smoke_unified_wall_tool.mjs`) существуют, и первые два действительно используют приватный `_tool = 'partition'`/`_partitionClick` способом, который ТЗ обязуется устранить — заявление в AC2 не голословно. - **Обратная совместимость не сужается.** Legacy warm-viewport токен `'partition'` продолжает нормализоваться в `'draw'` уже существующим кодом (`normalizeUnifiedWallTool`), который ТЗ явно не трогает и защищает unit-тестом (контракт п.2) — соответствует `docs/CONFIG-COMPATIBILITY.md` в части «не сужать уже принятый вход», хотя формально warm viewport — page-memory, а не persisted storage schema, которую документ описывает напрямую. - **Откат тривиален и корректен** — один revert без миграции данных, поскольку persisted-схема не меняется ни в одной части. ## Чего не проверял - Реализацию — её нет: issue в `S4-spec-review`, ветки `issue/176-*` не существует (`git branch -a` пуст по этому номеру) — проверено. - Гейты `typecheck`/`test`/`build`/browser smoke/golden — не относятся к этапу ревью ТЗ; предмет будущего код-ревью (PROCESS.md §2.7/§8). - `python -m pytest tests_backend` — backend (`custom_components/**/*.py`) не упомянут ни в issue, ни в затронутых файлах, и не содержит `partition` как tool-state (только независимая от frontend-инструмента persisted-схема, если вообще есть — не проверялась специально, так как ТЗ прямо заявляет «Backend … не меняется», а весь код-путь — frontend-only `MarkupTool`). - Точный будущий текст source-contract теста и точный способ переписать `smoke_free_walls.mjs`/`smoke_plan_snap_overlay.mjs` на публичный draw/finish flow — это техническое решение реализации (§7.1: «всё, чего пользователь не наблюдает, агенты решают сами»), а не предмет ревью ТЗ. - Реальный визуальный результат `npm run golden:verify` — CSS/геометрия не меняются по заявлению ТЗ и по чтению фикстур (см. «Как проверялось» п.7), но фактический прогон гейта относится к реализации/пре-релизу, не к этому этапу. - Точность числовых оценок аналитики (ценность 1/10, сложность 3/10, P3) по существу — поле владельца (PROCESS.md §2.2), не предмет ревью ТЗ. ## Вердикт Зелёный. High: 0, Medium: 0, Low: 0. ТЗ описывает точно очерченную, доказанную чтением кода (не декларированную) границу между мёртвым tool-state и живой persisted-моделью независимых стен; все технические утверждения (недостижимость `this._tool === 'partition'`, точный список осиротевших i18n-ключей, безопасность сужения golden-matrix assert) проверены построчно и подтвердились. Продуктовых вопросов нет обоснованно — задача не создаёt наблюдаемого изменения, а не умалчивает о нём. AC однозначны и снабжены доказуемыми способами проверки. **Вердикт: зелёный · цикл r1/2 · High: 0 · Medium: 0 → нет · Документ: docs/reviews/SPEC-REVIEW-176-r1.md**