Files
houseplan-card/docs/reviews/SPEC-REVIEW-472-r1.md
T
2026-09-06 11:00:46 +00:00

209 lines
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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,
которой нужен список прогонов; job `smoke`/`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` для job `changes`,
которой нужен список прогонов, тогда как job `smoke`/`frontend`, которые
тоже вызывают `actions/download-artifact`, никакого собственного
`permissions:` не имеют и получают безлимитный дефолт `validate.yml` —
в этом файле верхнего `permissions:` нет вовсе). У `mutation-gate.yml`,
наоборот, верхний `permissions: contents: read` уже есть (строки 31–32),
так что job `report` с одним `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 уже
зелёный на этом же SHA `615045cb` (упомянутый в контексте прогон), а
задача ещё в `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 циклов лёгкого трека.
---
<!-- material-anchors: сгенерировано конвейером (#414) -->
## Материал раунда
- Ветка: `dev`, коммит `` — ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет.
- Якоря снять не удалось: ветки задачи нет, материал читался по `dev`.