docs(reviews): manual code review of the pending-round witness (#781)
Проверка (CI) / Предполёт: документация, провенанс, процесс (push) Canceled after 0s
Проверка (CI) / Классификация изменённых файлов (push) Canceled after 0s
Проверка (CI) / Переиспользование: это дерево уже проверено (push) Canceled after 0s
Проверка (CI) / HACS: валидация репозитория (push) Canceled after 0s
Проверка (CI) / Hassfest: манифест интеграции (push) Canceled after 0s
Проверка (CI) / Мутанты по диффу (1/6): затронутые свидетели краснеют (push) Canceled after 0s
Проверка (CI) / Мутанты по диффу (2/6): затронутые свидетели краснеют (push) Canceled after 0s
Проверка (CI) / Мутанты по диффу (3/6): затронутые свидетели краснеют (push) Canceled after 0s
Проверка (CI) / Мутанты по диффу (4/6): затронутые свидетели краснеют (push) Canceled after 0s
Проверка (CI) / Мутанты по диффу (5/6): затронутые свидетели краснеют (push) Canceled after 0s
Проверка (CI) / Мутанты по диффу (6/6): затронутые свидетели краснеют (push) Canceled after 0s
Проверка (CI) / Фронтенд: типы, юниты, мутанты, синхрон бандла (push) Canceled after 0s
Проверка (CI) / Бандл головы dev для стенда (push) Canceled after 0s
Проверка (CI) / Смоки в браузере (шард 1 из 3) (push) Canceled after 0s
Проверка (CI) / Смоки в браузере (шард 2 из 3) (push) Canceled after 0s
Проверка (CI) / Смоки в браузере (шард 3 из 3) (push) Canceled after 0s
Проверка (CI) / Смоки: все шарды зелёные (push) Canceled after 0s
Проверка (CI) / Golden-кадры против принятых эталонов (push) Canceled after 0s
Проверка (CI) / Перф-смок: бюджет времени кадра (push) Canceled after 0s
Проверка (CI) / Геометрия: TS/Python parity исполнена (push) Canceled after 0s
Проверка (CI) / Бэкенд: pytest в Home Assistant (push) Canceled after 0s
Проверка (CI) / Доказательство выполненных проверок (push) Canceled after 0s

Manual review requested by the owner after the automated reviewer failed.
Material e931a949: green, no High/Medium findings, one non-blocking Low.

Issue: #781
User-Visible: no
This commit is contained in:
Codex
2026-10-02 19:57:08 +03:00
committed by claude[bot]
parent 73b0cfff5b
commit 9fa5efbdac
2 changed files with 99 additions and 1 deletions
+97
View File
@@ -0,0 +1,97 @@
# CODE-REVIEW-781-r1
Вердикт: зелёный · заход r1 · блокирующих циклов 0/2 · High: 0 · Medium: 0 · Low: 1
## Материал и скоуп
- Issue: [#781](https://github.com/Matysh/houseplan-card/issues/781), `tests`, `infra`, `process`, `track:show`.
- Материал: `e931a949d2be4191ff226dc04b2a43194a178e4b`, ветка `issue/781-pending-revives-previous-request`, дерево `8d6833788659176fd46dc4b63d9a54dd419e00f2`.
- База: `9e1a2f4a0777faecbb47f391407d3d37d7d916f2`. Полный разбор одного коммита и одного файла (`test/process-pending-round.test.mjs`, +5/−1). Это не повторный раунд.
- Ревью выполнено вручную по поручению владельца вместо модели. Независимая сессия: автором реализации ревьюер не был. [Неудавшийся Process 36981454478](https://github.com/Matysh/houseplan-card/actions/runs/36981454478) полного результата не дал и цикла не образовал.
- Контракт — тело issue (отчёт ночного мутационного прогона): мутант `pending-revives-previous-request` сбежал, guard `node --test test/process-pending-round.test.mjs`. Задача восстанавливает чувствительность свидетеля. Продуктового ТЗ инфраструктура не требует. С [хендоффом](https://github.com/Matysh/houseplan-card/issues/781#issuecomment-5947740145) код сопоставлен.
Задача не меняет работу пользователя карточки (`docs/SCOPE.md`). Она возвращает доказательство границы запроса ревью: прогон, начавшийся до повторной постановки метки, не считается прогоном нового запроса (#775, PROCESS.md §10.4). Коммит класса B: `Issue: #781` и `User-Visible: no` стоят в терминальном блоке, changelog не нужен.
Перед разбором просмотрены строка #775 в `docs/reviews/INDEX.md` и `CODE-REVIEW-775-r1.md`. Его зелёный вердикт не использован вместо проверки этого диффа.
## Как проверялось
Изолированный worktree на точном SHA материала, Node 22, `git rev-parse HEAD` = `e931a949d2be`.
| Проверка | Результат |
|---|---|
| Typecheck, build, unit, синхрон бандла | Приняты по [Validate 36981115713](https://github.com/Matysh/houseplan-card/actions/runs/36981115713) (dispatch) и [36981040970](https://github.com/Matysh/houseplan-card/actions/runs/36981040970) (push) на точном `e931a949`: шаги `Typecheck`, `Build`, `Unit tests`, `Card bundle trees in sync` — `success`, а не `skipped` |
| `node --test test/process-pending-round.test.mjs` | 11/11, пропусков 0 |
| `node scripts/mutation-gate.mjs --check` | Exit 0; `ok pending-revives-previous-request`: якорь `&& at(run.createdAt) >= at(request.at))` в `scripts/process-reconcile.mjs` сходится. Общие предупреждения реестра: 4, browser guards 216/200, к диффу не относятся. Мутанты не применялись (§2.7, #709) |
| `node scripts/smoke-select.mjs --base 9e1a2f4a… --head e931a949…` | Тронут 1 файл, исполняемого frontend-диффа нет, browser-smoke выбирать нечего. Решение: браузер не запускать |
| `node scripts/process-gate.mjs --range 9e1a2f4a..HEAD` | Пройден, предупреждений 0 |
| Входной свидетель границы, без правки кода (ниже) | Граница различает «до» и «после» метки; соседнее утверждение на уровне решения — нет (L1) |
| Слияние с текущим `dev` (`13d8df55`) | `scripts/process-reconcile.mjs`, `scripts/mutation-registry.mjs` и тестовый файл между базой и `dev` не менялись; конфликтов нет. Ветка `show` к `dev` до ревью не приводится (§10.4) |
## Контракт и отрицательные свидетели
| Требование | Чем доказано | Чем краснеет |
|---|---|---|
| Прогон, начавшийся за 1 с до повторной метки, не входит в кандидаты нового запроса | Новое `assert.deepEqual(reviewRunsForRequest([ready], f.issue, justReapplied), [])` — прямой вызов фильтра, по которому кандидатов отбирают `decideReconciliation`, `snapshot` (`process-reconcile.mjs`) и `decideResume` (`process-resume.mjs:62`) | Мутант реестра `pending-revives-previous-request`: окно `>= at(request.at) - 120_000` возвращает `[ready]` вместо `[]` |
Без применения мутанта чувствительность проверена входом. Тот же прогон подан с запросом, сдвинутым на 120 с назад, — это ровно окно мутанта, код не правился.
```
boundary (code): 0 кандидатов
mutant-equivalent window via input: 1 кандидат
```
Утверждение `deepEqual(..., [])` различает эти случаи. Мутант на этом входе даёт `[ready]`, и тест краснеет.
Почему мутант сбежал. В #775 (`09fb02cf`) свидетелем было только утверждение на уровне решения: `decideReconciliation(... now = момент метки ...)` → `wait`. Это `wait` получается при обоих вариантах кода, по разным причинам:
```
decide now=req, correct: label event is still within delivery grace
decide now=req, run treated as matching: completed run is still within label-application grace
```
Ветка «завершённый прогон ещё в grace» существует с #555, поэтому утверждение было нечувствительно к границе с самого появления мутанта. Новая строка прямо закрывает эту причину.
## Что проверено и корректно
1. Исправление попадает в причину, а не в симптом. Граница запроса живёт в одной функции `reviewRunsForRequest`, мутант патчит именно её, и тест теперь судит её результат. Ослаблять реестр или переносить мутант не понадобилось.
2. Импорт `reviewRunsForRequest` берётся из того же модуля, что и `decideReconciliation`, — это не копия логики в тесте. Экспорт в `process-reconcile.mjs` уже существовал, поэтому продуктовый и скриптовый код не тронут, что подтверждает `--stat` (1 файл, только тест).
3. Фикстура — реальная идентичность инцидента #740 (`run 36877086418`, `14:32:05Z`), запрос `14:32:06Z`. Сдвиг в 1 с — самый жёсткий случай границы: он отделяет `>=` от сколько-нибудь широкого окна. Обратный случай «прогон в момент метки входит» уже держат соседние тесты, где `request.at === run.createdAt` и решение `resume`/`retry`.
4. `track:show` обоснован. Меняется один тест, новых поверхностей, миграций, UX, геометрии и perf-касаний нет. Маршрут `fix`, оснований для `reclassify` нет.
5. «Одно число — один источник»: новых чисел дифф не вводит. Литерал `14:32:06Z` был в тесте раньше и связан с фикстурой (+1 с); новая строка переиспользует ту же переменную `justReapplied`.
## Находки
High: 0. Medium: 0. Low: 1.
### L1 — утверждение на уровне решения по-прежнему не различает границу
`test/process-pending-round.test.mjs:166–167`: `decideReconciliation({ ..., now: Date.parse(justReapplied.at), runs: [ready] }).action === 'wait'`. Сообщение утверждает «cannot be recovered as the new request», но при `now`, равном моменту метки, оба варианта кода дают `wait` (см. выше). Если сдвинуть `now` за grace, они снова совпадают: `ready` несёт `pending` с завершённым Validate, а это `retry` в обоих случаях. Различает только прогон с `conclusion: 'failure'` после grace: правильный код даёт `retry`, а окно мутанта — `escalate`.
Утверждение безвредно и после этой задачи не является единственным свидетелем. Улучшить его можно двумя способами: сравнивать `reason` или взять failure-прогон после grace. На треке `show` нечувствительный тест на побочный вызов — Low (§10.4). Снимаю с блокировки с записью, цикла не открывает. Код в рамках ревью не правился.
## Чего не проверял
- Поимку мутанта исполнением: по процессу ревьюер мутанты не применяет (§2.7, #709). Чувствительность показана входом с окном мутанта и подтвердится ночным прогоном. Автор заявляет `mutation-gate --id=pending-revives-previous-request` → пойман 1/1, повторно это не исполнялось.
- Полный `npm test`, typecheck и build локально: приняты по зелёному Validate на точном SHA (см. таблицу).
- Golden, браузер, HA harness, инварианты геометрии, performance: продуктового диффа и соответствующих меток или AC нет.
- Остальные свидетели `process-pending-round` и `process-resume` на скрытую нечувствительность целиком: проверены только утверждения, касающиеся этого мутанта.
- Слияние в dev и перевод в S8 этим поручением не выполнялись.
## Итог
Зелёный, `route: fix`. Свидетель снова доказывает границу запроса прямым вызовом фильтра, причина побега установлена и закрыта. L1 снят с записью. Перед итогом `git rev-parse HEAD` в worktree материала и `git ls-remote` ветки подтвердили неизменный `e931a949d2be`. Собственный последующий docs-only коммит с этим документом новым материалом реализации не считается.
---
<!-- material-anchors: сгенерировано конвейером (#414) -->
## Материал раунда
- Ветка: `issue/781-pending-revives-previous-request`, коммит `e931a949d2be` — ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет.
- Дерево материала: `8d6833788659176fd46dc4b63d9a54dd419e00f2`
```
git log --all --format='%H %T' | grep 8d6833788659
```
- Тело issue: `cb771dcfda0f023231529cc79970ae4838359a1e4c993c5b30c1ea72fac5f3ef`
- Вердикт конвейера: `green` · High 0 · маршрут `fix`
+2 -1
View File
@@ -1,11 +1,12 @@
# Индекс ревью
Генерируется `node scripts/reviews-index.mjs` (#635) — не редактировать руками. Документов: 285, issue: 141. Вердикт: 🟢 зелёный · 🟡 жёлтый · 🔴 красный · ⚪ не распознан (свободная форма старых документов). H/M — число High/Medium по строке вердикта или заголовкам находок. Файлы — пути, названные в находках; ищите по имени файла: `grep form-kit INDEX.md`.
Генерируется `node scripts/reviews-index.mjs` (#635) — не редактировать руками. Документов: 286, issue: 142. Вердикт: 🟢 зелёный · 🟡 жёлтый · 🔴 красный · ⚪ не распознан (свободная форма старых документов). H/M — число High/Medium по строке вердикта или заголовкам находок. Файлы — пути, названные в находках; ищите по имени файла: `grep form-kit INDEX.md`.
| Issue | Документ | Этап · раунд | Вердикт | H | M | Находки | Файлы |
|---|---|---|---|---:|---:|---|---|
| бета v1.79.0-beta.2 | [SHIP-REVIEW-v1.79.0-beta.2.md](SHIP-REVIEW-v1.79.0-beta.2.md) | пакетное ревью ship · — | ⚪ — | 0 | 0 | — | — |
| бета v1.79.0-beta.1 | [SHIP-REVIEW-v1.79.0-beta.1.md](SHIP-REVIEW-v1.79.0-beta.1.md) | пакетное ревью ship · — | ⚪ — | 0 | 0 | — | — |
| #781 | [CODE-REVIEW-781-r1.md](CODE-REVIEW-781-r1.md) | code · r1 | 🟢 зелёный | 0 | 0 | утверждение на уровне решения по-прежнему не различает границу | `test/process-pending-round.test.mjs` |
| #780 | [SPEC-REVIEW-780-r1.md](SPEC-REVIEW-780-r1.md) | spec · r1 | 🟡 жёлтый | 0 | 5 | · Medium — в редакторе устройств нет «существующего контекстного лотка» и модели выделения; · Medium — поведение бэкенда на висящую ссылку не определено и ломает сохранение «старо…; · Medium — запрет поднимать бюджеты исполним только ленивой загрузкой, а ТЗ её не требует; · Medium — смещение от грани не определено для стен нулевой толщины и смешанных лент; · Medium — AC17 не проверяем: нет порогов; · Low — D назван «диаметром устройства пространства», а такой величины нет | `validation.py` `scripts/bundle-budget.mjs` `types.ts` `houseplan-card.ts` |
| #780 | [SPEC-REVIEW-780-r2.md](SPEC-REVIEW-780-r2.md) | spec · r2 | 🟢 зелёный | 0 | 0 | — | — |
| #775 | [CODE-REVIEW-775-r1.md](CODE-REVIEW-775-r1.md) | code · r1 | 🟢 зелёный | 0 | 0 | — | — |