Files
houseplan-card/docs/reviews/CODE-REVIEW-173-r1.md
T
2026-08-18 20:33:44 +03:00

317 lines
30 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.
# CODE-REVIEW-173-r1
- **Issue:** https://github.com/Matysh/houseplan-card/issues/173
- **Диапазон:** `origin/dev..HEAD` (`d096a02` ТЗ, `4f67bac` ревью ТЗ, `f931159`
`feat: unify Plan wall drawing` — единственный коммит класса A/B)
- **Роль:** ревьюер кода (не автор), этап `S7-code-review`, сессия без
контекста реализации
- **Цикл:** r1/4
- **ТЗ:** `docs/specs/173-unified-wall-tool.md`, ревью ТЗ —
`docs/reviews/SPEC-REVIEW-173-r1.md` (зелёный, r1/4, High:0 Medium:0, три Low)
## Скоуп ревью
Основной диф (`f931159`, 28 файлов, +3304/-1926):
- `src/wall-face-graph.ts` — новый чистый модуль planar-graph (atomization,
half-edge face traversal, before/after delta);
- `src/houseplan-card.ts` — интеграция: единый tool «Стены», finish открытой
цепочки, `_wallFaceBatch` (очередь faces), atomic batch commit, provenance
толщины, compatibility со старым `partition` warm-tool;
- `test/wall-face-graph.test.mjs` — unit на чистый модуль;
- `demo/smoke_unified_wall_tool.mjs` (новый), `demo/smoke_room_autoclose.mjs`,
`demo/smoke_draw_wall_thickness.mjs` — целевые/регрессионные smoke;
- `demo/benchmark_large_house.mjs`, `demo/performance/*` — accepted-click
измерение и bounded-cache бюджет для `large-house-plan-snap-v1`;
- i18n `en.json`/`ru.json`, `docs/{ARCHITECTURE,CANVAS,WALL-THICKNESS,UX-MODES,
USER-GUIDE.ru,USER-GUIDE,TESTING,TOUCH-SUPPORT,STATUS}.md`,
`docs/CHANGELOG{.ru,}.md`;
- `dist/houseplan-card.js`, две другие копии бандла (класс D).
Не в скоупе изменений (проверено по `git diff --stat`, класс не задет):
`custom_components/houseplan/**/*.py`, backend schema, `demo/golden/**`.
## Как проверялось — гейты
| Гейт | Команда | Результат |
|---|---|---|
| typecheck | `npx tsc --noEmit` | pass, без ошибок |
| unit | `npm test` | **843/843 pass** (`npm run inventory`: Node unit 843) |
| build + sync бандла | `npm run build && cmp dist/houseplan-card.js custom_components/houseplan/frontend/houseplan-card.js && cmp dist/houseplan-card.js demo/srv/assets/houseplan-card.js` | pass, три копии побайтно идентичны |
| целевой smoke (AC1–AC13) | `node demo/smoke_unified_wall_tool.mjs` | **19/19 pass** |
| регрессия #138/AC7 | `node demo/smoke_room_autoclose.mjs` | 9/9 pass, включая `xIntersectionOffersFacesWithoutPartialRooms`, `openingCutPreventsAutoClose` |
| регрессия толщины (AC1/AC12) | `node demo/smoke_draw_wall_thickness.mjs` | 11/11 pass, `drawButtonNamedWalls: true` |
| touch safety floor (AC3/AC16) | `node demo/smoke_editor_gestures.mjs` | 5/5 pass — pinch/pan не рисуют, tap рисует |
| overlap/island примитив (косвенно к AC8) | `node demo/smoke_island_rooms.mjs` | 7/7 pass — но см. Medium-2, этот smoke не тронут #173 и не проходит через новую интеграцию |
| performance (AC15) | `npm run benchmark:large-house-plan-snap --profile=large-house-plan-snap-v1` | pass, без брошенных `structural contract failed`/`accepted-click contract failed`; `wallFaceAcceptedClickMs` ≈ 2.4–4.1 мс (порог в самом бенчмарке — 1000 мс); `wallFaceGraph` cache entries = 2 (бюджет: max 4, growth 0) |
| process-gate (локально) | `node scripts/process-gate.mjs` | «гейт пройден, предупреждений 0» (диапазон `origin/dev..HEAD`, 3 коммита) |
| мутационная проверка «тест умеет падать» | вручную: 1) снял фильтр `sourceKeys.includes(addedSourceKey)` в `findNewWallFacesInGraphs` — ни один тест не покраснел (см. «Что проверено», пункт про математическую избыточность); 2) снял `consumed.has(atom.key)` в `_applyWallFaceBatch` — **2/19** проверок `smoke_unified_wall_tool` покраснели (`acceptCreatesRoomAtomically`, `mixedQueueAppliesOnce`); оба раза откат, бандл пересобран и сверен | подтверждает дисциплину §18 для проверенных тестов |
**Не прогонялось и почему:**
- `npm run golden:verify` — diff не содержит изменений `demo/golden/**`,
никакой baseline не принимался; по процессу (§8/§11.4) golden — предрелизный
гейт, а не гейт код-ревью. AC12 (визуальная неизменность Plan/View/Iso)
проверено чтением кода (см. ниже), не исполнением golden.
- `python -m pytest tests_backend -q` — ни один файл
`custom_components/houseplan/**/*.py` не тронут (подтверждено `git diff
--stat`), AC14 не требует прогона backend.
- Полный набор из 138 browser-smoke — диспропорционально задаче: тронута
ровно одна поверхность (Plan editor, единый инструмент стен), прогнаны все
smoke, прямо названные в AC/§15.2 плюс два соседних (толщина, gestures,
island-overlap) по diff-риску. Остальные 132 не имеют пересечения с
изменённым кодом.
- `npm run benchmark:compare` против baseline SHA — нет сохранённого отчёта
предыдущего SHA в этой сессии; сырые цифры бенчмарка (см. выше) сверены с
`demo/performance/budgets-large-house-plan-snap.json` вручную — ни один
существующий бюджет (`cacheEntries`, `cacheGrowth`, timings) не ослаблен,
только добавлены новые ключи `wallFaceGraph: 4 / growth 0`.
## Проверка AC1–AC17
| AC | Вердикт | Как доказано |
|---|---|---|
| AC1 | ✅ | unit (`legacy Partition token becomes Walls…`) + smoke (`oneWallsButton`, `partitionButtonRemoved`, `splitRemains`). Golden не прогонялся — предрелизный гейт |
| AC2 | ✅ | код-чтение `_persistActiveDraftSegment`/`_markupClick` (первая точка не пишет config) + smoke (`openChainIsCrashSafeDraft`, `toolChangeFinishesPartitions`, `finishedChainDoesNotResume`) |
| AC3 | ✅ | код-чтение `_onKey`/`_finishWallChain`/`_cancelPath` (pan/pinch/pointercancel не вызывают finish — подтверждено отсутствием изменений в их обработчиках) + smoke (`escapeUndoContractStillRemovesLastSegment`, `ctrlZInQueueRemovesTerminalPoint`, `cancelKeepsWholeTerminalDraft`) + `smoke_editor_gestures` (pinch/pan не рисуют). Reload/remount — только чтением, отдельного smoke на это нет |
| AC4 | ✅ | unit `wall-face-graph.test.mjs` (endpoint/T/X/collinear, near-miss, malformed, permutation invariance, 2000-сегментный sparse plan) |
| AC5 | ✅ | unit + прочитан алгоритм: canonical half-edge/DCEL walk с `identityCycle`, area-фильтром > 0 корректно исключает compound/exterior циклы структурно, не эвристически |
| AC6 | ✅ | unit (`delta only returns faces introduced by…`) + smoke (`closedFaceOpensDialog`, `terminalSegmentPersistedBeforeDecision`) + `smoke_room_autoclose` |
| AC7 | ✅ | smoke (`multiFaceTClosureOrdered`, T/X-сценарии `smoke_unified_wall_tool` и `smoke_room_autoclose`); gap через opening — `openingCutPreventsAutoClose: true` |
| AC8 | ⚠️ проверено чтением, не исполнением | `_overlapRoom`/`roomsOverlap` не изменены этим issue; wiring в `_offerWallFaces`/`_applyWallFaceBatch` прочитан и выглядит корректным (двойная проверка: на предложении face и повторно при commit). **Но** ни unit (в `wall-face-graph.test.mjs`, где такой тест структурно невозможен), ни smoke не проверяют duplicate/partial-overlap/nesting именно через новую интеграцию — см. Medium-2, заведён #177 |
| AC9 | ✅ | smoke (`cleanSplitUsesOneCandidate`, `cleanSplitRetainsParentMetadata`, `cleanSplitConsumesDivider`); `splitRoomPath`/`roomsOverlap` не изменены, регрессия Split подтверждена тем, что связанные unit-тесты (не тронуты) в общем прогоне 843/843 зелёные |
| AC10 | ✅ | smoke (`multiFaceTClosureOrdered` — area order; `firstQueueDecisionIsBuffered`; `mixedQueueAppliesOnce` — одна транзакция); Cancel/error — `cancelKeepsWholeTerminalDraft` |
| AC11 | ✅ | smoke + мутационная проверка (снятие `consumed.has(atom.key)` ломает `acceptCreatesRoomAtomically`/`mixedQueueAppliesOnce`) |
| AC12 | ⚠️ проверено чтением, не исполнением | `_applyWallFaceBatch` переиспользует немодифицированные `materializeWallIntervals`, `applyWallThicknessToNewRoom`, `wallIntervals`, `setWallThickness`, `_normalizeWalls` — те же хелперы, что и старый `_commitRoom`/Split; второй renderer не введён. Golden (визуальная неизменность Plan/View/Iso) не прогонялся — предрелизный гейт |
| AC13 | ✅ | код-чтение (`MAX_PARTITIONS`/`MAX_ROOMS` проверяются до mutation в `_finishWallChain` и `_applyWallFaceBatch`) + `history.wall_chain_finish`/`history.wall_face_batch` как именованные команды. Save-conflict path не эмулировался отдельно — проверено чтением, что batch не трогает существующий reload/conflict механизм |
| AC14 | ✅ | `git diff --stat` подтверждает: ни один файл `custom_components/houseplan/**/*.py` не тронут; новых persisted полей нет (модель диффа `src/houseplan-card.ts` не добавляет новых ключей конфига, только новые runtime-поля класса) |
| AC15 | ✅ | `npm run benchmark:large-house-plan-snap` выполнен: budgets не ослаблены, accepted-click ~2.4–4.1 мс, cache bounded (2 из максимум 4), traversal не запускается на pointermove (assert `wallFaceCacheStableOnPointer` в самом бенчмарке) |
| AC16 | ✅ | `smoke_editor_gestures` (pinch/pan не рисуют) + код-чтение: в диффе нет ни одного изменения в обработчиках `pointercancel`/pinch/pan — новые finish-точки триггерятся только явной сменой tool/mode/space, что подтверждено grep по всему диффу (0 совпадений на `pointercancel|pinch|suppress`) |
| AC17 | ✅ | typecheck/unit/build зелёные, три копии бандла идентичны (см. таблицу гейтов), RU/EN i18n и все перечисленные в §17 ТЗ документы обновлены тем же коммитом `f931159`; `User-Visible: yes`, оба changelog правлены |
## Находки
Находок уровня **High** нет.
### Medium-1 — мёртвый код старого инструмента `partition` не удалён
**Файлы:** `src/houseplan-card.ts:502,533,2187,2216,2269,5216,5283,5304,5341,6178,
6415,6643-6644,6943,9221,9259-9260,9278,11388,16587,17243,17358,17360,17410`;
`src/i18n/en.json`/`ru.json` (`title.markup_partition`, `markup.hint_partition`)
Кнопка `Partition` убрана из toolbar (AC1 подтверждён), но сам инструмент
внутри модели — нет. `MarkupTool` по-прежнему содержит значение `'partition'`,
`MARKUP_TOOLS` его тоже содержит, приватный метод `_partitionClick(...)`
полностью реализует старую логику «два клика → одна перегородка», и порядка
15 условных веток (`this._tool === 'draw' || this._tool === 'partition'`) по
всему файлу продолжают его учитывать — в лимитах, snap, hints, Undo/Escape,
рендере превью.
**Воспроизведение недостижимости:** единственное место, присваивающее
`this._tool` из внешнего/сохранённого значения —
`this._tool = normalizeMarkupTool(vp.tool)` (warm viewport). Внутри
`normalizeMarkupTool` любое значение сначала проходит через
`normalizeUnifiedWallTool(value)`, которое безусловно маппит
`'partition' → 'draw'` **до** проверки `MARKUP_TOOLS.has(...)`. Ни один
обработчик клика тулбара не вызывает `_activateMarkupTool('partition')`
(`grep -n "_activateMarkupTool("` — 10 вызовов, ни одного с `'partition'`).
Значит, `this._tool === 'partition'` не может стать истинным при обычном
пользовательском взаимодействии — код действительно недостижим, а не просто
редко используем.
Функционального дефекта это не создаёт (проверено чтением и грепом
исчерпывающе), но:
1. противоречит собственной идее #173 — «единый инструмент» — на уровне
внутренней модели, а не только UI;
2. `title.markup_partition`/`markup.hint_partition` стали осиротевшими i18n
ключами (использовались только удалённой кнопкой);
3. риск: будущий контрибьютор может по аналогии вернуть кнопку/ветку,
реанимировав нетестируемое, не соответствующее новому crash-safe/finish/
limit контракту поведение.
**Решение ревьюера:** Medium, не блокирует — заведён
[#176](https://github.com/Matysh/houseplan-card/issues/176)
(`tech-debt`, `P3`, `S1-new`).
### Medium-2 — AC8 не имеет unit/smoke-доказательства именно для новой интеграции
**Файлы:** `test/wall-face-graph.test.mjs` (нет теста overlap/duplicate/
nesting), `demo/smoke_unified_wall_tool.mjs`, `demo/smoke_room_autoclose.mjs`
(ни один не проверяет exact-duplicate/partial-overlap отказ или
nested-room через `_offerWallFaces`/`_applyWallFaceBatch`)
ТЗ §15.1 п.6 относил проверку «exact duplicate, partial overlap, inner/outer
nesting» к unit-суите `wall-face-graph.test.mjs`. Структурно это невозможно:
overlap-семантика (`_overlapRoom`, `roomsOverlap`) живёт в
`src/houseplan-card.ts` и не тронута этим issue, а `wall-face-graph.ts` —
чистый модуль без знания о комнатах. Ни там, ни в добавленном/изменённом
smoke нет сценария, который замыкает новую площадь поверх существующей
комнаты (duplicate), частично поверх неё (partial) или полностью внутри неё
(nesting) через новый unified-инструмент. Существующий
`demo/smoke_island_rooms.mjs` (не тронут #173) проверяет тот же примитив
`_overlapRoom` напрямую и через легаси `_commitRoom()`, минуя
`_markupClick`/`_offerWallFaces` — то есть не покрывает именно новую
интеграцию.
Код-чтение подтверждает, что wiring корректен: `_offerWallFaces` фильтрует
`eligible` через `!this._overlapRoom(face.ring)` при предложении, а
`_applyWallFaceBatch` независимо повторяет проверку через `roomsOverlap`
против `existingRooms` перед commit (defense-in-depth по §10.3 ТЗ) — но
заявленный в AC8 способ доказательства (`unit + smoke`) для этой конкретной
поверхности фактически не выполнен.
**Решение ревьюера:** Medium, не блокирует — заведён
[#177](https://github.com/Matysh/houseplan-card/issues/177)
(`tests`, `tech-debt`, `P3`, `S1-new`).
### Low-1 — `_activateMarkupTool` использует if/else без скобок на одной значимой ветке
**Файл:** `src/houseplan-card.ts:6057-6060`
```ts
if (this._tool === 'draw' && !this._finishWallChain()) return;
else this._cancelPath();
this._tool = tool;
```
Работает корректно (проверено чтением и трассировкой всех вызовов
`_activateMarkupTool`): при активном tool `draw` и успешном
`_finishWallChain()` код всё равно доходит до `_cancelPath()`, что и нужно
для сброса `_pendingSplit`/`_splitSel`/т.п. при смене инструмента. Но форма
`if (...) return; else ...;` без фигурных скобок на пустой видимой границе
читается медленнее, чем заслуживает; в проекте, разделяющем ревью на «что
проверено чтением», лишняя секунда на разбор управляющей структуры — не
абстрактная придирка.
**Решение ревьюера:** Low, не блокирует. Можно поправить форматированием при
следующей правке этого метода либо оставить как есть.
### Low-2 — accepted-click измерение не оформлено как отдельный отслеживаемый бюджет
**Файл:** `demo/performance/budgets-large-house-plan-snap.json`,
`demo/benchmark_large_house.mjs`
`wallFaceAcceptedClickMs` проверяется инлайновым порогом прямо в скрипте
бенчмарка (`> 1000` → throw), а не как отслеживаемая метрика в
`timings` схемы бюджета (в отличие от `planSnapPointerMs`,
`spaceSwitchMs` и т.д., которые сравниваются с историческим SHA через
`benchmark:compare`). Фактический результат (2.4–4.1 мс) настолько далёк
от порога, что регрессии первого порядка это не пропустит, но постепенный
дрейф (например, до 300–500 мс) не будет замечен `compare.mjs` между
релизами, как замечаются остальные тайминги.
**Решение ревьюера:** Low, не блокирует. На усмотрение автора — добавить
`wallFaceAcceptedClickMs` в отслеживаемые `timings` бюджета в будущей
правке производительности, либо оставить как есть, раз запас по порогу
на два с лишним порядка.
## Что проверено и корректно
- **Алгоритм planar-graph (`wall-face-graph.ts`)** — canonical half-edge/DCEL
traversal с `identityCycle` (убирает derived collinear-вершины из identity,
чтобы безобидная T-подсадка на прямой стене не выглядела «новой» faces) и
положительной/отрицательной ориентацией для отсечения exterior/compound
циклов **структурно**, не эвристическим pairwise-поиском, который прямо
запрещён §9.3 ТЗ. Sweep-broadphase на interval treap даёт заявленную
сложность без деградации на разреженных планах (unit: 2000 сегментов,
`faces.length === 0`, детерминированный порядок атомов при реверсе ввода).
- **Дисциплина «тест должен уметь падать» подтверждена мутационно.** Снятие
проверки `consumed.has(atom.key)` в `_applyWallFaceBatch` (AC11) ломает
2/19 проверок `smoke_unified_wall_tool.mjs`
(`acceptCreatesRoomAtomically`, `mixedQueueAppliesOnce`); откат и повторная
сборка подтвердили возврат к 19/19. Отдельно проверено (не для отчёта, а
для собственной уверенности), что снятие фильтра
`sourceKeys.includes(addedSourceKey)` в `findNewWallFacesInGraphs` не ломает
ни одного теста — это математически корректно: в планарном графе
добавление ровно одного ребра не может создать face, не содержащую это
ребро на границе, поэтому фильтр — защитный дубль поверх уже достаточного
`beforeKeys`-diff, а не непротестированная дыра.
- **Finish-контракт (AC2/AC3) на всех трёх точках выхода.** `_slideTo`,
`_onHashChange` и переключение mode (`_setMode`-путь) единообразно вызывают
`if (this._wallFaceBatch) this._roomDialogCancel(); if (this._mode ===
'plan' && this._tool === 'draw' && !this._finishWallChain()) return;` —
проверено построчно по всем трём местам диффа, поведение идентично.
- **Cancel/Escape корректно восстанавливает terminal draft.** `_roomDialogCancel()`
при активном `_wallFaceBatch` восстанавливает `_path`/`_draftSegmentCms`/
`_activeDraftId` из снимка `batch.activePath`/`activeCms`/`activeDraftId`,
сделанного в момент `_beginWallFaceBatch`; плейн Escape достаёт
`_roomDialogCancel()` через уже существующую ветку `if (this._roomDialog)`.
- **Лимиты проверяются до mutation.** И `_canAppendRoomDraftPoint` (на каждый
клик — резервирует место под будущий finish), и `_finishWallChain`
(`MAX_PARTITIONS`), и `_applyWallFaceBatch` (`MAX_ROOMS`, повторно
`MAX_PARTITIONS`) — везде проверка предшествует записи в `sp`, что закрывает
риск ТЗ «Finish превысит partition limit при смене tool».
- **Clean split переиспользует немодифицированные `splitRoomPath`/`roomsOverlap`.**
`_offerWallFaces` вызывает существующий `splitRoomPath` на полном `this._path`
для каждой существующей комнаты; поскольку `splitRoomPath` требует, чтобы
все промежуточные точки пути лежали строго внутри полигона комнаты и ни
один сегмент не пересекал её стену, геометрически невозможно, чтобы тот же
клик одновременно давал net clean split одной комнаты И независимую
постороннюю face за её пределами — ветка `return` сразу после первого
найденного split-совпадения безопасна, не теряет параллельные кандидаты.
- **Provenance и толщина (AC10.4/AC11).** `_wallSourceCmAt` ищет существующий
сегмент (`sourceKind === 'partition'`/room-draft) по расстоянию до точки
раньше активной цепочки, обеспечивая приоритет существующей authoritative
толщины над `DRAW_WALL_DEFAULT_CM`; consumed/unconsumed atoms разделяются
через `atomizeWallSegments(...).sourceKeys` пересечение с `activeSourceCms`.
- **Совместимость (AC14).** Ни один файл `custom_components/houseplan/**/*.py`
не тронут; `git diff --stat origin/dev...HEAD` подтверждает отсутствие
изменений backend/schema. Legacy `room_drafts`/`partitions` не мигрируются
автоматически (не тронуты нормализацией при чтении, только на explicit
finish/batch) — соответствует §11 ТЗ.
- **Документация консистентна и полна (AC17, §17 ТЗ).** Все перечисленные в
§17 документы обновлены тем же коммитом; все три Low-находки предыдущего
SPEC-REVIEW-173-r1 (отсутствие заголовка «Проблема», дрейф терминологии
«Walls» в `UX-MODES.md`, опечатка в AC17) исправлены автором в этом же
диффе (`git diff d096a02..HEAD -- docs/specs/173-unified-wall-tool.md`,
`docs/UX-MODES.md`).
- **i18n RU/EN синхронны** — каждый новый/изменённый ключ (`markup.add`,
`title.markup_add`, `markup.hint_points`, `markup.hint_start`,
`btn.keep_as_walls`, `room.queue_progress`, `toast.wall_rooms_saved`,
`toast.wall_chain_saved`, `history.wall_chain_finish`,
`history.wall_face_batch`) присутствует в обоих файлах.
- **Трейлеры и changelog.** Единственный коммит класса A/B, `f931159`, несёт
`Issue: #173` и `User-Visible: yes`; оба changelog (`docs/CHANGELOG.md`,
`docs/CHANGELOG.ru.md`) правлены тем же коммитом. `process-gate.mjs`
подтверждает 0 предупреждений на диапазоне `origin/dev..HEAD`.
## Чего не проверял
- Не прогонял `golden:verify` — diff не трогает `demo/golden/**`, ни один
baseline не принимался; AC12 (визуальная неизменность рендера) проверено
только чтением кода (переиспользование немодифицированных canonical
хелперов), не исполнением golden. Это предрелизный гейт по процессу.
- Не прогонял `pytest tests_backend` — ни один Python-файл не тронут, AC14 не
требует backend-исполнения.
- Не прогонял полный набор из 138 browser-smoke — прогнаны все явно названные
в AC/§15.2 плюс три соседних по diff-риску (толщина, gestures, island-
overlap primitve); оставшиеся 132 не пересекаются с изменённой
поверхностью.
- Не воспроизводил multi-client optimistic-lock conflict во время открытого
`_wallFaceBatch` вживую (WS reconnect с параллельной правкой) — только
чтением кода, что batch не вводит новый conflict-путь помимо уже
существующего `_saveConfig`/reload механизма (AC13 «save conflict»).
- Не проверял reload/remount во время активной цепочки исполнением (нет
выделенного smoke на это в диффе) — только чтением: ни один код-путь
`_finishWallChain`/`_wallFaceBatch` не вызывается из reload-обработчика,
только из явных tool/mode/space переходов.
- Не запускал `npm run benchmark:compare` против сохранённого отчёта базового
SHA (такого отчёта нет в этой сессии) — сверил сырые цифры одного прогона
бенчмарка вручную с `budgets-large-house-plan-snap.json`.
- Не проверял алгоритмическую сложность `O((E + I) log E)` формальным
профилированием на больших E — доверился unit-тесту на 2000 сегментов
(не показал деградации) и общей архитектуре sweep+interval-treap, которая
структурно даёт эту сложность.
## Вердикт
Зелёный. High: 0, Medium: 2 (оба вынесены отдельными issue —
[#176](https://github.com/Matysh/houseplan-card/issues/176) мёртвый код
инструмента `partition`, [#177](https://github.com/Matysh/houseplan-card/issues/177)
недостающее unit/smoke-покрытие overlap/duplicate/nesting для новой
интеграции), Low: 2 (не блокируют, оставлены на усмотрение автора). Все 17 AC
подтверждены — большинство тестами с проверенной дисциплиной «тест умеет
падать» (мутационная проверка), часть чтением кода с явной пометкой; ни одна
находка не свидетельствует о том, что изменение не решает заявленный сценарий
или ухудшает смежное поведение.