# SPEC-REVIEW-472-r2 Этап: spec (PROCESS.md §2.4) · Трек: лёгкий (`small`) · Заход r2 · блокирующих циклов 2/2 (лимит лёгкого трека — 2, §4) SHA на момент ревью: `f52b2449eaadaa2f604634780a0da926779ad9c3` (dev) Ревьюер ≠ автор ТЗ. ## Скоуп повторного раунда Issue #472: еженедельный полный мутационный прогон (`mutation-gate.yml`) может падать/отменяться без адресата. ТЗ в теле issue (лёгкий трек), контракт из 6 пунктов, AC1–AC8, теперь три мутанта на будущий `scripts/mutation-gate.mjs`. Продукт не затронут — чистая CI/process-задача, кода ещё нет (ветки `issue/472-*` не существует, см. «Как проверялось»). Раунд r1 (жёлтый, `docs/reviews/SPEC-REVIEW-472-r1.md`, SHA `615045cb`) нашёл M1 (AC3 не различает «сбежавший мутант» и «уже красный guard»), M2 (AC5 не даёт job `report` прав на скачивание артефактов) и L1 (неверный номер строки). Автор ответил комментарием «Оба Medium исправлены в теле ТЗ, Low тоже» и отредактировал тело issue. **Предмет этого раунда — дельта тела issue**: правки контракта п.3 (AC3, «Мутанты»), п.4 (AC5) и абзаца «Проблема» (ссылка на строку). Полный разбор не требуется: dev не сдвинулся в затронутых файлах между `615045cb` и текущим HEAD (см. ниже), новая подсистема не появилась, контракт поведения не сменился — только текст ТЗ. ## Как проверялось - Перечитано тело issue #472 целиком (текущая редакция) и все четыре комментария (S2, «ТЗ готово», вердикт r1, «Оба Medium исправлены»). - `git log --oneline 615045cb..HEAD -- .github/workflows/mutation-gate.yml .github/workflows/validate.yml .github/workflows/announce.yml scripts/mutation-gate.mjs test/mutation-gate.test.mjs` → пусто: ни один из файлов, на которые опирается контракт, не менялся между материалом r1 и текущим HEAD (`f52b2449`, второй коммит после `615045cb` — не по этой задаче, документ ревью #475). Значит фактическое состояние репозитория, описанное в r1, актуально и сейчас; переоткрывать сверку с файлами вне делты незачем. - `scripts/mutation-gate.mjs`: `runMutant` (строка 6759 — `FAIL ${mutant.id}: тест остался зелёным на сломанном коде`), `runCleanGuards` (строки 6771–6793 — `FAIL чистый прогон: ${guard} красный без мутанта`), `main` (строка 6897, вызов `runCleanGuards` до цикла мутантов) — перечитаны заново, подтверждают, что обе формы `FAIL` печатаются в один и тот же лог именно так, как описывает исправленный п.3. - `grep -oP "id: '\K[^']+" scripts/mutation-gate.mjs | grep -vE '^[a-z0-9-]+$'` → пусто (509 id, `grep -c "id: '"` подтверждает то же число, что и в r1) — техническая посылка M1 «все id — `[a-z0-9-]+`, различить формы дёшево» не сломалась. - `.github/workflows/mutation-gate.yml`: `permissions: contents: read` — строка 31; `concurrency.group: mutation-gate` / `cancel-in-progress: true` — строки 34–36; `checkout ref: ... || 'dev'` — строка 52. L1 (ссылка была на строку 33) подтверждена закрытой: текст issue сейчас ссылается на 52. - `.github/workflows/validate.yml`: job `preflight.permissions` — строки 38–41 (`contents: read`, `actions: read`, `issues: read`) — прецедент, на который ссылается M2-фикс в контракте п.4, подтверждён построчно. - `git status --short`, `git branch -a | grep 472`, `git log --all --oneline | grep 472` — рабочее дерево чистое, ветки `issue/472-*` нет, кода по этой задаче в истории нет: контракт всё ещё чисто в стадии ТЗ, преждевременного кода (§12) не обнаружено. - Gates (`typecheck`/`test`/`build`) не запускал: для #472 ещё нет ни строки продуктового или тестового кода (проверено выше), это ревью ТЗ, а не код-ревью — гейты неприменимы, ровно как в r1. `docs/SCOPE.md`, `docs/USER-GUIDE.ru.md`, канонические документы подсистем — не применимы: задача не трогает продукт и видимое пользователю поведение (чистый CI/process, как и установил r1). ## Закрытие раунда r1 | Находка r1 | Чем закрыта | Где это видно | |---|---|---| | **M1** — AC3 не отличал «сбежавший мутант» от строки «guard уже красный без мутанта»; наивный парсер получил бы фиктивный id | Контракт п.3 теперь явно вводит `escaped` (id обязан существовать в реестре, иначе — `unparsed`) и `redGuards` (для `FAIL чистый прогон: …`) как разные ведра; AC3 переписан с перечислением всех четырёх случаев; добавлен третий мутант, целящийся именно в эту путаницу | Тело issue, «Контракт» п.3 (абзац «две формы `FAIL`…»), таблица AC/строка AC3, «Мутанты»/строка `mutation-report-red-guard-as-mutant` | | **M2** — AC5 называл для job `report` только `permissions: issues: write`, job не смог бы скачать артефакты шардов (`actions: read` отсутствует) | Контракт п.4 и AC5 перечисляют `contents: read, actions: read, issues: write` с обоснованием каждого права (checkout, `download-artifact`, `gh issue`) | Тело issue, «Контракт» п.4 (первый абзац), таблица AC/строка AC5 | | **L1** — ссылка на строку `mutation-gate.yml:33` для checkout `ref: dev` неверна (фактически 52) | Ссылка исправлена на `mutation-gate.yml:52`, подтверждено `grep -n` на HEAD `f52b2449` | Тело issue, «Проблема», последний абзац | Все три закрыты по существу (строка контракта/AC, не только заявление автора в комментарии). ## Унаследовано из r1 Не проверялось заново — дельта этого раунда их не задевает, а файлы, от которых зависит их доказательство, не менялись с `615045cb`: - Контракт п.1 (раздельные concurrency-группы по `github.event_name`) и AC1; - Контракт п.2 (артефакт лога на шард, `if: always()`) и AC2; - Контракт п.5 (`workflow_sync` расширяется на `mutation-gate.yml`) и AC8; - Контракт п.6 («что не меняется» — реестр, гварды, шардирование, `--changed`, права `mutants`); - AC6 (повторный отказ дописывает комментарий, не создаёт issue), AC7 (отсутствие Telegram-секретов не роняет job); - «Откат» (один коммит, без миграции данных); - Критерии лёгкого трека §5 (все пять выполнены, отказа от трека не требуется). Источник: `docs/reviews/SPEC-REVIEW-472-r1.md`, материал — SHA `615045cb1882 fa36164a55d544dc6520f2f58618` (dev). Проверено, что `git log --oneline 615045cb..HEAD` для `.github/workflows/mutation-gate.yml`, `.github/workflows/validate.yml`, `.github/workflows/announce.yml`, `scripts/mutation-gate.mjs`, `test/mutation-gate.test.mjs` пуст — основание для наследования не устарело. ## Находки ### Medium (в скоупе задачи) — 1 **M3 (новая, введена самим фиксом M1). Контракт п.3 противоречит себе насчёт того, попадают ли `redGuards`/`unparsed` в тело письма, которое видит владелец, — а AC4 проверяет только `escaped`-часть тела.** - Файл: тело issue #472, раздел «Контракт», п.3 «Репортёр — отдельный тестируемый скрипт», и таблица AC, строка AC4. - Первый абзац п.3 (тот самый, что закрывает M1) прямо обещает: «В теле они идут отдельными разделами: сбежавшим — `--id=`, красным гардам — их команда как есть» — то есть `redGuards` обязаны попасть в тело отдельным разделом. - Третий абзац того же пункта, который буквально перечисляет содержимое тела, называет только: «дата, ссылка на прогон, SHA `dev`, таблица шардов, список сбежавших мутантов с их `guard` из реестра… и одна фраза «что делать»… [`--id=`] на каждый» [сбежавшего] — ни `redGuards`, ни `unparsed` здесь не упомянуты вовсе. - AC4 — единственный критерий, доказательство которого касается содержимого `title`/`body`, — описывает то же самое узкое перечисление: «тело — ссылку на прогон, SHA и по одной команде `--id=` на сбежавшего». Про `redGuards`/`unparsed` в теле AC4 не говорит ничего. - Симметричный, более мелкий след того же недосмотра: первый bullet п.3 объявляет форму возврата `mutationGateReport` как `{ title, body, escaped, redGuards, shards }` — без поля `unparsed`, хотя тот же абзац требует отдельный список `unparsed` для строк с id вне реестра, и AC3 явно включает `unparsed` в перечень того, что репортёр обязан произвести. - Как это проявляется (разобрано по тексту — реализации ещё нет, «проверено чтением, не исполнением» неприменимо, применяю «проверено по контракту»): реализация, которая берёт AC4 буквально и пишет unit ровно на «заголовок + дата + ссылка + SHA + таблица шардов + список escaped с `--id=`», проходит все восемь AC — и при этом `redGuards`/`unparsed` никогда не попадают в письмо, которое читает владелец. Это тот же класс дефекта, ради устранения которого заведена вся задача («у отказа нет адресата»): «guard был красным ещё до мутанта» или «встретилась нераспознанная `FAIL`-строка» — оба случая реального отказа шарда — снова тихо не долетают до issue/Telegram, просто на один уровень глубже (не сам факт падения шарда, а его причина). - Почему это находка именно r2, а не пропуск r1: до фикса M1 контракт вообще не различал `redGuards`/`unparsed` как отдельные сущности — само это разделение и обещание «отдельных разделов» появилось в тексте только сейчас, вместе с ответом на M1. Дельта этого раунда его и порождает. - Что чинит: одна фраза в AC4 (или новый AC4b) — тело обязано содержать `redGuards` отдельным разделом с командой гарда «как есть», и явно сказать, что происходит с `unparsed` в теле письма (свой раздел — либо, если решение «в тело не выводить, только в служебное поле», написать это прямо, а не оставлять молчание, которое ревьюер обязан трактовать как недосказанность, а не как решение); плюс добавить `unparsed` в объявленную форму возврата `mutationGateReport`. ### Low — 1 (не блокирует, на усмотрение автора) **L2.** Раздел «Затронутые файлы» всё ещё говорит «`scripts/mutation-gate.mjs` — два мутанта», хотя после фикса M1 в разделе «Мутанты» их три (добавлен `mutation-report-red-guard-as-mutant`). Реализацию не путает — список мутантов в разделе «Мутанты» исчерпывающий и однозначный, реализатор будет читать его, а не считать по итоговой сводке, — но счётчик стоит поправить для согласованности документа. ## Что проверено и корректно - M1: закрытие сверено с фактическим кодом `runMutant`/`runCleanGuards`/ `main` на HEAD — обе формы `FAIL` действительно печатаются в один лог в точности так, как теперь описывает контракт; техническая посылка (все id — `[a-z0-9-]+`, различение дёшево) реверифицирована и держится. - M2: закрытие сверено с прецедентом `validate.yml` (`preflight.permissions`, строки 38–41) — набор прав `contents: read, actions: read, issues: write` и обоснование каждого совпадают с моделью GitHub Actions (job-level `permissions` заменяет, а не дополняет). - L1: номер строки `mutation-gate.yml:52` подтверждён `grep -n` на HEAD. - AC1, AC2, AC6–AC8, «Откат», критерии лёгкого трека §5 — не тронуты дельтой, файлы, на которых основано их доказательство, не менялись с `615045cb`; наследуются без переисполнения (раздел выше). - Кода по issue #472 нет нигде в дереве и истории — задача честно остаётся в стадии ТЗ, преждевременного кода (§12) не обнаружено. ## Чего не проверял - `npx tsc --noEmit` / `npm test` / `npm run build` — не запускал: для #472 нет ни строки кода (проверено `git status`/`git branch`/`git log --all`), гейты неприменимы на этапе ревью ТЗ. - `node scripts/mutation-gate.mjs` живьём, эмуляция `--shard` со сломанным guard — не запускал: `scripts/mutation-gate-report.mjs` и `test/mutation-gate-report.test.mjs` ещё не существуют (подтверждено `ls`), исполнять нечего; вывод по M1/M3 получен чтением контракта и существующего `scripts/mutation-gate.mjs`, а не исполнением. - golden / браузерные смоки / инварианты модели / performance-профили — не применимы: diff не существует, задача не трогает продукт, фронтенд или геометрию. - Контракт п.1, п.2, п.5, п.6, AC1, AC2, AC6–AC8, «Откат» — не переразбирал по существу сверх сверки «файлы не менялись» (см. «Унаследовано из r1»); если у ревьюера были к ним вопросы, они должны были прозвучать в r1. - Точность фразы «паттерн [пропуска при отсутствующих секретах] уже есть в `announce.yml`» (AC7, контракт п.4, абзац про Telegram) — эта часть текста не менялась между r1 и r2 и не входит в дельту; r1 принял AC7 как доказанный, переоткрывать вне делты по правилам §2.10 не стал. ## Вердикт Одна находка Medium **в скоупе задачи** (M3), High нет. По §2.4/§7.2 это жёлтый вердикт: автор правит текст ТЗ (тело/AC4, при необходимости — форму возврата в п.3), фикс проходит третий заход. Бюджет циклов лёгкого трека — **2** (§4); эта находка использует второй из них, то есть лимит на этом раунде исчерпан. Дальнейшая правка возможна («заход» и «цикл» — разные величины, заходов может быть больше двух), но при новой блокирующей находке в r3 решение по §4 переходит владельцу (разделить/отклонить/арбитраж), а не автоматически продолжает цикл. --- ## Материал раунда - Ветка: `dev`, коммит `f52b2449eaadaa2f604634780a0da926779ad9c3`. - Файлы, от которых зависит контракт, не менялись с материала r1 (`615045cb1882fa36164a55d544dc6520f2f58618`): `git log --oneline 615045cb..HEAD -- .github/workflows/mutation-gate.yml .github/workflows/validate.yml .github/workflows/announce.yml scripts/mutation-gate.mjs test/mutation-gate.test.mjs` → пусто. - Ветки задачи `issue/472-*` не существует — ТЗ живёт в теле issue (лёгкий трек), материал этого раунда — текущая редакция тела issue #472 на момент разбора. --- ## Материал раунда - Ветка: `dev`, коммит `` — ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет. - Якоря снять не удалось: ветки задачи нет, материал читался по `dev`.