From 41325a852f378e5cb9cb7daa245b4b04afddddec Mon Sep 17 00:00:00 2001 From: Matysh Date: Thu, 20 Aug 2026 23:17:16 +0300 Subject: [PATCH] fix: spend the review budget on blocking verdicts only MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The pipeline punished what it prescribed: after a failed merge it tells the author to rebase and restore S7-code-review, and that attempt finished the budget. On #225 (light track, limit 2) the sequence yellow, green, rebase produced review-4 on a task whose code review was green and whose CI was green, with no product change after the verdict — the owner had to arbitrate work that was already accepted. A cycle under section 4 is a verdict with blocking findings followed by a return to the author, so only yellow and red verdicts spend the budget now. A green verdict returned nothing and consumes nothing, which also removes any need to mark rebase re-runs specially. Attempts and cycles are now separate quantities. The attempt number keeps naming the document, because two runs sharing a number would overwrite each other's review artefact, while the limit compares blocking cycles only. The exhaustion comment lists the verdicts it counted, and the guard no longer strips review-4 — it reports the recount and leaves the decision with the owner. Rule 7 of the process gate follows: its document threshold rises above the cycle limit, because legitimate attempts can exceed cycles and a threshold equal to the limit would refuse the very rebase the pipeline demands. Issue: #227 User-Visible: no --- .github/workflows/process.yml | 79 ++++++++++++++++++++++++++--------- 1 file changed, 60 insertions(+), 19 deletions(-) diff --git a/.github/workflows/process.yml b/.github/workflows/process.yml index a080a149..6a615679 100644 --- a/.github/workflows/process.yml +++ b/.github/workflows/process.yml @@ -39,6 +39,7 @@ jobs: outputs: stage: ${{ steps.decide.outputs.stage }} cycle: ${{ steps.decide.outputs.cycle }} + spent: ${{ steps.decide.outputs.spent }} limit: ${{ steps.decide.outputs.limit }} steps: - id: decide @@ -63,19 +64,41 @@ jobs: limit=4 if [ "$SMALL" = "true" ] || [ "$TRIVIAL" = "true" ]; then limit=2; fi - # Счётчик считает вердикты ТОЛЬКО своего этапа. Раньше он брал все - # подряд, и вердикт по ТЗ съедал цикл из бюджета код-ревью: на #89 - # первое код-ревью получило r2/4. На задаче с двумя циклами ТЗ второе - # код-ревью упиралось бы в review-4 после одной правки. + # Считаются ДВЕ РАЗНЫЕ величины, и это не педантизм (#227). # - # Этап опознаётся по имени документа в теле комментария. Если документа - # нет, вердикт не посчитается — недосчёт даёт лишний цикл, а перерасчёт - # остановил бы работу досрочно; из двух ошибок выбрана обратимая. - done_cycles=0 + # `attempt` — сколько раз ревью уже отработало на этом этапе. Он нужен + # только для имени документа и метки: два захода с одинаковым номером + # означают, что второй документ перезапишет первый и артефакт ревью + # исчезнет. + # + # `spent` — сколько циклов израсходовано из бюджета §4. Цикл — это + # «отправка на ревью → вердикт с блокирующими находками → возврат + # автору», поэтому бюджет тратят ТОЛЬКО жёлтые и красные вердикты. + # Зелёный ничего на правки не вернул и цикла не образует. + # + # Раньше обе роли исполнял один счётчик всех вердиктов, и конвейер + # наказывал за то, что предписывал сам: при неудавшемся слиянии он + # велит вернуть S7-code-review после ребейза, и этот заход добивал + # бюджет. На #225 (лёгкий трек, лимит 2) последовательность + # жёлтый → зелёный → ребейз дала review-4 на задаче с зелёным ревью и + # зелёным CI: работа встала, хотя после вердикта не было ни одной + # правки продуктового кода. + # + # Вердикты считаются ТОЛЬКО своего этапа: иначе вердикт по ТЗ съедал + # цикл из бюджета код-ревью (#89 получило r2/4). Этап опознаётся по + # имени документа в теле комментария; документа нет — вердикт не + # посчитается. Недосчёт даёт лишний заход, перерасчёт остановил бы + # работу досрочно: из двух ошибок выбрана обратимая. + attempt=1; spent=0; spent_list="" if [ -n "$stage" ]; then - done_cycles=$(gh issue view "$NUM" --repo "${{ github.repository }}" \ - --json comments \ - -q "[.comments[] | select(.body | test(\"Вердикт:\")) | select(.body | test(\"$marker\"))] | length") + comments=$(gh issue view "$NUM" --repo "${{ github.repository }}" --json comments) + of_stage="[.comments[] | select(.body | test(\"Вердикт:\")) | select(.body | test(\"$marker\"))]" + # Блокирующим считается вердикт, у которого в строке вердикта стоит + # «жёлтый» или «красный». Регистр и окружение слова не важны. + blocking="$of_stage | map(select(.body | test(\"Вердикт:[^\\n]*(жёлт|красн)\"; \"i\")))" + attempt=$(( $(printf '%s' "$comments" | jq -r "$of_stage | length") + 1 )) + spent=$(printf '%s' "$comments" | jq -r "$blocking | length") + spent_list=$(printf '%s' "$comments" | jq -r "$blocking | map(\"- \" + .url) | join(\"\\n\")") fi # Отказ обязан быть виден в issue, а не только в логе прогона. @@ -109,19 +132,33 @@ jobs: refuse "стоит blocked — конвейер не запускается" \ "на issue стоит \`blocked\` — задача ждёт внешнего решения. Снять метку, когда решение принято." elif [ "$EXHAUSTED" = "true" ]; then + # Метку снимает владелец, а не конвейер: автоматика, отменяющая + # остановку работы, дороже ручного снятия. Но пересчёт печатается — + # метка могла остаться от прежнего правила, когда бюджет тратил и + # зелёный вердикт (#227). + stale="" + if [ "$spent" -lt "$limit" ]; then + stale=" Пересчёт по действующему правилу: блокирующих циклов $spent из $limit — метка могла остаться от прежнего правила, когда бюджет тратил любой вердикт. Снять её может владелец." + fi refuse "стоит review-4 — решение за владельцем" \ - "на issue стоит \`review-4\`: лимит циклов ревью исчерпан, дальше решает владелец — разделить задачу, отклонить или арбитраж (PROCESS.md §4)." - elif [ "$done_cycles" -ge "$limit" ]; then - echo "циклов этапа $stage пройдено $done_cycles из $limit — лимит исчерпан" + "на issue стоит \`review-4\`: лимит циклов ревью исчерпан, дальше решает владелец — разделить задачу, отклонить или арбитраж (PROCESS.md §4).$stale" + elif [ "$spent" -ge "$limit" ]; then + echo "блокирующих циклов этапа $stage: $spent из $limit — лимит исчерпан" gh issue edit "$NUM" --repo "${{ github.repository }}" --add-label review-4 + # Перечень учтённого обязателен: иначе владельцу приходится читать + # всю ленту, чтобы понять, из чего сложился счёт. gh issue comment "$NUM" --repo "${{ github.repository }}" --body \ - "Лимит циклов ревью исчерпан ($done_cycles из $limit на этапе \`$stage\`). Пятого захода нет: решение владельца — разделить задачу, отклонить или арбитраж (PROCESS.md §4)." + "Лимит циклов ревью исчерпан: блокирующих циклов $spent из $limit на этапе \`$stage\` (заход $attempt). Следующего захода нет: решение владельца — разделить задачу, отклонить или арбитраж (PROCESS.md §4). + + Учтены вердикты с блокирующими находками — зелёные бюджет не тратят: + $spent_list" stage="" else - echo "этап $stage, цикл $((done_cycles + 1)) из $limit" + echo "этап $stage, заход $attempt, блокирующих циклов $spent из $limit" fi echo "stage=$stage" >> "$GITHUB_OUTPUT" - echo "cycle=$((done_cycles + 1))" >> "$GITHUB_OUTPUT" + echo "cycle=$attempt" >> "$GITHUB_OUTPUT" + echo "spent=$spent" >> "$GITHUB_OUTPUT" echo "limit=$limit" >> "$GITHUB_OUTPUT" review: @@ -230,7 +267,11 @@ jobs: Этап: ${{ needs.guard.outputs.stage }} spec — ревью ТЗ (PROCESS.md §2.4) code — код-ревью (PROCESS.md §2.7) - Цикл: r${{ needs.guard.outputs.cycle }} + Заход: r${{ needs.guard.outputs.cycle }} · блокирующих циклов израсходовано ${{ needs.guard.outputs.spent }} из ${{ needs.guard.outputs.limit }} + Бюджет §4 тратят только жёлтые и красные вердикты: зелёный + ничего не вернул на правки и цикла не образует (#227). + Номер захода нужен для имени документа — два документа с + одинаковым номером затёрли бы друг друга. **Если цикл не первый — объём разбора по дельте, а не заново** (PROCESS.md §2.9, issue #214). Раньше промпт был одинаковым для @@ -359,7 +400,7 @@ jobs: Затем оставь в issue краткий комментарий: вердикт, ключевые находки и ссылка на документ. Первой строкой — вердикт в формате §7.2: - `Вердикт: зелёный/жёлтый/красный · цикл r${{ needs.guard.outputs.cycle }}/${{ needs.guard.outputs.limit }} · High: N · Medium: N → в задаче | #…` + `Вердикт: зелёный/жёлтый/красный · заход r${{ needs.guard.outputs.cycle }} · блокирующих циклов ${{ needs.guard.outputs.spent }}/${{ needs.guard.outputs.limit }} · High: N · Medium: N → в задаче | #…` («→ #…» — только у Medium вне скоупа; находки в скоупе возвращаются автору жёлтым) Затем верни JSON по схеме. Это последнее действие и оно обязательно: