18 KiB
SPEC-REVIEW-472-r1
Этап: spec (PROCESS.md §2.4) · Трек: лёгкий (small) · Заход r1 · блокирующих циклов 0/2
SHA на момент ревью: 615045cb1882fa36164a55d544dc6520f2f58618 (dev)
Ревьюер ≠ автор ТЗ.
Скоуп
Issue #472: еженедельный полный мутационный прогон (mutation-gate.yml) может
падать/отменяться без адресата — нет прав на issue, нет шага на отказ, одна
concurrency-группа разрешает ручному прогону убить расписание. ТЗ в теле issue
(лёгкий трек), контракт из 6 пунктов, AC1–AC8, два мутанта на
scripts/mutation-gate.mjs. Продукт (карточка, фронтенд, бэкенд-интеграция) не
затронут — чистая CI/process-задача.
Как проверялось
- Прочитаны: тело issue #472, оба комментария (
S2 — разбор,ТЗ готово). - Сверено с фактическим состоянием репозитория на
615045cb:.github/workflows/mutation-gate.yml,.github/workflows/validate.yml(шагworkflow_sync),.github/workflows/announce.yml(образец Telegram-шага),scripts/mutation-gate.mjs(реестр,runMutant,runCleanGuards,main()— ветки--shard/--check/--build-only),test/mutation-gate.test.mjs(существующее покрытие), стиль контрактных тестов на YAML вtest/gate-reuse.test.mjs(прецедент: строковые проверки, без YAML-парсера — вpackage.jsonего и нет). - Проверена модель прав GitHub Actions (
permissions:на уровне job заменяет, а не дополняет уровень workflow) против прецедентов в самом репозитории (validate.yml:38,40—actions: readявно объявлен для job, которой нужен список прогонов; jobsmoke/frontend, качающие артефакты черезdownload-artifact, не имеют собственногоpermissions:и потому наследуют неограниченный дефолтvalidate.yml, где верхнегоpermissions:нет вовсе — в отличие отmutation-gate.yml, где верхнийpermissions: contents: readуже ограничивает всё, что не названо). - docs/SCOPE.md, docs/USER-GUIDE.ru.md, канонические документы подсистем (SUN/LIGHT/CANVAS/WALL-THICKNESS/UX-MODES/CONFIG-COMPATIBILITY/ TOUCH-SUPPORT) — не применимы, задача не трогает продукт и видимое пользователю поведение. Первым вопросом («какую строку Core user jobs закрывает задача») можно пренебречь: это не продуктовая, а процессная/ инфраструктурная задача, и §5 лёгкого трека такие явно допускает («одна поверхность» — сам workflow и его репортёр).
- Файла в
docs/specs/не создано — верно дляsmall(§2.3).
Лёгкий трек: критерии §5 — все выполнены
Подтверждаю разбор автора в S2: сложность/риск низкие (один workflow + один новый чистый скрипт + контрактные тесты), одна поверхность, нет миграции конфига, нет нового UX-контракта (пользователь продукта ничего не видит), нет влияния на perf/touch. Отказа от лёгкого трека не требуется.
Находки
Medium (в скоупе задачи) — 2
M1. AC3 не отличает «сбежавший мутант» от «чистый прогон уже красный» — риск ложного/потерянного сигнала в самом отчёте, который эта задача обязана делать надёжным.
- Файл:
scripts/mutation-gate.mjs, функцияrunMutant(строка 6759) иrunCleanGuards(строки 6771–6793); контракт — тело issue #472, раздел «Контракт», пункт 3, и AC3. - Воспроизведение (по коду, не по прогону — «проверено чтением»): шаг
«Каждый тест ловит свою поломку» вызывает
node scripts/mutation-gate.mjs --shard=N/4. Внутриmain()(строка 6897) до цикла по мутантам всегда вызываетсяrunCleanGuards(selected)— прогон каждого уникальногоguardбез мутации. Если хоть один guard уже красный без мутанта (флеки, сломанное окружение, забытый мердж-конфликт в тесте), печатаетсяи функция возвращаетFAIL чистый прогон: <guard-команда> красный без мутантаfalseраньше, чем напечатается хоть одна строкаFAIL <id>: тест остался зелёным на сломанном коде— то есть эта форма реально попадает в тот же лог, что улетит артефактом (AC2), и относится к той же самой невыполненной задаче: «шард отказал, назови почему». Контракт AC3 описывает парсинг как «отсортированный список id изFAIL-строк всех шардов» — этого недостаточно, чтобы отличитьFAIL чистый прогон: …отFAIL <mutant-id>: …: если репортёр наивно берёт первый токен послеFAIL, он получит фиктивный «id» вродечистый(иличистый прогон, в зависимости от реализации), которого нет в реестре — а значит нет иguard-а для таблицы, и команда «что делать»node scripts/mutation-gate.mjs --id=чистыйв письме владельцу будет синтаксически некорректной (реестр ответит «мутант «чистый» не объявлен»). Если же вместо этого такую строку молча отбросить без специального правила — реальная поломка (тест уже красный без всякого мутанта) исчезнет из отчёта так же тихо, как исчезал изначальный сигнал, из-за которого заведена вся задача. - Все реальные id мутантов — строго
[a-z0-9-]+(проверено:grep -c "id: '" scripts/mutation-gate.mjs→ 509 совпадений, ни один не содержит кириллицу/пробел), так что различение технически дёшево — но ТЗ не называет правило, и заявленные фикстуры юнита («три лога») не гарантируют, что этот случай будет вообще замечен реализацией. - Почему это не «додумать самому за автора»: правило разбора лога — решение не продуктовое (агенты решают такие сами, §7.1), но оно прямо определяет, выполняется ли AC3 и AC4 корректно, поэтому это находка ревью, а не тихое согласие.
- Что чинит: одна фраза в контракте — например, «строки
FAIL чистый прогон: …не считаются сбежавшим мутантом; шард получает статусfailedбез записи вescaped, само сообщение уходит в отдельное поле/строку отчёта (broken guard)» — плюс лог-фикстура на этот случай в AC3.
M2. AC5 называет для job report только permissions: issues: write —
как буквально описано, job не сможет скачать артефакты шардов.
- Файл: тело issue #472, раздел «Контракт», пункт 4 («Job
report»), и таблица AC, строка AC5. - В GitHub Actions
permissions:, объявленный на уровне job, заменяет права workflow целиком для этой job, а не дополняет их (документированное поведение; в этом же репозитории есть подтверждающий прецедент:validate.yml:38-40явно прописываетactions: readдля jobchanges, которой нужен список прогонов, тогда как jobsmoke/frontend, которые тоже вызываютactions/download-artifact, никакого собственногоpermissions:не имеют и получают безлимитный дефолтvalidate.yml— в этом файле верхнегоpermissions:нет вовсе). Уmutation-gate.yml, наоборот, верхнийpermissions: contents: readуже есть (строки 31–32), так что jobreportс однимissues: writeполучит только это право и ничего сверх. - Job
reportпо контракту «скачивает артефакты всех шардов» —actions/download-artifactобращается к Actions REST API, которая требуетactions: read; без него шаг вернёт 403 независимо от того, что репозиторий публичный (это разграничение GitHub API, а не git-протокола). Ей также нужен код репозитория, чтобы выполнитьnode scripts/mutation-gate-report.mjsи прочитать реестр мутантов изscripts/mutation-gate.mjs— то есть checkout, для которого обычно указываютcontents: read(на публичном репозитории он, возможно, отработает и без явного права, но называть это гарантией не стоит). - Последствие: реализация «по букве AC5» ломает job
reportна первом же реальном отказе по расписанию — то есть именно тогда, когда его сигнал нужен. Это новый вариант того самого «у отказа нет адресата», ради которого заведена вся задача, просто на другом шаге. - Что чинит: расширить AC5 —
permissions: { contents: read, actions: read, issues: write }(или явно объяснить, почемуcontentsне нужен, если автор проверит, что публичный checkout работает без него).
Low — 1 (не блокирует, на усмотрение автора)
L1. Неверный номер строки в разделе «Проблема». Текст «checkout берёт
ref: dev (mutation-gate.yml:33)» — на 615045cb эта строка (шаблон
${{ github.event_name == 'workflow_dispatch' && inputs.ref || 'dev' }})
находится на строке 52, а не 33 (33–36 — блок concurrency). Утверждение по
существу верное (checkout действительно форсирует dev вне
workflow_dispatch), путаница только в ссылке — не влияет ни на один AC,
править по усмотрению автора.
Что проверено и корректно
- Все три «конструктивные причины» из «Проблема» подтверждены построчно:
permissions: contents: read— строки 31–32; отсутствие шага на отказ — в файле действительно нетif: failure()/уведомлений; одна concurrency-группаmutation-gateсcancel-in-progress: true— строки 34–36. - AC1 (раздельные concurrency-группы по
github.event_name) — реализуемо контрактным тестом на строкуgroup:, других триггеров у workflow нет (толькоworkflow_dispatchиschedule), двузначности нет. - AC8 и пункт 5 контракта — сверено с
validate.yml: шагworkflow_sync(строки 61-77) сегодня действительно сверяет толькоprocess.ymlчерезgit show origin/main:… origin/dev:…; расширение на второй файл тем же diff'ом тривиально и не пересекается с M1/M2. - AC7 (Telegram необязателен) — паттерн уже есть в
announce.yml(тот жеcurl, секреты черезenv,set -euo pipefail); контракт прямо ссылается на этот файл, реализуемо без новых допущений. - Формат
FAIL <id>: тест остался зелёным на сломанном кодев AC3/AC4 дословно совпадает с реальной строкой в коде (mutation-gate.mjs:6759) — сам факт этой конкретной строки не выдуман, выдумано (точнее, недосказано) только то, что она не единственная формаFAILв том же логе (см. M1). - Два новых мутанта (
mutation-report-drops-missing-shard,mutation-report-duplicates-escaped) целятся именно в AC3 и укладываются в существующий формат реестраscripts/mutation-gate.mjs. - Откат — одним коммитом, без миграции данных; согласуется с «Что не
меняется» (реестр, шардирование,
--changed, праваmutants). - Критерии лёгкого трека §5 — все пять выполнены одновременно, отказа от трека не требуется (см. выше).
- Файл в
docs/specs/не создан, соответствует правилуsmall(§2.3).
Чего не проверял
- Не проверял, действительно ли
actions/checkoutна публичном репозитории отработает без явногоcontents: read— сослался на общую практику GitHub (github-token без прав всё ещё может анонимно клонировать публичный git, но это не то же самое, что «гарантированно работает»), это дополнение к M2, а не отдельная находка, автор может закрыть её явной проверкой в коде. - Не прогонял
npx tsc --noEmit/npm test/npm run build— Validate уже зелёный на этом же SHA615045cb(упомянутый в контексте прогон), а задача ещё вS4-spec-review: кода для этих гейтов пока нет, реализации не существует. Это ревью ТЗ, а не код-ревью. - Не запускал
node scripts/mutation-gate.mjsживьём и не эмулировал--shardс искусственно сломанным guard — вывод для M1 получен чтением функцийrunMutant/runCleanGuards/main, а не исполнением; поведение однозначно следует из порядка вызовов в коде (runCleanGuardsвыполняется и может завершитьmainраньше цикла мутантов). - Не проверял golden/смоки/инварианты модели/performance-профили — diff не существует (ТЗ, не код), ни один из этих гейтов неприменим на этом этапе.
Вердикт
Обе находки — Medium в скоупе задачи (AC3 и AC5 самого текущего
контракта), High нет. По правилу §2.4/§5 это жёлтый вердикт: автор правит
текст ТЗ (уточняет разбор FAIL-строк в контракте+AC3, расширяет
permissions в контракте+AC5), фикс проходит второй заход в пределах
лимита 2 циклов лёгкого трека.
Материал раунда
- Ветка:
dev, коммит `` — ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет. - Якоря снять не удалось: ветки задачи нет, материал читался по
dev.