# CODE-REVIEW-636-r1 ## Скоуп Issue #636 (инфраструктура, класс B, ускоренный вход по §1 — сразу `S7`, без ТЗ/спека). Единственный коммит `351fef43d65ffbc6eeceda7e744bfe142db2236d` поверх `dev`@`e29dfdeb`. Ни одного файла класса A — только `scripts/**`, `test/**`, `.github/workflows/**`, `PROCESS.md`, `AGENTS.md`. Задача: стадия `prepare` конвейера ревью (`process.yml`) синхронно ждала Validate с мутантами на материале до 45 минут (в среднем 28,1–28,7 мин на раунд при 10–12 минутах работы модели), занимая раннер вхолостую. Правка заменяет ожидание внутри job на событийное продолжение: `validate-gate.mjs --no-wait` диспатчит прогон, убеждается, что тот встал на материал, и выходит с кодом 2 (`pending`), не дожидаясь завершения; `process-resume.yml` (`workflow_run: completed` на `Проверка (CI)`) переставляет `S7-code-review`, когда раунд действительно ждал именно этот прогон; `process-reconcile.mjs` служит страховкой на потерянное событие. AC из тела issue: - AC1. Ни одна job конвейера не ждёт другой workflow дольше 3 мин (тест на отсутствие цикла ожидания/`gh run watch` в `process.yml`). - AC2. Время от `S7` до начала работы модели не растёт (замер на 5 задачах до/после). - AC3. Раунд, чей Validate завершился во время простоя reconcile, стартует не позднее следующего тика reconcile. Заход r1 — первый цикл, разбор полный. ## Как проверялось Материал — рабочая копия на `351fef43d65ffbc6eeceda7e744bfe142db2236d`, без `git fetch`/`checkout`. Дешёвые гейты (`tsc`, `npm test`, `npm run build` со сверкой бандла) не перегонялись: Validate на этом SHA зелёный — https://github.com/Matysh/houseplan-card/actions/runs/35824683286 — и по условиям задачи это покрывает и мутационный гейт (шесть mutant-jobs на ревью). `check-docs.mjs` не требовался: diff не трогает `src/**`. Инварианты модели, smoke, golden, `pytest tests_backend`, перф-профили — не требовались: diff не трогает геометрию, рендер, Python или чувствительные к перфу пути. Дополнительно, сверх минимума, сам прогнал точечно: - `node --test test/process-resume.test.mjs test/validate-gate.test.mjs test/process-reconcile.test.mjs test/mutation-gate.test.mjs test/review-doc-guard.test.mjs` → 158/158 pass — подтверждает, что новые и задетые тесты зелёные на этом материале, а не только по слову автора. - Вручную применил все 4 новых мутанта из `scripts/mutation-registry.mjs` (`gate-no-wait-still-sleeps`, `resume-wakes-round-without-marker`, `resume-ignores-active-run`, `reconcile-wakes-pending-while-validate-active`) по одному и прогнал заявленный `guard`-тест на каждом патче: все четыре красят соответствующий тест (1 упавший тест на патч), рабочая копия восстановлена (`git status --porcelain` пуст после). Мутационный гейт не просто существует — он действительно ловит снятую защиту. - Прочитал полный текст `scripts/validate-gate.mjs`, `scripts/process-resume.mjs`, `scripts/process-reconcile.mjs` (диффовые и смежные функции), новый `.github/workflows/process-resume.yml`, изменённые куски `process.yml` и `validate.yml` — целиком, не только по хankам диффа. ## AC · чем доказан · чем краснеет | AC | Доказательство | Проверка ревьюера | |---|---|---| | AC1 | `test/validate-gate.test.mjs` — 4 новых теста `#636`: идущий dispatch → `pending` без единого `sleep` (`ops.now()===0`); прогон, который ещё не появился, — гейт диспатчит и ждёт появления (≤3 мин по `VALIDATE_APPEAR_MS`), не завершения; завершённый зелёный/красный — как раньше, сразу. `test/review-doc-guard.test.mjs` проверяет текст `process.yml`/`process-resume.yml` (условие `proceed == 'false'`, `--no-wait`, контракт маркера) | Прогнал тесты (158/158), убил мутант `gate-no-wait-still-sleeps` (1/1). Прочитал `validateGate`: ветка `if (!wait)` стоит строго после проверки `run.status === 'completed'`, поэтому завершённый прогон и с `--no-wait` возвращается как раньше — подтверждено тестом «зелёный/красный принимается сразу» | | AC2 | Замер на 5 задачах после слияния — по конструкции задачи это возможно только постфактум, автор явно пометил это как «Чего не прогонял: живой конвейер (только на main после слияния)» | Не переисполнимо на этапе ревью — измерение требует живого прогона в проде после мержа и зеркалирования `process-resume.yml`/`process.yml` в `main`. **Проверено чтением, не исполнением**: событие `workflow_run` физически приходит в момент завершения Validate (тот же момент, когда раньше просыпался poll, ≤20 c реакции), поэтому по конструкции задержка не растёт. Формальное закрытие AC2 остаётся за первым живым раундом после мержа — это ожидаемо для AC такого типа, не находка | | AC3 | `test/process-reconcile.test.mjs` — «#636 pending marker»: `pendingValidate==='active'` → `wait`; `completed`/`missing` → `retry` (следующий тик reconcile переставляет `S7` сам); без маркера — прежний `escalate` | Прогнал тест, убил мутант `reconcile-wakes-pending-while-validate-active` (1/1). Прочитал `decideReconciliation`: новая ветка стоит перед general `run.conclusion === 'success' → escalate`, поэтому успешный прогон с маркером не путается со старым «успех без вердикта» | ## Находки Блокирующих (High) и находок Medium в скоупе — нет. Одно наблюдение, не блокирующее и не требующее отдельного issue: - **AC1 в общей формулировке шире, чем то, что фактически исправлено.** `integrate`-job того же `process.yml` (через `scripts/merge-candidate.mjs`, функция `waitValidate`) по-прежнему синхронно поллит `gh run list` до 45 минут, когда за время ревью `dev` успел уйти вперёд и кандидата нужно пересобрать и перепроверить. Это тоже «job конвейера, ждущая другой workflow дольше 3 мин», но раздел «Факты» issue называет только стадию `prepare` (28,1–28,7 мин на каждый раунд) и по аналогии — `nightly.yml` и `release-gate.mjs`; про `merge-candidate.mjs`/`integrate` там не сказано ни слова, и сам AC1 в скобках сужает предмет проверки до текста `process.yml` на отсутствие цикла именно там, где раньше был. Не считаю это находкой в скоупе: путь `integrate`-ожидания условный (срабатывает только когда `dev` двигался во время ревью, а не на каждом раунде), для него уже заведён отдельный бюджет 55 минут (#551) заранее, под него, и диффом не тронут — то есть не регрессия. Если владелец сочтёт нужным закрыть и этот случай — это отдельная задача, не расширение текущего скоупа. - Аналогично не тронуты `nightly.yml` (`gh run watch`, 3–25 мин) и `release-gate.mjs` (ожидание `performance.yml` до 60 мин), хотя раздел «Правка» issue упоминает их как аналогичные случаи. AC1/AC2/AC3 их не называют, тест AC1 явно ограничен `process.yml` — вне скоупа этой задачи. ## Что проверено и корректно - `validateGate({ wait: false })`: ветвление корректно — сначала проверяется `run.status === 'completed'` (зелёный/красный возвращаются немедленно независимо от `wait`), и только для незавершённого прогона `wait: false` выходит в `pending` без `sleep`. Появление dispatch (если прогона ещё нет) по-прежнему ограничено `VALIDATE_APPEAR_MS` (3 мин, до двух попыток при гонке ref/SHA, #539) — не новая ветка, поведение унаследовано. - `process.yml`: `set +e / code=$? / set -e` корректно перехватывает код выхода 2 до строгого режима; `case` покрывает все три исхода; шаг «Validate идёт» и «Сохранить маркер ожидания» гейтятся именно на `proceed == 'pending'`; шаг «Validate красный» теперь — точно на `proceed == 'false'` (было `!= 'true'`, что раньше ошибочно включало бы и `pending`). Все шаги, зависящие от зелёного гейта («Зелёные гейты на этом SHA», сбор `prepared.json`, публикация артефакта модели), по-прежнему условие `== 'true'`, поэтому `pending` их не задевает — не нужно было трогать эти строки, и их не тронули. - `integrate`-job: явная короткая ветка `PROCEED = "pending"` — печатает причину, `proceed=false`, `exit 0`, не долетает до `failure()`-уведомления владельца (это не авария, а штатный промежуточный статус). - `process-resume.yml`: триггер `workflow_run` на точное имя workflow (`"Проверка (CI)"`, сверено с `validate.yml`); job-level `if` фильтрует `event == 'workflow_dispatch'` и `head_branch` `issue/*` до checkout — нерелевантные прогоны (push, nightly на `dev`, PR) не тратят раннер вообще. `concurrency: process-resume-` без `cancel-in-progress` сериализует повторные события на одной ветке. Метка переставляется `HP_PROCESS_TOKEN`, не `GITHUB_TOKEN` — иначе событие `labeled` не запустило бы `process.yml` (тот же инвариант, что и у остального конвейера). Код берётся из `dev`, как у `process-reconcile.yml`. - `decideResume`: проверил все ветви руками — не-`workflow_dispatch`, незавершённый прогон, чужой `headSha`, отсутствие `S7`, `blocked`/`review-4`, активный процесс-прогон (в т.ч. НЕ только последний, а любой активный — это на случай гонки со свежим раундом), отсутствие маркера, маркер на другом SHA, устаревший/проигравший маркер (сортировка по `createdAt`+`id`, решает только новейший прогон) — все корректно ведут к `noop`, и только полное совпадение — к `resume`. Специально проверил сценарий «Validate дождался сам merge-candidate во время `integrate`»: в этот момент процесс-прогон, из которого запущен `integrate`, ещё `in_progress` → `mine.some(active)` → `noop`; ложного резюме на постороннем dispatch того же branch не будет. - `process-reconcile.mjs`: `pending`/`pendingValidate` гидратируются только для `run.status === 'completed'` (не тратит вызовы API на активные прогоны); новая ветка `run.conclusion === 'success' && run.pending` стоит раньше общей `success → escalate`, поэтому не путает «ждём Validate» с «успех без вердикта»; `validateStateOnMaterial` фильтрует только `workflow_dispatch` и предпочитает `active` над `completed`, что верно (пока хоть один активен — ждать). `loadSealedArtifact` — рефакторинг прежнего `loadPreparedArtifact` без потери проверки чек-суммы; используется одинаково для `prepared.json` и `pending.json`. - Дедупликация меток: `retry` в reconcile для pending-случая проходит через тот же `alreadyReported`/`reconciliationKey`, что и остальные ветки — разные `reason` для `active`/`completed`/`missing` дают разные ключи, повторный тик не долбит комментариями бесконечно на неизменном состоянии (комментарий появляется один раз на пару issue+run+reason). - `actions/upload-artifact@ea165f8d...# v4` — тот же закреплённый SHA, что уже используется в трёх других местах `process.yml` и в `process-reconcile.yml`; не новый непроверенный pin. - `validate.yml` preflight обновлён третьим файлом (`process-resume.yml`) в списке сверки `main`↔`dev` — без этого новый workflow-файл разошёлся бы молча между ветками, как когда-то `process.yml`/`mutation-gate.yml` (#472). - Трейлеры коммита: `Issue: #636`, `User-Visible: no` — верно, видимого пользователю поведения нет; changelog не требуется и не тронут. - `PROCESS.md`/`AGENTS.md` обновлены в том же коммите, описывают именно то поведение, что в коде (сверил формулировки построчно с реализацией). ## Чего не проверял - **AC2 не переисполним на этапе ревью** — требует замера на 5 живых задачах после мержа и зеркалирования `process-resume.yml` в `main` (сам автор назвал это в разделе «После слияния — владельцу»). Принято по чтению конструкции (`workflow_run` срабатывает синхронно с завершением Validate), формальная приёмка — задача первого живого раунда после мержа, не этого ревью. - Не гонял `npx tsc --noEmit` и полный `npm test`/`npm run build` отдельно — Validate на `351fef43` зелёный, эти гейты и mutant-jobs в нём уже исполнены; перегонять секунды не добавили бы уверенности сверх точечного прогона (158/158) и ручной проверки 4 мутантов, который сделал сам. - Не проверял `demo/smoke_*.mjs`, `golden:verify`, `pytest tests_backend`, `model-invariants` — diff не трогает `src/**`, рендер, Python или геометрию; неприменимо по составу diff. - Не проверял живое поведение `gh run download`/`gh api` в реальном GitHub Actions окружении (сетевые вызовы `realOps`/`processRuns`/обёртки в `resume()`) — это тонкие обёртки без собственной логики, покрытые fixture-тестами; живая проверка возможна только в реальном прогоне (тот же пробел, что у AC2). ## Материал раунда ``` material: 351fef43d65ffbc6eeceda7e744bfe142db2236d ``` ## Вердикт **Зелёный.** Все три AC доказаны (AC1, AC3 — автотестом с проверенной способностью падать; AC2 — чтением конструкции с честной пометкой «формальная приёмка после мержа»). High и Medium-в-скоупе находок нет. Единственное наблюдение (частичное покрытие общей формулировки AC1 — `integrate`/`merge-candidate.mjs`, `nightly.yml`, `release-gate.mjs`) вне скоупа этой задачи по тексту issue и не является регрессией — оставлено как информация владельцу, отдельный issue не завожу. --- ## Материал раунда - Ветка: `issue/636-review-wait-event`, коммит `351fef43d65f` — ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет. - Дерево материала: `84cbf14af04c3438b6c5ff3e588fc64c3eb5593f` ``` git log --all --format='%H %T' | grep 84cbf14af04c ``` - Тело issue: `6b7ad1bfe1fff446e1ab2c3d089af99b61d2286903f313d2ec0f764dbb6d446e` - Вердикт конвейера: `green` · High 0