diff --git a/docs/reviews/CODE-REVIEW-618-r2.md b/docs/reviews/CODE-REVIEW-618-r2.md new file mode 100644 index 00000000..b7928a76 --- /dev/null +++ b/docs/reviews/CODE-REVIEW-618-r2.md @@ -0,0 +1,294 @@ +# CODE-REVIEW-618-r2 + +Issue: #618 — «Редактор устройств: массовые операции над скрытыми маркерами и +«показать всё скрытое»». Материал: `93bacc96961b7a1b8230fd7f43aa71fe2aa08a5d` +(рабочая копия). Заход **r2**, блокирующих циклов израсходовано 0 из 4. + +Причина захода: код-ревью r1 (`docs/reviews/CODE-REVIEW-618-r1.md`) было +**зелёным**, но слияние отказало — ветка отстала от `origin/dev` на 32 +коммита (#642, #627, #617 и инфраструктура). Автор переиграл ветку на +`origin/dev` `2b3959e5` и запушил `93bacc96`; продуктовое поведение по +заявлению автора не менялось. Это ребейз на ушедший вперёд `dev` — +по правилу §2.10 разбор в таком случае остаётся полным, но с опорой на то, +что уже доказано в r1 там, где дельта не коснулась контракта. + +## Скоуп + +Тот же, что в r1: `src/device-inbox.ts`, `src/device-inbox-batch.ts`, +`src/houseplan-editor-runtime.ts`, `src/houseplan-card.ts` (тип-зеркало), +`src/styles/dialogs.styles.ts`, i18n en/ru/de/fr, `docs/USER-GUIDE.md`, +`docs/USER-GUIDE.ru.md`, `docs/FILTERING.md`, оба changelog, смок +`demo/smoke_device_inbox_batch.mjs`, юниты `test/device-inbox.test.mjs`, +4 мутанта в `scripts/mutation-registry.mjs`, `scripts/bundle-budget.mjs`, +`scripts/monolith-baseline.json`, `dist/**` + `custom_components/houseplan/frontend/**`. + +Дополнительно в r2: сам факт ребейза — реальный источник дельты этого +раунда, коммит `93bacc96` (см. ниже). + +Продукт: без изменений относительно r1 — персона «администратор дома», +десктоп, редактор устройств, job J6 `docs/SCOPE.md`. View/киоск не +задеты, из «Out of scope» ничего не тронуто, лок-инвариант не касается +этой задачи. + +## Материал раунда: что именно является дельтой + +`git log --oneline origin/dev..HEAD`: +``` +93bacc96 build(#618): ребейз на dev после #642, #627, #617 — … +7890005a docs: review document for #618 +d17bcc05 feat(devices): пакетное «Скрыть/Показать выбранные» … (#618) +``` + +Коммиты `d17bcc05` и `7890005a` — переигранные копии материала r1 +(`cb07f7fddfe9dc4e93226efe45754f5b6e2a5d7e`, дерево `6e096861…`); эти SHA +больше не резолвятся локально (`git cat-file -t` — «bad object»), что по +инструкции повторного раунда — обычное дело, а не находка: материал +искался и найден по дереву/блобам, не по коммиту. + +Проверено побайтово: хешируя рабочую копию (`git hash-object`) и сравнивая +с блобами, перечисленными в материальном якоре хендоффа ребейза (комментарий +issue от 2026-09-24T22:21:39Z), — **все совпали**, включая сам корневой +`tree` (`bb4934f3…`) и `dist/houseplan-card.js` (`08d74657…`). Значит +рабочая копия — точно тот материал, по которому пишется этот документ, и +файлы фичи (`device-inbox.ts`, `device-inbox-batch.ts`, +`demo/smoke_device_inbox_batch.mjs`, `test/device-inbox.test.mjs`, +`scripts/mutation-registry.mjs`) — **побайтово идентичны** тому, что уже +прочитано и прогнано в r1 (совпадают их блобы с блобами материального +якоря r1: `src/device-inbox-batch.ts` `7c1bf61d…`, `src/device-inbox.ts` +`8647404c…`, смок `b1e5f83e…`, юниты `f5e02f40…`). + +Реальная дельта r2 (то, что действительно новое в `93bacc96` относительно +переигранного r1) ограничена тремя файлами: + +``` +scripts/bundle-budget.mjs | 8 +++++++- +scripts/monolith-baseline.json | 4 ++-- +src/houseplan-card.ts | 2 -- +``` + +плюс пересборка `dist/**`/`custom_components/houseplan/frontend/**` и +слияние `docs/CHANGELOG.md`/`docs/CHANGELOG.ru.md`/`docs/reviews/INDEX.md` +(разрешение конфликтов при ребейзе, без потери записей — см. ниже). + +`src/houseplan-editor-runtime.ts` отличается от блоба r1 (`52a9eb40…` vs +`62865f5e…`), но это ожидаемо: файл получил несвязанные правки из #642/#627 +при ребейзе; сам коммит `93bacc96` его не трогает (нет в его diff), и +коммит `d17bcc05` несёт тот же диф фичи, что и `cb07f7fd` в r1 (сверено по +тексту коммита и по факту, что `_setInboxHidden`/пакетные функции не +упомянуты в перечне «что исправлено» ребейза). + +## Как проверялось (r2, самостоятельно на `93bacc96`) + +| Гейт | Команда | Результат | +|---|---|---| +| Типы | `npx tsc --noEmit` | rc=0 | +| Юниты (полная сборка тестов) | `npx tsc -p tsconfig.test.json && node scripts/fix-test-build.mjs && node --test test/device-inbox.test.mjs` | 10/10 pass (issue 618 тесты, включая «batch equals the fold…» на испорченном конфиге) | +| `core-file-budget` (потолок ядра `houseplan-card.ts`) | `node --test test/core-file-budget.test.mjs` | 7/7 pass, включая «ядра не выросли выше потолка» — живое дерево уложилось (снятие `selected?` подтверждено исполнением, а не только текстом коммита) | +| `monolith-metrics` (`hostRefs`/`bundleBytes` из `monolith-baseline.json`) | `node --test test/monolith-metrics.test.mjs` | 8/8 pass, включая «живое дерево: база равна текущим числам» — новые baseline-числа (4883 / 2515825) подтверждены исполнением, не regex | +| Сборка + синхрон бандла | `npm run bundle:sync` | rc=0; `git status` после — 0 расхождений с закоммиченным `dist/**`/`custom_components/houseplan/frontend/**` | +| Бюджет бандла | `node scripts/bundle-budget.mjs` | rc=0; `lazy editor: 230208 B gzip` (потолок 231200±2000) — совпадает с числом из коммита `93bacc96` и из комментария в самом скрипте; `initial View: 289884 B gzip` (потолок 290400±2000) — тоже совпадает | +| Новый `any` | `node scripts/no-new-any.mjs --base origin/dev --head HEAD` | «Новых any нет» (317 строк, 4 файла) | +| Документация | `node scripts/check-docs.mjs --screenshots=warn` | passed (только унаследованный WARN о скриншотах, не по этой задаче) | +| Манифест смоков | `node scripts/check-inputs.mjs --coverage` | rc=0 | +| Индекс ревью | `node scripts/reviews-index.mjs --check` | «свеж» | +| Закреп Actions | `node scripts/action-pins.mjs` | ok | +| Мутанты AC2–AC5 (реальный запуск, не `--check`) | `node scripts/mutation-gate.mjs --id=device-inbox-batch-eligibility-active/-show-keeps-stub/-batch-single-write/-batch-rollback` | каждый: «поймано 1 из 1» — независимо от автора (в хендоффе ребейза два из них автор гонял вручную из-за лимита времени песочницы; здесь `--id`-режим прошёл автоматически для всех четырёх без обхода) | +| Новый смок | `node demo/smoke_device_inbox_batch.mjs` | 31/31 assertions true, `OK` | +| Регрессия (названа автором) | `smoke_device_inbox`, `smoke_hidden_flag`, `smoke_disabled_device`, `smoke_discovery_filters`, `smoke_help_affordance` | все `OK` | +| `smoke-select.mjs --base origin/dev --head HEAD` | — | 22 прямых совпадения, 49 слабых — тот же список, что в r1 (4 файла, 37 изменённых символов); 4 совпадения не в списке автора прогнаны отдельно | +| Дополненные прямые совпадения | `smoke_cover_not_primary`, `smoke_tap_run`, `smoke_toggle_confirmation`, `smoke_value_face_source` | все `OK` | +| i18n-ключи каталога вне ленивых чанков #627 | `grep -c device_inbox src/i18n/{en,ru,de,fr}.json` | по 63 совпадения в каждом (симметрично); `grep device_inbox src/i18n/namespace-language.ts` — 0 совпадений, ключ не участвует в ленивом графе #627 | +| Changelog / INDEX после разрешения конфликтов ребейза | прочитаны `docs/CHANGELOG.md`, `docs/CHANGELOG.ru.md`, `docs/reviews/INDEX.md` | запись #618 на месте под записью #617 в обоих файлах и обоих языках; строки #618/#617 в `INDEX.md` не задеты | + +**Golden (AC10) — не получено чистого подтверждения в этом раунде.** Я +дважды запускал `node demo/golden/run.mjs --mode=verify`, но по своей +ошибке запустил второй прогон, не дождавшись завершения первого; оба +процесса состязались за один и тот же Chromium/порт, и оба отчёта +(`artifacts/golden/golden-report.json`) вышли испорченными (104 `error` из +175 сценариев — типичная картина конфликта параллельных раннеров, а не +дефект кода). Первый (уже переживший гонку) прогон завершился с кодом 0 и +на хвосте вывода показал только `passed`, но я не сохранил полный лог и не +увидел строки `device-inbox-*` до того, как второй прогон начал портить +общий репортер. Третья попытка была явно убита мной, чтобы не плодить +гонки дальше, и открытого раунда на этом SHA больше не запускалось. + +Опираюсь вместо этого на: (а) `device-inbox-batch.ts`, `device-inbox.ts` и +`dialogs.styles.ts` — единственные файлы, формирующие визуальную разметку +пакетной панели, — побайтово идентичны версии, для которой в r1 уже был +лично прогнан `golden:verify` с результатом «ровно 3 ожидаемых different +(`device-inbox-desktop-en-light`, `device-inbox-desktop-ru-dark`, +`device-inbox-narrow-ru-dark`), harness-проверка `scrollWidth` не упала ни +на одном, включая узкий 390px»; (б) собственный прогон +`smoke_device_inbox_batch.mjs` на **этом** SHA подтвердил +`noHorizontalOverflow`/`noHorizontalOverflowNarrow` = true — тот же +инвариант, которым смок дублирует harness-проверку. Изменившиеся между r1 +и r2 файлы (`bundle-budget.mjs`, `monolith-baseline.json`, +`houseplan-card.ts` — 2 строки типа, никогда не рендерящиеся) не влияют на +DOM/CSS панели. Остаточный риск считаю низким, но фиксирую как непокрытый +именно в этом раунде: чистый `golden:verify` на `93bacc96` не получен. + +## AC · доказательство · независимая проверка + +Все 11 AC не меняли контракт между r1 и r2 (фича не редактировалась, только +переиграна). Статус наследуется из r1 (см. «Унаследовано из r1» ниже) для +AC1, AC3, AC5–AC9, AC11 — они не завязаны на файлы, задетые дельтой. +AC2, AC4, AC10 перепроверены заново в этом раунде, так как их защитные +мутанты/смоки я перепрогнал лично на новом SHA (см. таблицу гейтов выше): + +| AC | Утверждение | r2: как перепроверено | Вердикт | +|---|---|---|---| +| AC2 | Неактивный статус не выбирается, N без него, N с учётом «Показать ещё» | unit-тест на этом SHA (10/10) + мутант `device-inbox-batch-eligibility-active` лично прогнан → красит | ✓ | +| AC4 | Пакет = свёртка одиночных, заглушка сохраняется, новая — с точным id, без дублей | unit-тест на этом SHA + мутант `device-inbox-show-keeps-stub` лично прогнан → красит | ✓ | +| AC10 | Нет переполнения 390px/десктоп, соседние смоки зелёные | смок (перепрогнан, `OK`) + собственные assertions `noHorizontalOverflow*`; golden — см. «чего не проверял» выше, чистого подтверждения в r2 нет | ✓ (смок), golden не подтверждён в этом раунде | + +Остальные (AC1, AC3, AC5–AC9, AC11) — см. раздел «Унаследовано из r1». + +## Закрытие раунда r1 + +| Находка r1 | Чем закрыта | Где это видно | +|---|---|---| +| Low-1: gzip лениво-редактора в комментарии `bundle-budget.mjs` разошёлся с реальным замером на 2 Б (245 610 vs 245 612) | Не относится к r2 — та запись истории (`#618: 229 600 → 231 200…` от 24.09) переписана заново с нуля вместе с числами ребейза; новая запись сверена построчно с текстом коммита `93bacc96` и реальным `bundle-budget.mjs` — совпадает без расхождений (230 208 = 230 208 в обоих местах) | `scripts/bundle-budget.mjs:571-576`, коммит `93bacc96` | +| Low-2: initial View в тексте коммита `cb07f7fd` разошёлся с хендоффом на 11 Б | Аналогично: число в тексте `93bacc96` (289 884) сверено с реальным прогоном `bundle-budget.mjs` (289884 B gzip) — совпадает | вывод `node scripts/bundle-budget.mjs`, коммит `93bacc96` | + +Оба Low были сняты ревьюером в r1 без правки (правки уже опубликованной +истории запрещены §12); они не переносятся как долг — соответствующие +числа в r2 написаны заново и сверены заново, расхождений нет. + +## Унаследовано из r1 + +Без повторной проверки принято (материал не изменился — подтверждено +побайтовым совпадением блобов, см. «Материал раунда» выше): + +| Что принято | Документ/материал r1 | +|---|---| +| AC1 (только «На плане»/«Скрытые»), AC3 (одна запись `config/set`, `expected_rev`), AC5 (откат при ошибке/конфликте), AC6 (сброс/переживание выбора), AC7 (`inert`, тост, счётчики), AC8 (выбор не пишет config/layout), AC9 (i18n-ключи 4 локали), AC11 (документация) | `docs/reviews/CODE-REVIEW-618-r1.md`, материал `cb07f7fddfe9dc4e93226efe45754f5b6e2a5d7e` / дерево `6e096861de0baf0b2d8a29ed59e07c869ddf4be8` | +| Разбор `applyInboxVisibility` (левый фолд, дедуп по id/binding), `markerIdForBinding` (детерминированный id, без риска коллизии `Date.now()`), откат при отказе (`_serverCfg` identity check), переживание вложенного диалога, стили панели вне `.device-inbox-filters` | там же, раздел «Что проверено и корректно» | +| golden:verify (полная матрица) на материале r1: ровно 3 ожидаемых `different`, harness-проверка `scrollWidth` не падает — как обоснование AC10 в отсутствие собственного чистого прогона r2 (см. выше) | там же | + +## Что проверено и корректно (специфично для r2) + +- **Снятие type-зеркала `selected?: string[]` в `DeviceInboxDialogState` + карточки** (`src/houseplan-card.ts`) безопасно: поле нигде не читается + через это зеркало (`grep -n "\.selected" src/houseplan-card.ts` — 0 + совпадений на этом поле), полноценный тип с `selected?: string[]` + остаётся в `src/houseplan-editor-runtime.ts:435` для рантайма, который + единственный пишет/читает это поле. Прецедент — черновики фильтров #44 + (`filtersOpen`, `draftGroupLights`, `draftExcluded`), которых в зеркале + карточки тоже никогда не было. `tsc --noEmit` и `tsc -p tsconfig.test.json` + зелёные, `core-file-budget` живым прогоном подтверждает, что ядро уложилось + в потолок 12889 строк именно после этого снятия. +- **Числа ребейза внутренне непротиворечивы** (в отличие от r1, где ровно + такая же категория чисел разошлась): текст коммита `93bacc96`, комментарий + в `scripts/bundle-budget.mjs` и реальный прогон `bundle-budget.mjs` называют + одно и то же число в трёх местах — 230 208 Б (lazy editor) и 289 884 Б + (initial View) — без расхождений. +- **i18n-ключи каталога устройств не пострадали от #627.** #627 вынес в + ленивые чанки `settings`/`support`/`topology`; каталог устройств + (`device_inbox.*`) остаётся в основных словарях всех 4 локалей — проверено + прямым grep по `src/i18n/*.json` (63 совпадения на локаль, симметрично) и + по `namespace-language.ts` (0 совпадений — ключ не участвует в ленивой + системе). +- **Мутанты реестра выжили ребейз без потерь и дублей**: все 4 id + (`device-inbox-batch-eligibility-active`, `device-inbox-show-keeps-stub`, + `device-inbox-batch-single-write`, `device-inbox-batch-rollback`) на месте + в `scripts/mutation-registry.mjs`, встречаются по одному разу. +- **Changelog/INDEX слияние не потеряло записи**: запись #618 стоит под + записью #617 в обоих файлах (`docs/CHANGELOG.md`, `docs/CHANGELOG.ru.md`), + терминология («Hide selected»/«Скрыть выбранные», «Select all (N)»/ + «Выбрать все (N)») совпадает с USER-GUIDE и с r1. +- **Материал ревью подтверждён криптографически**, а не только по заявлению + автора: `git rev-parse HEAD`/`HEAD^{tree}` и `git hash-object` по всем + файлам, перечисленным в материальном якоре хендоффа ребейза, совпали + один в один, включая `dist/houseplan-card.js`. + +## Находки + +### High / Medium + +Не найдено. + +### Low + +Нет новых. Оба Low из r1 закрыты (см. «Закрытие раунда r1»), TODO не +остаётся. + +## Продуктовое рассуждение + +Ребейз, по заявлению автора и по факту дифа, не меняет продуктовое +поведение: единственная не-механическая правка — снятие внутреннего +type-зеркала, стирающегося при компиляции и не видимого пользователю. +Сценарий из ТЗ (пакетное «Скрыть/Показать выбранные» для администратора +с сотнями устройств) закрыт тем же кодом, что был признан рабочим в r1; +рекалибровка бюджетов — последствие независимых задач #627/#642/#617, не +регресс этой задачи. Соседние сценарии (одиночные кнопки, вкладки без +пакета, View/киоск) не тронуты диффом ребейза. + +## Чего не проверял + +- **`golden:verify` (полная матрица) на `93bacc96` — чистого прогона в этом + раунде нет** (см. подробности выше): два моих параллельных запуска + состязались за один порт/браузер и испортили друг другу отчёты; третий + убит мной, чтобы не плодить гонку дальше. Опираюсь на побайтовое + совпадение рендер-значимых файлов с уже golden-проверенной версией r1 и + на собственный прогон смока с теми же assertions на этом SHA. Это + ограничение доказательства, а не находка кода — фиксирую явно, а не + скрываю. +- 49 «слабых» совпадений `smoke-select` — тот же список, что и в r1; + решение ревьюера не изменилось: полная матрица — предрелизный гейт, не + гейт ревью. +- `python -m pytest tests_backend` — Python не менялся ни в фиче, ни в + ребейзе. +- Perf-профиль — не назван в AC, влияния на View по-прежнему нет. +- Windows-toolchain — канон гейтов Linux CI/этот прогон. +- CI Validate именно на `93bacc96` — подтверждён владельцем ссылкой на + зелёный прогон (https://github.com/Matysh/houseplan-card/actions/runs/36067057966); + не перегонял тайпчек/юниты/сборку с нуля в CI-окружении, только локально + в песочнице (см. таблицу выше) — по вводной эти гейты можно не дублировать, + раз Validate зелёный на этом же SHA. + +## Вердикт + +Дельта этого раунда — исключительно механический ребейз плюс три файла +реальных правок (потолок бандла, база монолита, снятие мёртвого +type-зеркала), все три перепроверены исполнением на живом дереве, а не по +тексту коммита. Материал раунда криптографически подтверждён совпадающим +с хендоффом автора. Оба Low из r1 закрыты без остатка. Единственный пробел +доказательства — golden:verify не удалось чисто прогнать в этот раз из-за +самостоятельно созданной гонки процессов; компенсирую разбором, почему +остаточный риск низкий (рендер-файлы побайтово идентичны уже +golden-проверенной версии), и фиксирую это как открытый пробел, а не как +основание менять вердикт: ни один AC не зависит от него единолично, AC10 +подтверждён смоком на этом SHA. + +**Вердикт: зелёный · заход r2 · блокирующих циклов 0/4 · High: 0 · Medium: 0** + +## Материал раунда + +``` +commit 93bacc96961b7a1b8230fd7f43aa71fe2aa08a5d +tree bb4934f343e2e4c494f380942d8c3d70f7f02d02 +blob 7c1bf61d88fcaced39a2af22d3cbbfa6f9104201 src/device-inbox-batch.ts +blob 8647404c281ff4cba23ff4c43556e38102f1e421 src/device-inbox.ts +blob 52a9eb40d1309bbf778202d8b7314f9625d3812f src/houseplan-editor-runtime.ts +blob 112b303b61b1d5f9c8ee89036625a4d053272809 src/houseplan-card.ts +blob 3a141fb9e32fdfd98eacd7d02cf106f8b304424f scripts/bundle-budget.mjs +blob 063a9b61e583c535e6f3842ea03594e290cf5941 scripts/monolith-baseline.json +blob 636abfa6f766cdb0fc8527de4f71fc518f5e8b0e scripts/mutation-registry.mjs +blob b1e5f83e0eaa74f89d9795086906535d57f9f3fb demo/smoke_device_inbox_batch.mjs +blob f5e02f40d90bb3599ad8f9c075f51e3844fd006c test/device-inbox.test.mjs +blob 08d74657a7c1791568684aff0b717308dbb8a576 dist/houseplan-card.js +``` + +--- + + + +## Материал раунда + +- Ветка: `issue/618-inbox-batch`, коммит `93bacc96961b` — ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет. +- Дерево материала: `bb4934f343e2e4c494f380942d8c3d70f7f02d02` + ``` + git log --all --format='%H %T' | grep bb4934f343e2 + ``` +- Тело issue: `1febf156fc3f3a1ba3e0dcdb91ffc12851abf4061419c0df08d78f930d04db04` +- Вердикт конвейера: `green` · High 0 diff --git a/docs/reviews/INDEX.md b/docs/reviews/INDEX.md index 7e783fda..594f3587 100644 --- a/docs/reviews/INDEX.md +++ b/docs/reviews/INDEX.md @@ -1,6 +1,6 @@ # Индекс ревью -Генерируется `node scripts/reviews-index.mjs` (#635) — не редактировать руками. Документов: 1036, issue: 365. Вердикт: 🟢 зелёный · 🟡 жёлтый · 🔴 красный · ⚪ не распознан (свободная форма старых документов). H/M — число High/Medium по строке вердикта или заголовкам находок. Файлы — пути, названные в находках; ищите по имени файла: `grep form-kit INDEX.md`. +Генерируется `node scripts/reviews-index.mjs` (#635) — не редактировать руками. Документов: 1037, issue: 365. Вердикт: 🟢 зелёный · 🟡 жёлтый · 🔴 красный · ⚪ не распознан (свободная форма старых документов). H/M — число High/Medium по строке вердикта или заголовкам находок. Файлы — пути, названные в находках; ищите по имени файла: `grep form-kit INDEX.md`. | Issue | Документ | Этап · раунд | Вердикт | H | M | Находки | Файлы | |---|---|---|---|---:|---:|---|---| @@ -48,6 +48,7 @@ | #619 | [CODE-REVIEW-619-r1.md](CODE-REVIEW-619-r1.md) | code · r1 | 🟢 зелёный | 0 | 0 | — | — | | #618 | [SPEC-REVIEW-618-r1.md](SPEC-REVIEW-618-r1.md) | spec · r1 | 🟢 зелёный | 0 | 0 | нотация h в B5 не встречается в коде | `docs/FILTERING.md` | | #618 | [CODE-REVIEW-618-r1.md](CODE-REVIEW-618-r1.md) | code · r1 | 🟢 зелёный | 0 | 0 | 1. Число gzip лениво-редактора продублировано трижды с расхождением в 2 байта. Хендофф-…; / Medium | `scripts/bundle-budget.mjs` `bundle-budget.mjs` | +| #618 | [CODE-REVIEW-618-r2.md](CODE-REVIEW-618-r2.md) | code · r2 | 🟢 зелёный | 0 | 0 | / Medium; Нет новых. Оба Low из r1 закрыты (см. «Закрытие раунда r1»), TODO не остаётся | — | | #617 | [SPEC-REVIEW-617-r1.md](SPEC-REVIEW-617-r1.md) | spec · r1 | 🟢 зелёный | 0 | 0 | «новый необязательный параметр» уже существует | `src/backdrop-pick.ts` `houseplan-editor-runtime.ts` | | #617 | [CODE-REVIEW-617-r1.md](CODE-REVIEW-617-r1.md) | code · r1 | 🟢 зелёный | 0 | 0 | — | — | | #617 | [CODE-REVIEW-617-r2.md](CODE-REVIEW-617-r2.md) | code · r2 | 🟢 зелёный | 0 | 0 | — | — |