diff --git a/.github/workflows/process.yml b/.github/workflows/process.yml index 6fcdd301..880ecab4 100644 --- a/.github/workflows/process.yml +++ b/.github/workflows/process.yml @@ -98,8 +98,8 @@ jobs: # Вердикты считаются ТОЛЬКО своего этапа: иначе вердикт по ТЗ съедал # цикл из бюджета код-ревью (#89 получило r2/4). Этап опознаётся по # имени документа в теле комментария; документа нет — вердикт не - # посчитается. Недосчёт даёт лишний заход, перерасчёт остановил бы - # работу досрочно: из двух ошибок выбрана обратимая. + # посчитается. Недосчёт считался обратимой ошибкой — «даёт лишний + # заход» — и в этой оценке была ошибка, см. ниже. # # Счёт по комментариям остаётся ровно тем же, но он БОЛЬШЕ НЕ # ЕДИНСТВЕННЫЙ (#454). Маркер этапа попадает в тело комментария, @@ -156,8 +156,20 @@ jobs: counters=$(node scripts/review-doc-guard.mjs --counters \ --marker="$marker" --num="$NUM" --names="$names" --docs="$docs" \ --comment-attempt="$attempt" --comment-spent="$spent") - attempt=$(printf '%s\n' "$counters" | sed -n 's/^attempt=//p') - spent=$(printf '%s\n' "$counters" | sed -n 's/^spent=//p') + # Пустой ответ означает, что скрипт не отработал. Тогда действуют + # прежние значения: guard обязан продолжить работу, а не встать — + # худшее, что даёт откат к прозе, это сегодняшнее поведение. + new_attempt=$(printf '%s\n' "$counters" | sed -n 's/^attempt=//p') + new_spent=$(printf '%s\n' "$counters" | sed -n 's/^spent=//p') + new_blocking=$(printf '%s\n' "$counters" | sed -n 's/^blocking=//p') + case "$new_attempt" in ''|*[!0-9]*) echo "::warning::счёт по файлам не дал числа — остаётся счёт по комментариям" ;; *) attempt="$new_attempt" ;; esac + case "$new_spent" in ''|*[!0-9]*) : ;; *) spent="$new_spent" ;; esac + # Перечень учтённого обязан сходиться с числом: если цикл виден + # только документом, ссылка на комментарий его не объяснит. + if [ -n "$new_blocking" ]; then + spent_list="$spent_list + - документы: $new_blocking" + fi echo "ветка материала: ${branch:-нет, читался dev}" fi diff --git a/scripts/review-doc-guard.mjs b/scripts/review-doc-guard.mjs index 3c06b226..202a9b11 100644 --- a/scripts/review-doc-guard.mjs +++ b/scripts/review-doc-guard.mjs @@ -372,8 +372,18 @@ export function attemptFromRounds(rounds) { return known.length ? Math.max(...known) + 1 : 1; } -/** Строка, ОБЪЯВЛЯЮЩАЯ вердикт (а не упоминающая слово). */ -const VERDICT_DECLARATION = /^[>\s]*(?:[-*+]\s*)?\*{0,2}Вердикт[:*\s]/; +/** + * Строка, ОБЪЯВЛЯЮЩАЯ вердикт (а не упоминающая слово). + * + * Двоеточие обязано стоять сразу за словом. Проверено на корпусе: + * `CODE-REVIEW-441-r1.md` (в нём, кстати, тоже лежит документ ВТОРОГО раунда — + * та же коллизия, что чинится) описывает предыдущий раунд строкой + * «- Вердикт r1: жёлтый, High 0…». Правило, допускавшее слово между «Вердикт» + * и двоеточием, засчитывало эту строку объявлением и добавляло зелёному + * раунду блокирующий цикл. Ошибка необратимая: лишний цикл останавливает + * работу по §4. + */ +const VERDICT_DECLARATION = /^[>\s]*(?:[-*+]\s*)?\*{0,2}Вердикт\*{0,2}\s*:/; /** Заголовок раздела вердикта: «## Вердикт», «## 6. Вердикт». */ // `\b` здесь не работает: он ASCII-словесный, а «т» кириллическая, и граница // после неё не находится. Хвост ограничивается явно. @@ -521,6 +531,9 @@ if (invokedDirectly) { + ` циклы: файлы ${counters.spentFiles}, комментарии ${counters.spentComments}`); console.log(`attempt=${counters.attempt}`); console.log(`spent=${counters.spent}`); + // Перечень учтённого по файлам: без него владелец видит число, но не может + // сверить, из чего оно сложилось, когда комментарии цикла не показывают. + console.log(`blocking=${counters.blocking.join(', ')}`); process.exit(0); } diff --git a/test/review-doc-guard.test.mjs b/test/review-doc-guard.test.mjs index 3dc37f3e..4cc70171 100644 --- a/test/review-doc-guard.test.mjs +++ b/test/review-doc-guard.test.mjs @@ -461,3 +461,19 @@ test('guard считает раунды скриптом, а не inline-shell ( // Скрипт лежит в репозитории, значит guard обязан его выкачать. assert.match(workflow.slice(0, workflow.indexOf(' - id: decide')), /actions\/checkout/); }); + +test('описание чужого раунда не объявляет вердикт (#454, находка на корпусе)', () => { + // CODE-REVIEW-441-r1.md: документ зелёного второго раунда описывает жёлтый + // ПЕРВЫЙ раунд строкой-буллитом. Правило, допускавшее слово между «Вердикт» + // и двоеточием, добавляло зелёному раунду блокирующий цикл. + assert.equal( + verdictDeclaration('- Вердикт r1: жёлтый, High 0, Medium 1 в скоупе — красный `check-docs.mjs`'), + null, + ); + assert.equal(verdictDeclaration('- Вердикт предыдущего раунда: красный'), null); + // Штатные формы объявления при этом обязаны читаться по-прежнему. + assert.ok(verdictDeclaration('Вердикт: жёлтый · заход r1')); + assert.ok(verdictDeclaration('**Вердикт: красный** · заход r2')); + assert.ok(verdictDeclaration('- **Вердикт:** жёлтый')); + assert.ok(verdictDeclaration('- Вердикт: зелёный')); +});