34 KiB
CODE-REVIEW-74-r1
- Issue: https://github.com/Matysh/houseplan-card/issues/74
- Этап: код-ревью (PROCESS.md §2.7)
- Материал:
git log --oneline origin/dev..HEADиgit diff origin/dev...HEADна SHAfc63f3bf36687cd98c9777ce683d2f2e49d39ff6(веткаissue/74-device-position-undo). Ветка приведена конвейером кdevдо ревью (легло 10 коммитовdev,d0e1a4b0→fc63f3bf) — по §7.2 это другой код, разбор полный, не по дельте. - Заход: r1 (первый код-ревью; предыдущий вердикт в issue — по ТЗ,
SPEC-REVIEW-74-r1, зелёный, бюджет код-ревью не расходовал) - Вердикт: жёлтый
- High: 0 · Medium: 1 (в скоупе, чинится в этой же задаче) · Low: 2 (записаны, не блокируют)
Скоуп разбора
Первый заход код-ревью — полный разбор диапазона origin/dev..HEAD (6 коммитов:
спецификация, актуализация, документ спек-ревью, реализация, применение замечаний спек-ревью,
выравнивание регрессионных смоков), всех 14 AC из docs/specs/074-device-position-undo.md,
трейлеров, changelog, и гейтов из PROCESS.md §8/§10.2, соразмерно объёму задачи (новая
independent history для позиций устройств; backend/схема не менялись — подтверждено:
git diff --stat не содержит custom_components/**/*.py).
Как проверялось
Зелёного Validate на этом SHA нет, поэтому дешёвые и часть требуемых по диффу гейтов прогнаны самостоятельно:
| Гейт | Команда | Результат |
|---|---|---|
| Typecheck | npx tsc --noEmit |
OK, без ошибок |
| Unit | npm test |
tests 1634, pass 1633, fail 0, skipped 1 |
| Build | npm run build |
OK, dist собран |
| Bundle sync | cmp dist/houseplan-card.js custom_components/houseplan/frontend/houseplan-card.js |
совпадает; npm run bundle:sync не создал diff в git — дерево уже синхронизировано в коммите |
| Bundle budget | npm run bundle:budget |
initial View 281796 B gzip (бюджет 300000 B, запас 18204 B) |
| Docs fingerprint | node scripts/check-docs.mjs |
Documentation checks passed (7 files, 10 external links) |
any-гейт |
node scripts/no-new-any.mjs --base origin/dev --head HEAD |
Новых any нет (380 добавленных строк в 3 файлах) |
| Coordinate write barrier | node scripts/coordinate-write-barrier-guard.mjs |
OK (обновлённый инвариант: 3 канонических layout/update) |
| Process gate | node scripts/process-gate.mjs --base origin/dev --head HEAD --issues |
гейт пройден, предупреждений 0 (6 коммитов в диапазоне) |
| Smoke selection | node scripts/smoke-select.mjs --base origin/dev --head HEAD |
35 прямых совпадений, 21 слабая связь (полный вывод — раздел «Выбор смоков» ниже) |
| Targeted smokes (AC1-AC12, AC5, AC9, AC14) | node demo/smoke_device_position_history.mjs |
OK, все 27 полей true |
node demo/smoke_drag_bounds.mjs |
OK | |
node demo/smoke_modes.mjs |
OK | |
node demo/smoke_pan_any_zoom.mjs |
OK | |
node demo/smoke_grid_snap.mjs |
OK | |
node demo/smoke_editor_tabs.mjs |
OK | |
node demo/smoke_layout_sync.mjs |
OK | |
| Мутационная проверка «тест умеет падать» | node scripts/mutation-gate.mjs --id=device-position-cancel-routed-to-commit |
чистый прогон OK, мутант «pointercancel → commit» — smoke_device_position_history.mjs краснеет, как обязан |
node scripts/mutation-gate.mjs --id=stale-space-position-guard-removed |
чистый прогон OK, мутант краснеет, как обязан | |
| Golden (диагностика, не приёмка) | node demo/golden/run.mjs --mode=capture --scenario=geometry-devices-editor-dark |
different — см. находку Medium-1 |
node demo/golden/run.mjs --mode=capture --scenario=geometry-plan-editor-dark |
passed (использован для локализации причины Medium-1, см. ниже) |
|
| Model invariants | не прогонялся | diff не трогает геометрию комнат/стен/marker.space/open_spans — только houseplan/layout (позиции устройств), это вне предмета scripts/model-invariants.mjs (проверено чтением скрипта: инварианты про полигоны комнат и толщину стен) |
python -m pytest tests_backend |
не прогонялся | diff не содержит custom_components/**/*.py (подтверждено git diff --stat) |
Выбор смоков — обоснование по каждой строке
smoke-select.mjs вернул 35 прямых совпадений. Прогнаны все, что относятся к изменённым путям
исполнения (drag/preview/persist/toolbar/keyboard/layout-sync), а не только упомянутые в тексте
AC14:
- Прогнаны (7):
smoke_device_position_history(новый, основной AC-смок),smoke_drag_bounds,smoke_modes,smoke_pan_any_zoom,smoke_grid_snap(все изменены самим диффом — обязаны быть перепроверены целиком, а не доверием к тому, что автор их поправил),smoke_editor_tabs(проверяет количество кнопок панели и доступные имена — прямое AC13/AC9),smoke_layout_sync(совпал по_sentPos/_serverStorage, которые diff меняет по типу — точечный update/delete tombstone теперь может бытьnull). - Не прогнаны, решение — false positive (
redoName/undoNameсовпадения вsmoke_furniture.mjs,smoke_unified_wall_tool.mjs,smoke_v8_draft_write.mjs): проверено чтением — это геттеры другого, не тронутого экземпляраCommandStack(_geometryHistory.undoName/redoName,src/houseplan-editor-runtime.ts), совпадение по имени свойства класса, не по факту зависимости от нового кода._geometryHistoryне тронут этим диффом (не входит вgit diff --stat), новый_devicePositionHistory— отдельный самостоятельный экземпляр без общего состояния. - Остальные 28 из 35 (
smoke_junction_limits,smoke_active_chain_ink,smoke_align_guides,smoke_controls,smoke_device_inbox,smoke_editor_gestures,smoke_fixed_floor,smoke_help_affordance,smoke_infinite_canvas,smoke_junction_holes,smoke_lattice_write_barrier,smoke_long_press_gesture,smoke_orphan_space_references,smoke_partition_openings,smoke_plan_snap_overlay,smoke_room_cards,smoke_room_resize,smoke_tap_ctx,smoke_wallthick_standalone,smoke_ws_resilience,smoke_zero_wall_migration_unblocked,smoke_zoom_out,smoke_decor,smoke_dialog_footer_width,smoke_optimize_coordinate_canonicalization,smoke_optional_space_model,smoke_v8_draft_write(частично)) — не прогнаны: совпадение по общим символам верхнего уровня (NORM_W,_mode,_showToast,_serverStorage,_drag,_suppressClick,_selId,_commitSpace,_holdFired,_restoreZoom,_savePosчерез room-label путь), ни один из которых этот diff не меняет по семантике — прогон всех 35 не соразмерен задаче (PROCESS.md §8, «полные наборы — предрелизный гейт»). Автор в хендоффе также прогонял более широкий список (smoke_room_cards,smoke_ws_resilience,smoke_long_press_gesture,smoke_fixed_floor) — не переисполнялись повторно, доверие к зафиксированной команде и результату оправдано, поскольку это чистые "прочитал/не менял" пути. - 21 слабая связь (все —
_mode, самый частый символ проекта): не прогонялись,_modeпроверяется по значению'devices'в изменённом коде, но сам механизм переключения режимов не менялся (проверено чтением_setMode/_mode— единственное новое место, где_mode === 'devices'влияет на поведение, это_onKey, уже покрыт прогоном AC9). golden/performance_smokeполным набором — не прогонялись намеренно: предрелизный гейт (PROCESS.md §8, §11.4), не гейт код-ревью. Один сценарий прогнан диагностически (см. ниже).
Находки
Medium-1 — новый Undo/Redo toolbar рендерится без иконок из-за неактуального demo/srv/assets/icons.js; сломан именно опубликованный в этом же коммите скриншот docs/images/06-device-editor.png
В скоупе задачи — картинка и golden-сценарий, которые проваливаются, это собственная поставка
#74 (новый Device-editor toolbar), обновлённые в том же коммите bdf81fad.
Воспроизведение:
node demo/golden/run.mjs --mode=capture --scenario=geometry-devices-editor-dark
# → different
Диагностический diff-кадр (artifacts/golden/diff/geometry-devices-editor-dark.png) подсвечивает
розовым ровно область панели устройств справа от «Icon rules» и в подписи вкладки — крупная
геометрия плана снизу идентична существующему baseline. Кроп фактического кадра
(artifacts/golden/actual/geometry-devices-editor-dark.png, область x=950-1130,y=108-160,
увеличено ×5) показывает две пустые скруглённые кнопки без единого видимого штриха иконки — рядом
с полностью корректно отрисованной иконкой mdi:close.
То же самое видно в уже закоммиченном как часть этого диффа пользовательском скриншоте
docs/images/06-device-editor.png (обновлён коммитом bdf81fad именно для показа новых кнопок):
кроп той же области подтверждает пустые кнопки без иконок рядом с корректной mdi:close. Это
опубликованная картинка из docs/USER-GUIDE.md, а не только внутренний тестовый артефакт.
Причина (проверено чтением и точечным экспериментом, изменения не закоммичены):
demo/srv/demo.html эмулирует <ha-icon> через window.__ICONS[iconName], заполняемый
сгенерированным demo/srv/assets/icons.js (генератор — demo/gen_icons.mjs, сканирует все
mdi:* в src/+demo/ и берёт SVG-path из пакета @mdi/js). В закоммиченном
demo/srv/assets/icons.js нет записей для mdi:undo-variant/mdi:redo-variant
(grep -c → 0 совпадений), хотя @mdi/js их экспортирует (mdiUndoVariant, mdiRedoVariant
существуют). Диагностический прогон node demo/gen_icons.mjs (не закоммичен, откачен обратно
git checkout -- demo/srv/assets/icons.js) подтверждает: после регенерации файл вырастает с 33424
до 42629 байт и обе иконки появляются в карте (155 из 157 вместо меньшего числа), то есть
единственная причина — устаревший сгенерированный файл, не отсутствие иконки в принципе.
Это не регрессия конкретно #74 по корню, но именно #74 — первая задача, которая закоммитила
видимое проявление. Тот же дефект уже присутствует на origin/dev до этой ветки: диагностический
node demo/golden/run.mjs --mode=capture --scenario=geometry-plan-editor-dark вернул passed
(рендер идентичен уже принятому baseline), но кроп самого demo/golden/baselines/geometry-plan-editor-dark.png
в области persistent Undo/Redo кнопок Plan-редактора (src/houseplan-editor-runtime.ts:11737-11745
на origin/dev, тот же mdi:undo-variant/mdi:redo-variant, тот же паттерн .btn.ghost) показывает
точно такие же пустые кнопки без иконок — значит, icons.js был неактуален уже на dev до
этой ветки, и никто это не заметил, потому что до сих пор ни один поддерживаемый в актуальном
docs/images/* кадр не показывал Plan-редактор с открытой панелью инструментов крупным планом.
#74 — первая задача, которая (а) добавляет второе место с тем же сломанным паттерном и
(б) коммитит новый документационный скриншот именно этой области, тем самым превращая
молчаливый локальный гэп в опубликованный дефект.
Почему Medium, а не High: реальные пользователи Home Assistant не увидят проблему — их
браузер резолвит <ha-icon> через полный набор MDI из фронтенда HA, а не через
demo/srv/assets/icons.js (это стенд-заглушка только для демо/тестового харнесса, подтверждено
чтением demo/srv/demo.html:21-40). Функционально кнопки работают полностью корректно (все 27
проверок smoke_device_position_history.mjs, включая доступные имена и disabled-состояние, зелёные)
— это дефект качества визуального тестового/документационного артефакта, а не поведения продукта.
Почему в скоупе, а не отдельный issue: сломанный артефакт — docs/images/06-device-editor.png
и golden-сценарий geometry-devices-editor-dark — оба созданы/обновлены именно этим коммитом ради
показа новых кнопок из #74; по смыслу задачи AC13 требует «Golden + DOM/a11y assertions» на этот
самый toolbar, и golden-часть доказательства сейчас недостоверна. Возврат на правку: node demo/gen_icons.mjs, зафиксировать обновлённый demo/srv/assets/icons.js, пересобрать бандл-стенд,
пересъёмка docs/images/06-device-editor.png каноническим Docs screenshots workflow +
npm run docs:accept -- --reviewed, обновление golden-эталона geometry-devices-editor-dark через
npm run golden:accept -- --reviewed на полном Linux CI артефакте (PROCESS.md §8, §13). Побочный
эффект — тот же коммит заодно чинит уже существующий на dev дефект Plan-редактора; это не
расширение скоупа, а тот же самый generated-файл одной командой, но если автор предпочтёт не трогать
несвязанный geometry-plan-editor-dark baseline в этой ветке — это не блокирует #74: обязателен
только фикс, покрывающий заявленный в AC13 сценарий этой задачи.
Low-1 — AC2 «два устройства в LIFO-порядке» не продемонстрирован реальным перетаскиванием двух устройств подряд
docs/specs/074-device-position-undo.md:246 требует для AC2 браузерный смок, показывающий exact
before/after «для двух устройств... в LIFO-порядке». demo/smoke_device_position_history.mjs
перетаскивает только один deviceId повторно (несколько drag одного устройства — эта часть AC2
покрыта, см. noopKeepsRedo/повторные drag() вызовы); межпространственный otherDevice в конце
файла (строки 246-274) не перетаскивается по-настоящему, а получает команду через
c._devicePositionHistory.push(...) вручную, в пустой на тот момент стек — ни разу оба устройства
не оказываются в стеке одновременно, чтобы проверить порядок отмены между ними.
Решение ревьюера: снимаю как Low без возврата в цикл. LIFO-механика — свойство самого
CommandStack (src/command-stack.ts, не тронут этим диффом), уже доказано для разнородных
пейлоадов существующим test/command-stack.test.mjs («undo and redo preserve names…», строки
14-25: два разных именованных пуша, undo() возвращает последний). Каждая DevicePositionCommand
самодостаточна (deviceId/spaceId хранятся внутри команды, не выводятся из внешнего состояния на
момент undo) — проверено чтением _pointerUp/_runDevicePositionHistory
(src/houseplan-card.ts:6617-6628, :5145-5177): архитектурно перепутать устройства при
чередующемся undo/redo невозможно, не только «пока тесты не поймали». Автору стоит перед следующим
изменением в этой области добавить реальный двух-device drag-сценарий в смок, но отдельного цикла
это не требует.
Low-2 — AC13 «доступные имена на поддерживаемых ширинах» проверены на одной ширине/теме
Golden-сценарий geometry-devices-editor-dark (demo/golden/matrix.mjs:381) — единственный кадр
для нового toolbar, viewport: 1180×900, тема dark; DOM-проверка
(demo/smoke_editor_tabs.mjs:158-165, out.deviceHistoryControlsArePersistent) подтверждает
наличие кнопок, доступные имена (title/aria-label) и исходное disabled-состояние, но не на
нескольких ширинах. AC13 говорит «на поддерживаемых ширинах» во множественном числе.
Решение ревьюера: снимаю как Low. Device editor — desktop-first по docs/TOUCH-SUPPORT.md,
«поддерживаемые ширины» здесь исторически означают диапазон десктопных окон, а не мобильные
брейкпоинты (сравнимые персистентные кнопки — barclose — тоже проверяются на одной ширине в
существующей практике проекта). Кнопки — фиксированного .btn.ghost размера, идентичного другим
icon-only элементам панели, реальный риск переполнения на разумных десктопных ширинах низкий.
Не блокирует.
Что проверено и признано корректным
- AC1 (один drag — одна команда/запись):
smoke_device_position_history—previewDoesNotPersist,oneDragOneWrite,oneDragOneCommand— всеtrue; 10pointermoveмежду down/up не создают запись — проверено и чтением (_previewDevicePlacementне трогает_dirtyPos/_persistLayout,src/houseplan-card.ts:5076-5079). - AC2 (exact before/after, LIFO): проверено чтением + существующим
command-stack.test.mjs(см. Low-1) +undoRestoresExactStart/redoRestoresExactEnd/keyboardUndoWorks/keyboardRedoWorksв смоке — всеtrue. - AC3 (auto→manual→Undo=delete, Redo=update):
autoUndoDeletesExplicitPlacement,autoRedoRestoresPlacement—true, включая проверкуwrites[...]type === 'delete'/'update'. - AC4 (no-op не создаёт команду/не чистит Redo):
noopKeepsRedo—true; чтением подтверждено в_pointerUp(src/houseplan-card.ts:6607):sameDevicePlacement(after, drag.start)возвращает раньше_persistDevicePlacement/push. - AC5 (cancel/lost capture/Escape/mode switch/disconnect/second pointer → 0 записей): все шесть
сценариев —
escapeAbortsWithoutWrite,modeSwitchAbortsWithoutWrite,cancelRestoresWithoutWrite,lostCaptureRestoresWithoutWrite,secondPointerAbortsWithoutWrite—true; disconnect —disconnectedCallbackвызывает_cancelDeviceDrag()(src/houseplan-card.ts:2672, проверено чтением, не исполнением — юнит-тест наdisconnectedCallbackв проекте отсутствует, что ожидаемо для lifecycle-хука). Мутационный тест (device-position-cancel-routed-to-commit) независимо доказал, чтоsmoke_device_position_history.mjsобязан упасть, если pointercancel направить в commit-путь — «тест умеет падать» подтверждено экспериментом, не предположением. - AC6 (Undo во время drag только abort):
undoDuringDragOnlyAborts—true; чтением —_runDevicePositionHistoryначинается сif (this._cancelDeviceDrag() || ...) return;(src/houseplan-card.ts:5145). - AC7 (persist failure → rollback + направление stack):
failedDragRollsBack,failedUndoRestoresStackDirection—true. Чтением подтверждена тонкость восстановления направления: на неудачном Undo вызывается.redo()(а не повторный.undo()), что верно снимает побочный эффект предварительного.undo()со стека (src/houseplan-card.ts:5162-5165) — без прогона смока эта инверсия легко читается неверно, поэтому проверена и по коду, и по фактическому результатуcanUndo/canRedo. - AC8 (
k: 0/unknown-поля/room-label):test/device-position-history.test.mjs— 4 юнит-теста, включая явную проверку иммутабельности входа (assert.notEqual) и сохраненияk: 0(falsy, специально проверяется отдельно от truthyk) — тест умеет падать:deepEqualоткажет при потере поля,notEqualоткажет при мутации на месте.rl_*использует отдельный_savePos-путь через_labelMove(src/houseplan-editor-runtime.ts:10741-10759) — не пересекается с_devicePositionHistory, проверено чтением (единственный вызывающий_savePosтеперь — только label-путь, устройства используют новый_devicePlacementForCanvas/applyDevicePlacement). - AC9 (кнопки/shortcuts только в Device editor, native field не перехватывается):
nativeInputHistoryNotIntercepted,qwertzCtrlZIsUndo/qwertzCtrlYIsRedo(существующие вsmoke_editor_tabs),deviceHistoryControlsArePersistent— всеtrue; чтением подтверждено, что_mode === 'devices'ветка в_onKey— новая и не существовала наdev(сравнение сorigin/dev:src/houseplan-card.ts— раньше devices проваливался в общийif (!this._markup) return), регресс в decor/plan-ветки исключён (они выше поifи не задеты диффом). - AC10 (own echo/reconnect сохраняют, remote content чистит):
sameContentReloadKeepsHistory,remoteContentClearsHistory—true; чтением —_reloadLayoutOnlyтеперь сравниваетcontentFingerprintвсегда против текущегоthis._layout(убран фолбэк на потенциально устаревший кэш,src/houseplan-card.ts:4801-4809) — усиление, не ослабление контракта. - AC11 (кросс-space команда переключает пространство):
otherSpaceUndoIsVisible—true. - AC12 (deleted/rebound/disabled device — fail-closed):
deletedDeviceCommandFailsClosed,reboundDeviceCommandFailsClosed,disabledDeviceCommandFailsClosed— всеtrue;_devicePositionStateValidпроверяет существование,space,ha_disabledи наличие space-модели перед каждым undo/redo (src/houseplan-card.ts:5136-5142). - AC14 (geometry/decor history, pan/zoom, layout sync не регрессируют): целевые
smoke_pan_any_zoom,smoke_grid_snap,smoke_editor_tabs,smoke_layout_sync— зелёные;_geometryHistory/decor-стеки не входят в diff (git diff --statне содержит изменений вcommand-stack.ts, а_geometryHistoryвhouseplan-card.tsне тронут за пределами добавления соседнего поля) — проверено чтением, не исполнением полного regression-набора (несоразмерно задаче, см. §8). - Трейлеры и changelog:
bdf81fad(feat: add device position undo history) несётIssue: #74/User-Visible: yesи в одном коммите содержитdocs/CHANGELOG.md+docs/CHANGELOG.ru.md(по 6 строк, со ссылкой на #74) — оба сразу, как требует правило. Второй коммитfc63f3bf—User-Visible: no, трогает только два demo-смока, корректно.process-gate.mjs --issuesподтверждает диапазон формально чистым. - Backend/схема: не затронуты —
git diff --statне содержитcustom_components/**/*.py;houseplan/layout/update/deleteвызываются с той же сигнатурой, что и раньше (третий сайт-вызова в_persistDevicePlacement, инвариантcoordinate-write-barrier-guard.mjsобновлён на 3 и проходит). - Один источник числа: новый функционал не вводит ни одной новой пользовательски видимой
величины (позиция маркера не отображается как число где-либо ещё) — правило неприменимо,
test/single-source-numbers.test.mjsв общем прогонеnpm testзелёный без изменений. - i18n:
history.device_move/history.device_staleдобавлены во все 4 языка (en/ru/de/fr), плейсхолдер{name}согласован с существующим паттерномhistory.undo_named/redo_named.
Чего не проверял
- Полный browser-smoke набор (все ~207 сценариев) и полный HA backend harness — не прогонялись: несоразмерно диффу (PROCESS.md §8), backend не тронут вовсе.
golden:verifyполным набором — недоступен частично по дизайну (assertGoldenInvocationвdemo/golden/policy.mjsотказывает при--scenarioдля verify, только--mode=captureразрешает диагностику одного сценария); полный прогон — предрелizный гейт (см. Medium-1, где диагностика--mode=captureна два сценария уже вскрыла дефект без полного прогона).performance_smoke— не прогонялся; в AC не назван явно, спец §12 отмечает влияние на perf как «объективно снижает нагрузку», что не оспаривается кодом (persist больше не идёт на каждыйpointermove, только_previewDevicePlacement/локальный layout).- Прогон полного
mutation-gate.mjs(все ~сотни мутантов) — прогнаны точечно только два новых мутанта, относящихся к диффу (--id=); полный прогон — тяжёлый гейт вне рамок код-ревью этого масштаба. - Ручное тестирование в браузере (реальный HA, реальный
<ha-icon>) — не выполнялось; вывод про «в проде иконки отрисуются нормально» — по чтению кода демо-заглушки, не по факту в реальном Home Assistant. - Реальный факт прогона канонического
Docs screenshotsworkflow для обновлённыхdocs/images/*.png— не проверялся (нет ссылки на прогон в трейлерах,check-docs.mjsсверяет отпечаток источника, а не байтовую строгую принадлежность конкретному CI-раннеру); дефект Medium-1 воспроизводится независимо от того, каким Chromium снят кадр, поскольку причина — отсутствующие данные вicons.js, а не отличие рендерера.
Итог
Функциональная часть задачи выполнена основательно: все 14 AC доказаны либо зелёным,
демонстрируемо-падающим тестом (включая два прогнанных вживую мутационных теста,
подтвердивших, что smoke_device_position_history.mjs и regression-guard действительно ловят
регресс, а не просто существуют), либо разобраны по коду с точным указанием строк. Транзакционная
модель drag/preview/commit/abort, fail-closed инвалидация истории, кросс-пространственный undo и
сериализация записи выполнены точно по контракту ТЗ, вплоть до тонких деталей вроде направления
восстановления стека при неудачном Undo.
Единственная блокирующая находка — не в логике фичи, а в её собственном визуальном доказательстве:
опубликованный в этом же коммите скриншот docs/images/06-device-editor.png и golden-эталон
geometry-devices-editor-dark, оба созданные ради показа новых кнопок Undo/Redo, показывают их
пустыми из-за неактуального demo/srv/assets/icons.js. Фикс мелкий и механический
(node demo/gen_icons.mjs + пересъёмка/переприёмка двух артефактов), но обязателен в этой же
задаче, поскольку AC13 прямо ссылается на golden как доказательство, а опубликованная
документация не должна показывать несуществующие в кадре кнопки той самой фичи, которую иллюстрирует.
Вердикт: жёлтый. Возврат автору на устранение Medium-1; High не найдено, задача не пятый заход и бюджет циклов не исчерпан.