Files
houseplan-card/docs/reviews/CODE-REVIEW-556-r2.md
T
2026-09-13 17:16:43 +00:00

243 lines
21 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.
# Код-ревью #556 · заход r2
Материал: `2642fdb446be65f5ebed16b9f892d6f7bd47715a` (ветка `issue/556-ci-trust-boundaries`
на `origin/dev` = `ad886b7f…`; рабочая копия проверена на этом же коммите —
`git rev-parse HEAD` совпадает). Инфраструктурная задача (§1 PROCESS.md): полный
диапазон `origin/dev...HEAD` не содержит ни одного файла класса A — только
`.github/workflows/**`, `scripts/**`, `test/**`, `docs/DEVELOPMENT.md`.
Вердикт r1: **красный**, `docs/reviews/CODE-REVIEW-556-r1.md`, материал
`954eeff45aeb187f74d460d35426ffec7299ca7b`, H1 (блокирует).
## Дельта r1 → r2
`git diff 954eeff4..2642fdb4` — три содержательных коммита автора
(`b3555188`/`954eeff4` уже были материалом r1; новые — `e0098c8d`, `2642fdb4`;
`717308f0` — публикация документа r1 конвейером, не автора):
- `.github/workflows/process.yml`: у job `model_review` `permissions:` меняется
с `{contents: read, id-token: write}` на `{contents: read, issues: write}`;
шагу `Review` (`uses: anthropics/claude-code-action@9cdae7f0…`) добавлен
`github_token: ${{ secrets.GITHUB_TOKEN }}`.
- `test/review-doc-guard.test.mjs`: существующая проверка «права по job»
обновлена под новый набор permissions; добавлен новый тест «модель работает
job-scoped токеном, а не App-обменом (#556)», проверяющий и наличие
`github_token`, и отсутствие `additional_permissions`, и отсутствие
`id-token: write`.
- `scripts/mutation-gate.mjs`: два новых мутанта —
`review-model-gets-repository-write` (снимает `contents: read` → `write`) и
`review-job-trusts-the-app-token` (снимает строку `github_token:`).
- `docs/DEVELOPMENT.md`: новый раздел «What the review model is allowed to
do (#556)», описывающий тот же механизм.
Больше ничего в делте нет: `scripts/action-pins.mjs`, `scripts/review-result-gate.mjs`
и их тесты в диапазоне 954eeff4..2642fdb4 не тронуты ни строкой — сверено
`git diff` по этим путям напрямую (пусто).
## Закрытие раунда r1
| Находка r1 | Чем закрыта | Где видно |
|---|---|---|
| **H1** (High): `permissions: {contents: read, id-token: write}` у `model_review` не было потолком — `claude-code-action` без `github_token` меняет OIDC на собственный App-токен с дефолтом `contents/issues/pull_requests: write`, и `ghs_…` от `claude[bot]` лежал прямо в окружении Bash модели | Шагу `Review` передан `github_token: ${{ secrets.GITHUB_TOKEN }}` (`process.yml:892`); `id-token: write` снят, `permissions:` job сведены к `{contents: read, issues: write}` (`process.yml:762-764`). С переданным `github_token` `setupGitHubToken()` (`src/github/token.ts` на том же закреплённом SHA) возвращает `OVERRIDE_GITHUB_TOKEN` и на обмен не идёт — обмен, из которого рождался App-токен с write, исчезает | `process.yml:748-764` (комментарий с разбором), `process.yml:888-892`; тест `ревью: модель работает job-scoped токеном, а не App-обменом (#556)` в `test/review-doc-guard.test.mjs:920-933`; мутант `review-job-trusts-the-app-token` — прогнан мной, «поймано 1 из 1» |
Строка `env | grep -i GITHUB_TOKEN` из r1 (живое доказательство `ghs_…`)
воспроизведена и в этой сессии (см. «Как проверялось» ниже) — это не
регресс фикса, а следствие того, что r2 всё ещё выполняется по СТАРОМУ
`process.yml` из `main` (см. «Что не проверял»/раздел ниже); автор
предупредил об этом отдельным комментарием после хендоффа r2, и наблюдение
ему соответствует буквально.
## Унаследовано из r1 (без повторной проверки)
Документ `docs/reviews/CODE-REVIEW-556-r1.md`, материал
`954eeff45aeb187f74d460d35426ffec7299ca7b` — дельта r2 этих файлов не касается:
- **SHA-пины.** Все 116 `uses:` в 9 воркфлоу закреплены полным SHA;
`scripts/action-pins.mjs` + `test/action-pins.test.mjs` (7/7); гейт стоит
шагом `action_pins` в preflight Validate. Принято r1 как «реализовано
корректно и доказано исполнением».
- **Граница artifact → `integrate`.** `scripts/review-result-gate.mjs`: точный
набор файлов, контрольные суммы, все 17 полей паспорта, словарь вердикта;
15 враждебных фикстур `test/review-result-gate.test.mjs`, 4 мутанта индивидуально
прогнаны в r1 («поймано 1 из 1» по каждому). `integrate` по-прежнему ходит на
чистый checkout `dev` (`persist-credentials: false`) и получает только
запечатанный artifact — сверено чтением текущего `process.yml:1152-1237`,
строки не изменились относительно материала r1.
- Трейлеры `b3555188`/`954eeff4` (`User-Visible: no`) — приняты в r1.
## Как проверялось
Дешёвые гейты этого захода уже подтверждены Validate на `2642fdb4`
([run 34770378775](https://github.com/Matysh/houseplan-card/actions/runs/34770378775),
`conclusion: success`) — `typecheck`/`test`/`build`/bundle-sync/`hacs`/`hassfest`/
diff-мутанты по всем 6 шардам покрыты этим прогоном; `tsc --noEmit`, `npm test`,
`npm run build` не перегонял.
Прогнано мной дополнительно, целенаправленно на дельту r2:
| Команда | Результат |
|---|---|
| `node --test test/review-doc-guard.test.mjs` | 61/61 green, включая оба теста «по job» и «job-scoped» |
| `node scripts/mutation-gate.mjs --id=review-job-trusts-the-app-token` | «поймано 1 из 1» |
| `node scripts/mutation-gate.mjs --id=review-model-gets-repository-write` | «поймано 1 из 1» |
| `python3 -c yaml.safe_load(...)` по `.github/workflows/process.yml` | парсится без ошибок |
| `git diff 954eeff4..2642fdb4 -- scripts/action-pins.mjs scripts/review-result-gate.mjs test/action-pins.test.mjs test/review-result-gate.test.mjs` | пусто — r1-материал не тронут делтой |
| Живое наблюдение в этой же сессии (`model_review` для #556, событие `issues`) | `env | grep -i GITHUB_TOKEN` → `ghs_…`; `gh auth status` → `claude[bot]`; `OVERRIDE_GITHUB_TOKEN=` (пусто) |
| `node scripts/check-docs.mjs` и сверка `visualFingerprint(ROOT)` с `docs/images/screenshots.json` вручную | совпадают побайтово — фингерпринт НЕ устарел на этом материале (см. находку ниже) |
| `diff <(git show origin/main:.github/workflows/process.yml) <(git show origin/dev:.github/workflows/process.yml)`, то же для `mutation-gate.yml` | main и dev пока идентичны — расхождение появится только ПОСЛЕ слияния этой ветки |
### Почему живое наблюдение не опровергает фикс
Последнее наблюдение (`ghs_…`, `claude[bot]`) выглядит как повтор находки r1,
но это ожидаемо и не регресс: для события `issues` GitHub загружает
`process.yml` из ветки по умолчанию (`main`), а не из ветки задачи — фикс
физически не может подействовать на СОБСТВЕННУЮ ревью-сессию до слияния и
зеркалирования файла в `main`. Автор явно предупредил об этом отдельным
комментарием после хендоффа r2. Косвенное подтверждение того же вывода —
`OVERRIDE_GITHUB_TOKEN` в окружении этой сессии пуст: `github_token:` в
запущенный `с action` не передавался, то есть выполнялась именно старая
версия шага. Правильный признак после слияния и зеркалирования, по которому
это стоит проверить в журнале следующего живого прогона: строка
`Using provided GITHUB_TOKEN for authentication` вместо `Requesting OIDC
token…` / `Exchanging OIDC token for app token…`, и токен в окружении
Bash-инструмента модели не начинается с `ghs_` (или начинается, но
принадлежит не App, а ambient job-токену — различимо по `gh auth status`,
который для ambient токена покажет `github-actions[bot]`, а не `claude[bot]`).
Не проверял и не мог проверить: реальный интерсект `additional_permissions` с
серверной стороной обмена токена (сервис недоступен для инспекции) — но фикс
этот путь не использует вовсе, поэтому вопрос снят самой формой решения, а не
предположением о ней.
Не запускал: `golden:verify`, `demo/smoke_*`, `pytest tests_backend`,
`model-invariants`, performance-профили — diff не касается `src/**`,
`custom_components/**/*.py` и геометрии ни одной строкой; в Validate эти job
`skipped` закономерно.
## Находки
Блокирующих находок нет. Одно уточнение к двум замечаниям автора вне скоупа
(не Medium-дефект и не повод заводить отдельный issue — см. обоснование ниже).
### Уточнение к замечанию автора «нужно зеркало `process.yml` в `main`»
Автор дважды (r1 хендофф, r2 комментарий) отметил только `process.yml`.
Фактический охват шире и уже механизирован существующим (не новым) шагом
Validate `"Процесс: process.yml и mutation-gate.yml идентичны в main и dev"`
(`validate.yml:94-113`, `continue-on-error: true`, но входит в общий вердикт
preflight и красит его). Эта же задача SHA-пинами трогает `mutation-gate.yml`
(расписание еженедельного полного мутационного прогона грузится из `main`
ровно по той же причине, что и `process.yml` — комментарий на `mutation-gate.yml:99-101`
и issue #472). Сейчас `main` и `dev` по этим двум файлам идентичны (проверено
`diff` выше); после слияния этой ветки они разойдутся по обоим файлам сразу,
`workflow_sync` в Validate станет `FAIL`, и по правилу §8 («Гейт беты: CI
Validate зелёный на точном SHA») это заблокирует ближайший бета-кандидат, а не
только «сломает событие `issues`», как сформулировано в хендоффе.
Не завожу отдельный issue: это не дефект #556, а существующий (до-#556)
защитный гейт, который сработает штатно, и разовое действие, доступное только
владельцу (push в `main` — по AGENTS.md исключительно владельческая операция);
сам факт необходимости зеркалирования уже дважды зафиксирован автором в этом
issue, «оставили в тексте» здесь неприменимо — заводить второй issue про то же
самое разовое действие избыточно. Фиксирую только для точности: зеркалировать
нужно **оба** файла, и последствие при задержке — красный `workflow_sync` в
Validate на `dev`, а не абстрактное «расхождение».
### Замечание автора «check-docs красный на dev» — больше не воспроизводится
Дважды повторённое в r1/r2 замечание («стух отпечаток скриншотов на чистом
`dev`») на материале r2 не подтвердилось: `node scripts/check-docs.mjs` и
`node scripts/check-docs.mjs --screenshots=strict` проходят чисто, без единого
предупреждения; сверка `visualFingerprint(ROOT)` с
`docs/images/screenshots.json:sourceFingerprint` вручную дала побайтовое
совпадение
(`05e8facaeefbf66bd8a365ed55111ca0e583d2cb6edd188147c57953ba7bc1ec`). Между
базой r1 (`origin/dev` = `8fa2568c`) и базой r2 (`origin/dev` = `ad886b7f`)
в `dev` прошла отдельная задача (#563, судя по сообщениям коммитов
`aa13e226`/`cc0b41ba`), которая, по всей видимости, обновила скриншоты —
никакого действия по #556 это не требует, задача не в её скоупе и не задета
её диффом. Указывать отдельным issue нечего — состояние уже здоровое.
## Что проверено и корректно
- **Фикс H1 сам по себе.** Логика верна и минимальна: `github_token:` в
`with:` шага `Review` активирует `OVERRIDE_GITHUB_TOKEN` в
`setupGitHubToken()`, обмен OIDC→App-токен не происходит,
`permissions: {contents: read, issues: write}` job становится реальным
потолком для ambient-токена, который достаётся действию. `id-token: write`
корректно снят как более не нужный — других потребителей OIDC в этой job нет
(проверено чтением всех шагов `model_review`, `process.yml:776-1105`).
- **`issues: write` оставлено осознанно и обоснованно.** Нужно для двух
процессных обязанностей ревьюера — комментарий с вердиктом (§7.2) и отдельный
issue на Medium вне скоупа (§12); снятие потребовало бы переноса обеих
обязанностей в `integrate` — отдельная правка конвейера, справедливо не
делается в этой задаче. Записано и обосновано в коде (`process.yml:756-761`)
и в `docs/DEVELOPMENT.md`.
- **Свидетель проводки токена не полагается на порядок спреда чужого кода.**
Автор сознательно выбрал `github_token:` вместо `additional_permissions:`
именно по этой причине (сужение через `additional_permissions` зависит от
недокументированного порядка `{...DEFAULT_PERMISSIONS, ...additional}` в
чужом исходнике) — разумный, консервативный выбор.
- **Мутанты ловят именно то, что заявлено.** `review-job-trusts-the-app-token`
снимает ровно строку `github_token:` — без свидетеля это тихо возвращает
модели App-токен с `contents: write`, зелёным прогоном; поймано.
`review-model-gets-repository-write` меняет `contents: read` → `write` —
тоже поймано. Оба `--test-name-pattern` (`"job-scoped"`, `"по job"`)
проверены на уникальность совпадения по файлу тестов — каждый матчит ровно
один тест.
- **Тест `test/review-doc-guard.test.mjs` не даёт ложноположительного
совпадения.** Обновлённый тест «права по job» и новый «job-scoped» проверяют
разные, но не противоречащие друг другу свойства одного и того же блока;
61/61 зелёных сборок.
- **Трейлеры.** `e0098c8d`, `2642fdb4` — оба `Issue: #556`, `User-Visible: no`,
без изменений в changelog — верно для инфраструктурной, не User-Visible
правки.
- **YAML синтаксис** `process.yml` парсится `yaml.safe_load` без ошибок после
правки.
## Чего не проверял
- Полные `tsc`/`test`/`build` — зачтены зелёным Validate на `2642fdb4` (см.
выше), не перегонял.
- `golden:verify`, browser-смоки, `pytest tests_backend`, `model-invariants`,
performance-профили — diff не касается `src/**`, `custom_components/**/*.py`
ни геометрии; в Validate соответствующие job `skipped`.
- Реальный постмерж-прогон `model_review` под новым `process.yml` — недоступен
до слияния и зеркалирования в `main` (см. раздел выше); признак, по которому
это стоит проверить в журнале следующего запуска, зафиксирован там же.
- Серверную сторону обмена токена `api.anthropic.com/api/github/github-app-token-exchange`
— недоступна для инспекции извне; вывод не опирается на предположение о ней,
фикс обходит этот путь целиком.
## Вердикт
Находка H1 закрыта по существу и доказана: чтением кода (`github_token` →
`OVERRIDE_GITHUB_TOKEN` → обмен не происходит), тестом, который ловит обе
регрессии индивидуально прогнанными мутантами, и документацией решения.
Delta r2 не касается унаследованных из r1 частей (SHA-пины, граница
artifact→`integrate`), проверка которых сохраняет силу. Блокирующих находок
нет; два замечания автора вне скоупа уточнены/сняты выше без отдельных issue.
**Вердикт: зелёный · заход r2 · блокирующих циклов 1/4 · High: 0 · Medium: 0 → в задаче**
---
**Материал раунда:** ветка `issue/556-ci-trust-boundaries`, HEAD
`2642fdb446be65f5ebed16b9f892d6f7bd47715a` (`git rev-parse HEAD` в рабочей
копии совпадает). Предыдущий раунд: `docs/reviews/CODE-REVIEW-556-r1.md`,
материал `954eeff45aeb187f74d460d35426ffec7299ca7b`.
---
<!-- material-anchors: сгенерировано конвейером (#414) -->
## Материал раунда
- Ветка: `issue/556-ci-trust-boundaries`, коммит `2642fdb446be` — ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет.
- Дерево материала: `a1d7cf9fa60261ba39a52335e21e7287199b293a`
```
git log --all --format='%H %T' | grep a1d7cf9fa602
```
- Тело issue: `0fae31d317a2b03af2c7f61185acc7eb55eebe03858995b3549aa28aa5ff487d`
- Вердикт конвейера: `green` · High 0