docs: review document for #705

Issue: #705
User-Visible: no
This commit is contained in:
claude[bot]
2026-09-30 20:25:08 +00:00
parent e5c217111c
commit 6e2c75afc9
+192
View File
@@ -0,0 +1,192 @@
# CODE-REVIEW-705-r1
## Материал раунда
- Issue: #705 · трек `show` (инфраструктура, вход сразу в `S7-code-review`)
- SHA материала: `c17c05341bc503f1cf2aa85684a23c0f6afeaeeb` (рабочая копия уже на нём)
- `git log --oneline origin/dev..HEAD`: один коммит —
`c17c0534 fix(process): a GitHub push refusal is not a stale lease (#705)`
- Ветка к `dev` не приводилась (трек `show`, #696): `dev` впереди на 10
коммитов, слияние чистое — это подтверждено конвейером и не находка.
- Validate на этом SHA зелёный: https://github.com/Matysh/houseplan-card/actions/runs/36771211279
(`npx tsc --noEmit`, `npm test`, `npm run build` + сверка копий бандла на
этом прогоне не перегонялись — приняты по ссылке).
## Скоуп
Задача чинит инцидент #700: `pushWithLease`/`deleteBranch` в
`scripts/merge-candidate.mjs` считали устаревшим lease любой push-отказ,
содержащий слово `rejected`, включая `! [remote rejected]` — отказ самого
GitHub (например, кандидату, меняющему `.github/workflows/`, от токена без
права `workflow`). Диффу соответствуют три файла кода
(`scripts/merge-candidate.mjs`, `.github/workflows/_process.yml`,
`scripts/mutation-registry.mjs`) и два тестовых (`test/merge-candidate.test.mjs`,
`test/rebase-generated.test.mjs`), плюс абзац в `PROCESS.md` §4.2. Работа
инфраструктурная (класс B), под J-строку `docs/SCOPE.md` не подпадает —
верно, инфраструктура конвейера ревью в скоуп продукта не входит и
рецензируется по `AGENTS.md`/`PROCESS.md`, а не по `docs/SCOPE.md`.
## Как проверялось
Прочитан целиком дифф `git diff origin/dev...HEAD` (все 6 файлов), тело
issue #705 и единственный комментарий автора («Взял и сделал»). Код разобран
построчно: `classifyPushRefusal`, `redactSecrets`, `PushRefusal`,
`decideMerge` с новым полем `s.refused`, обёртка `mergeCandidate` →
`mergeAttempts`, изменения в `realOps.pushWithLease`/`deleteBranch`, новый
CLI-режим `--push-refusal=` (`describePushRefusal`/`pushRefusalMain`), правки
`.github/workflows/_process.yml` (шаг «Привести ветку к dev» и новый шаг
возврата автору), запись в `scripts/mutation-registry.mjs`, абзац
`PROCESS.md` §4.2.
Гейты, прогнанные лично в этом раунде:
| Гейт | Команда | Результат |
|---|---|---|
| Целевые unit-тесты диффа | `node --test test/merge-candidate.test.mjs test/rebase-generated.test.mjs` | 52/52 зелёные |
| Регресс канона ревью | `node --test test/process-digests.test.mjs` | 5/5 зелёные |
| Реестр мутантов синтаксически цел | `node scripts/mutation-registry-check.mjs` | exit 0, без вывода |
| Отбор браузерных смоков | `node scripts/smoke-select.mjs --base origin/dev --head HEAD` | «Исполняемого frontend-диффа нет … смоки не выбираются» — не гейт этой задачи |
| Защитный AC3 — мутант `push-refusal-kinds-glued` | вручную применил патч мутанта из реестра к `scripts/merge-candidate.mjs`, прогнал `node --test --test-name-pattern="#705 AC1" test/merge-candidate.test.mjs`, затем восстановил файл из `git diff`/резервной копии | **красный** — 2 из 3 тестов паттерна `#705 AC1` падают (`git push dev отклонён — stale:` вместо `stale (stale info):`), после восстановления файла — снова 52/52 зелёные |
Не прогонялось и почему: `npx tsc --noEmit`, `npm test` (полный), `npm run build`
+ сверка бандла — уже зелёные на этом SHA в Validate (ссылка выше), диффу они
не нужны повторно (#343). `golden:verify` — нет метки `ci:golden` и диффа в
`src/**`/`demo/**`. `pytest tests_backend` — Python не тронут. Инварианты
модели (`npm run invariants`) — геометрия не тронута. Performance-профили — в
AC не названы. Полный `mutation-gate --check` (весь реестр) — не гейт ревью,
мутанты в разработке не гоняются (#709), ночной прогон отдельно; проверен
только точечно мутант этой задачи, как того требует защитный AC.
## §8: число, видимое пользователю
Диффу не принадлежит ни одна продуктовая/пользовательская величина — правка
инфраструктурная, `User-Visible: no` (см. ниже). В коде есть два совпадающих
магических литерала `1500`: новый `PUSH_STDERR_LIMIT` (обрезка stderr для
классификации отказа) и уже существовавшая до #705 `.slice(0, 1500)` в
generic-обработчике `#492` (сбой шага слияния) — они относятся к разным
путям и разным текстам, случайное совпадение значения, не общий источник
факта. Это не дублирование пользовательского числа в смысле §8 (не то же
самое значение, показанное дважды из двух вычислений), поэтому не отмечаю
находкой — но называю прямо, раз диффом введён второй такой литерал.
## Находки
Нет. High: 0, Medium: 0, Low: 0.
## Что проверено и корректно
- **AC1.** `classifyPushRefusal` (scripts/merge-candidate.mjs:102-122)
различает три исхода по тексту stderr:
- устаревший lease — `! [rejected] … (stale info|fetch first|non-fast-forward)`
и гонка lease на сервере (`cannot lock ref … but expected`,
`incorrect old value provided`) — трактуется как тот же stale;
- отказ по праву на workflow — регэксп `WORKFLOW_REFUSAL` покрывает classic
PAT, fine-grained PAT, OAuth App, GitHub App/`GITHUB_TOKEN`, прежние формы
`bot`/`integration` — проверено тестом на каждую формулировку
(`test/merge-candidate.test.mjs`, блок `WORKFLOW_REFUSALS`);
- прочий `[remote rejected]` — pre-receive/protected-branch хуки и подобное.
`redactSecrets` вырезает userinfo в URL, `ghp_/gho_/ghu_/ghs_/ghr_/github_pat_`,
заголовок Authorization и явно переданные секреты (`split/join` до regex —
защита от новых форматов токенов) — проверено чтением и тестом с
комбинацией всех типов секретов в одном тексте, включая частичное
совпадение внутри URL. stderr печатается в журнал (`log()` внутри
`refusedPush`) — раньше не печатался вовсе (сам баг #700).
- **AC2.** `decideMerge` получил `s.refused` и отображает его в
`push-refused-workflow`/`push-refused` **до** проверки `conflict` —
корректно: отказ GitHub — не «ветка изменилась» (#312) и не конфликт
ребейза. `mergeCandidate` — новая тонкая обёртка над переименованным
`mergeAttempts`: ловит `PushRefusal`, брошенный из `realOps.pushWithLease`
на **любом** из трёх мест, где он вызывается (fast-forward в `dev`, публикация
кандидата в ветку задачи, финальный push кандидата в `dev`), и вместо
падения шага комментирует issue и возвращает `S6-in-progress`, не выполняя
`deleteBranch` — веткa не помечается влитой, потому что не влита. Прочертил
все три места вызова `pushWithLease` в `mergeAttempts` (строки ~440, ~456,
~483) — исключение из любого перехватывается одним и тем же catch.
Комментарий `push-refused-workflow` содержит дословную фразу ТЗ («кандидат
меняет workflow-файл, токен конвейера не может его опубликовать: ребейз и
push делает автор, либо владелец выдаёт право»), имя файла и ответ GitHub —
подтверждено тестами AC2 построчным `assert.match`.
Страж ребейза в `.github/workflows/_process.yml` (шаг «Привести ветку к
dev») разбирает stderr push **тем же кодом** —
`node scripts/merge-candidate.mjs --push-refusal=…` — а не своим regex;
новый шаг «Push ребейза отклонён по праву на workflow» переводит задачу в
`S6-in-progress` с комментарием и не расходует цикл ревью (код не читался).
Шаги «Зафиксировать SHA материала», «Зелёный вердикт применим без ревью» и
«Validate на материале» корректно пропускаются доп. условием
`steps.rebase.outputs.refused == ''` — проверено и юнит-тестом
(`test/rebase-generated.test.mjs`, разбор текста workflow), и
bash-исполнением реального фрагмента шага (`runStepPush`) с фейковым `git`,
отвечающим нужным stderr — воспроизводит фактическое поведение раннера, а
не только текст скрипта.
- **AC3.** Мутант `push-refusal-kinds-glued` (реестр,
`scripts/mutation-registry.mjs`) возвращает старую склеенную регулярку
`/stale info|rejected|fetch first|lease/i` **перед** веткой workflow — я
применил этот патч руками к рабочей копии, прогнал guard-тест
(`--test-name-pattern="#705 AC1"`) и получил красный результат (2 из 3
тестов падают, ассерты видят `kind=stale` там, где ожидался `workflow`),
затем восстановил файл. Мутация действительно ловится названным guard'ом —
«умеет падать» подтверждено исполнением, не заявлением автора.
- Регресс на существующий разбор: удаление влитой ветки (`deleteBranch`)
по-прежнему возвращает `false` только на устаревшем lease; отказ GitHub при
удалении ветки (`PUSH_REFUSAL.remote`/`workflow`) теперь бросает ошибку с
текстом причины вместо молчаливого `false` — проверено тестом
(`assert.throws(...).../remote-rejected/`).
- Тест на настоящем git с `pre-receive`-хуком (без сети, `bare`-репозиторий в
tmp) подтверждает, что классификация работает не только на сконструированных
строках stderr, а на реальном ответе git/сервера, и что `dev` не трогается
при отказе (`ls-remote` после отказа = `base`).
- Трейлеры коммита: `Issue: #705`, `User-Visible: no` — верно, правка
внутренняя, оба CHANGELOG не нужны.
- `test/process-digests.test.mjs` (сверка `REVIEWER.md` с `PROCESS.md`)
зелёный — новый абзац `PROCESS.md` §4.2 не ломает сверку выжимки.
## Риски, названные автором и подтверждённые как честные
Автор прямо пишет: «точный текст отказа именно от `HP_PROCESS_TOKEN` не
виден — логи #700 закрыты. Тексты взяты из публичных отчётов GitHub».
Согласен, что это не устранимо в рамках этой задачи: реальный текст отказа
для конкретно этого приложения/токена нельзя воспроизвести без доступа к уже
закрытым логам. Деградация безопасна: нераспознанный текст отказа падает в
`unknown` (сбой шага, как раньше, ничего не скрывается) или в generic
`remote-rejected` (комментарий с причиной, без ложного «ветка изменилась»).
Не нахожу это блокирующим — риск честно раскрыт, а поведение при ошибке
классификации не хуже прежнего.
Отдельно: сама эта задача меняет `.github/workflows/_process.yml`, то есть
её собственное слияние в `dev` — ровно тот случай, который она чинит. Если
владелец ещё не выдал `HP_PROCESS_TOKEN` право на workflow (пункт 1
предложения в ТЗ, явно вынесенный за скоуп кода), слияние этого кандидата
само продемонстрирует новый путь `push-refused-workflow` вместо ложного
#312 — это ожидаемое поведение, а не дефект, и не входит в код-ревью.
## Чего не проверял
- Полный `npm run gate:small`/CI-раннер заново — принят по зелёному Validate
на этом SHA (ссылка выше, #343).
- Реальный, боевой отказ GitHub с токеном `HP_PROCESS_TOKEN` — недоступен
(логи #700 закрыты); классификация проверена на текстах из публичной
документации GitHub, а не на воспроизведённом инциденте.
- Полный ночной прогон реестра мутантов — не гейт ревью на `show` (#709),
прогнан точечно только мутант этой задачи.
- Golden/perf/HA pytest/invariants — диффу не соответствуют (обоснование в
таблице гейтов выше).
## Вердикт
Зелёный. AC1–AC3 выполнены и доказаны исполнением (unit-тесты, реальный git
с pre-receive-хуком, ручная проверка мутанта на красный/зелёный), находок
нет.
---
<!-- material-anchors: сгенерировано конвейером (#414) -->
## Материал раунда
- Ветка: `issue/705-push-refusal-kinds`, коммит `c17c05341bc5` — ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет.
- Дерево материала: `43220b1aa1940537b1d101e7a3e377989273a6ad`
```
git log --all --format='%H %T' | grep 43220b1aa194
```
- Тело issue: `4e6ce37d10900c768172f1d382518d679447b0d0207891176c66cd833c344ae7`
- Вердикт конвейера: `green` · High 0