19 KiB
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-levelifфильтруетevent == 'workflow_dispatch'иhead_branchissue/*до checkout — нерелевантные прогоны (push, nightly наdev, PR) не тратят раннер вообще.concurrency: process-resume-<branch>без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.ymlpreflight обновлён третьим файлом (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— ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет. - Дерево материала:
84cbf14af04c3438b6c5ff3e588fc64c3eb5593fgit log --all --format='%H %T' | grep 84cbf14af04c - Тело issue:
6b7ad1bfe1fff446e1ab2c3d089af99b61d2286903f313d2ec0f764dbb6d446e - Вердикт конвейера:
green· High 0