Files
houseplan-card/docs/reviews/CODE-REVIEW-546-r1.md
T
2026-09-13 07:35:27 +00:00

141 lines
13 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.
# CODE-REVIEW-546-r1
Issue: #546 · Заход: r1 · Материал: `5fc596c748c59e53d87297085a6b053c7f149c6e` (HEAD ветки `issue/546-wait-round`, `git diff origin/dev...HEAD`)
## Скоуп
Инфраструктурная задача (класс A файлов не затронут — трек §1 корректен, без S1…S6,
сразу на `S7-code-review`). Правит `scripts/wait-verdict.mjs`: waiter больше не
считает *любой* последний pipeline-комментарий issue новым событием текущего
раунда. Вводится якорь раунда — последнее фактическое применение метки
`S4-spec-review`/`S7-code-review`, взятое из timeline issue
(`GET /repos/{repo}/issues/{number}/events`, постранично). Комментарии старше
этого якоря — baseline и не завершают ожидание; комментарии не раньше якоря
(включая уже опубликованные до старта waiter) доставляются как раньше.
Изменённые файлы: `scripts/wait-verdict.mjs`, `scripts/mutation-gate.mjs`,
`test/wait-verdict.test.mjs`, `AGENTS.md`, `PROCESS.md`. Один коммит,
трейлеры `Issue: #546` / `User-Visible: no` на месте — корректно: правится
только внутренний dev-tooling, пользовательского поведения нет, changelog не
требуется.
## Как проверялось
Validate на этом SHA зелёный ([run 34745280749](https://github.com/Matysh/houseplan-card/actions/runs/34745280749)),
поэтому `npx tsc --noEmit` / `npm test` / `npm run build` со сверкой копий
бандла не перегонялись — взят как подтверждение дешёвых гейтов. Diff не
трогает `src/**`, геометрию, `custom_components/**/*.py` и визуальный
результат, поэтому `check-docs.mjs`, `model-invariants.mjs`, `golden:verify`,
`pytest tests_backend` и браузерные смоки не применимы (смок-выбор не
запускал — diff вне `demo/**`/`src/**`, `smoke-select.mjs` не даёт кандидатов
по такому диффу).
Прогнано мной в этом раунде (код изменился со времени прогона в хендоффе,
плюс это ровно предмет находок предыдущих раундов #546-подобных задач —
дёшево и целевое):
| Команда | Результат |
|---|---|
| `node --test test/wait-verdict.test.mjs` | 8/8 green |
| `node --test --test-name-pattern="#546" test/wait-verdict.test.mjs` | 4/4 green (целевые тесты задачи) |
| `node scripts/mutation-gate.mjs --check` | зелёный, весь реестр, включая новый мутант |
| `node scripts/process-gate.mjs` | «гейт пройден, предупреждений 0» |
Не прогонял: `npm run gate:small` целиком, `npm run toolchain:check`,
performance-профили — не относятся к диффу (нет продуктового/фронтенд кода,
нет perf-чувствительных путей), и Validate на этом SHA их уже подтвердил там,
где они применимы.
## AC — доказательство и «чем краснеет»
| AC (из тела issue) | Чем доказан | Чем краснеет |
|---|---|---|
| Старый failure + новый running review: ожидание продолжается без ложного выхода | `test/wait-verdict.test.mjs`: «старый failure до нового запроса ревью — baseline, ожидание продолжается (#546)» | мутант `wait-verdict-reuses-historical-failure` (`scripts/mutation-gate.mjs`): снимает фильтр `eventBelongsToReview` — `node scripts/mutation-gate.mjs --check` ловит (строка `ok wait-verdict-reuses-historical-failure`; проверил также прямым запуском guard-команды `node --test --test-name-pattern="#546" test/wait-verdict.test.mjs` с патчем, гейт-скрипт сам применяет и откатывает патч и подтверждает провал под мутацией) |
| Текущий failure уже опубликован до начала wait: он обнаруживается сразу | «failure текущего раунда, опубликованный до запуска waiter, виден на первом poll (#546)» | тот же мутант — фильтр общий для обеих веток (baseline и current) |
| Новый verdict, отмена/смена material и owner-blocker различаются; не игнорировать все первоначальные comments | «новые outcome и owner blocker текущего раунда не скрываются baseline-фильтром (#546)» — прямое сравнение ожидаемого/фактического, не требует мутанта (ветвление не защитное, а обычная маршрутизация событий, уже покрытая исходным #496-набором тестов) | н/п — не защитный AC в терминах §2.7 (правило прямо выводит из-под мутанта «расположение/текст/формат», сюда же относится «какой code возвращается по какому типу события»); свидетель — сравнение |
| Тесты работают детерминированно без реального длительного sleep и без модели | проверено чтением: `sleep` — инъецируемая функция-счётчик, `readSnapshot` — синтетический замыкающий объект без сети; исполнением подтверждено временем прогона (`duration_ms` < 100 мс на всю группу #546) | н/п, не защитный AC |
Якорь раунда (`reviewRequestFromEvents`) проверен отдельно тестом «якорь
раунда — последнее применение любой review-метки (#546)»: смешанный поток
`labeled S4 → labeled P1 → unlabeled S4 → labeled S7` корректно даёт
последним именно `S7`-событие. Мутанта на этот отбор нет, но неверный выбор
анкера транзитивно ломает оба #546-теста через общий путь `stateOf` →
`eventBelongsToReview`, поэтому и здесь есть живой свидетель регрессии.
## Что проверено и корректно
- `ghSnapshotReader` использует `gh api --paginate --slurp
repos/{repo}/issues/{number}/events?per_page=100` и правильно разворачивает
результат: `--slurp` оборачивает каждую страницу (сама по себе массив) во
внешний массив, поэтому `pages` — массив массивов, и `pages.flat()`
корректен для одной и для многих страниц; пустая история (`pages = [[]]`
или `[]`) сводится к `timelineEvents = []`, а `reviewRequestFromEvents([])`
возвращает `null` → `eventBelongsToReview` откатывается к старому
поведению «всё принадлежит раунду» (совместимость со снимками без
timeline, в том числе с прямыми вызовами `snap()` в тестах 1–4, не
передающими `reviewRequest`) — прочитано и подтверждено прогоном тех же
тестов (не регрессировали).
- Формат события REST `/issues/{number}/events` (`event: 'labeled'`,
`created_at`, `label: {name}`) соответствует и коду, и фикстурам тестов —
прочитано, совпадает с задокументированной схемой GitHub REST API.
- `blocked` и смена метки-вердикта не проходят через
`eventBelongsToReview` вовсе (читаются из `labels`, не из `comments`),
поэтому сужение к текущему раунду не может случайно скрыть их — прочитано
в `decide`/`stateOf`, подтверждено тестом «новые outcome и owner blocker…».
- Документация (`AGENTS.md`, `PROCESS.md`) обновлена в том же коммите и
точно описывает новое поведение (baseline до последнего `S4`/`S7`, немедленная
доставка уже случившегося исхода текущего раунда) — соответствует §2.6/§11
DoD.
- Трейлеры коммита корректны (`Issue: #546`, `User-Visible: no`); ветка
`issue/546-wait-round` соответствует конвенции; `process-gate.mjs` зелёный
локально.
## Чего не проверял
- Реальный вызов `gh api .../events` против живого GitHub (сетевой путь
`ghSnapshotReader`) — не покрыт автотестом (как и раньше не был, для
соседнего блока `gh run list` в этом же файле — не новый пробел, а
существующий паттерн). Автор заявляет ручной read-only прогон на issue с
длинной историей; принимаю как «проверено чтением + разовая ручная
проверка автора», не как автотест. Риск низкий: контракт REST-эндпоинта
стабилен, фикстуры тестов повторяют его форму дословно.
- Тай-брейк `id.localeCompare` в `reviewRequestFromEvents` при **совпадающих
секундных** таймстемпах двух labeled-событий сравнивает числовые id как
строки (лексикографически, не численно) — теоretически может выбрать не
тот из двух событий при коллизии. Не тестировалось и не мутировалось.
Вероятность на практике пренебрежимо мала (два применения `S4`/`S7` в один
и тот же issue в одну и ту же секунду) и не относится к заявленным AC —
фиксирую как Low, не блокирует, правку не считаю обязательной.
- Полный `npm run gate:small`, perf-профили, golden, backend pytest — не
прогонялись, diff их не касается (см. «Как проверялось»).
## Находки
Нет High и Medium в скоупе. Одна Low-наблюдение (тай-брейк по id при
одинаковых секундных таймстемпах, см. выше) — не блокирует зелёный вердикт,
оставляю как запись без правки: цена коллизии ничтожна и вне AC задачи.
## Вывод
Все четыре AC доказаны детерминированными тестами; защитный AC (baseline
против ложного немедленного выхода / против маскировки текущего отказа)
дополнительно закрыт мутантом `wait-verdict-reuses-historical-failure`,
который я лично прогнал и подтвердил, что он ловит снятие фильтра. Гейты,
относящиеся к диффу, зелёные. Документация и трейлеры в порядке.
**Вердикт: зелёный**
---
<!-- material-anchors: сгенерировано конвейером (#414) -->
## Материал раунда
- Ветка: `issue/546-wait-round`, коммит `5fc596c748c5` — ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет.
- Дерево материала: `2350e7169f6f98b97900e4aabaebd5ea47dfe492`
```
git log --all --format='%H %T' | grep 2350e7169f6f
```
- Тело issue: `0968fbd2f409d44f80911b445838422e0563d955aa08ab72109bdec5253ca3d5`
- Вердикт конвейера: `green` · High 0