Files
houseplan-card/docs/reviews/SPEC-REVIEW-510-r3.md
T
2026-09-09 15:14:20 +00:00

103 lines
22 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-510-r3
- **Issue:** https://github.com/Matysh/houseplan-card/issues/510
- **ТЗ:** `docs/specs/510-mutants-on-candidate-and-review-waits-validate.md`
- **Этап:** spec (S4-spec-review) · заход r3 · блокирующих циклов израсходовано 2/4 до этого раунда
- **Материал:** ТЗ на HEAD рабочей копии (`3854fe72ac59b1e37a80b25faea0f8468356597a`), дельта против материала r2 (`5fc235c0e8de`)
- **Предыдущий раунд:** SPEC-REVIEW-510-r2, вердикт жёлтый, SHA материала `5fc235c0e8de`, High 0 · Medium 1 · Low 0
## Скоуп ревью
Разбор — по дельте (PROCESS.md §2.9/§2.10, issue #214): дельта локальна — правка того же ТЗ, той же ветки, без ребейза и без смены подсистемы, объём (3 предложения в одном разделе, `git diff --stat` — 4 вставки/4 удаления в одном файле) на порядок меньше исходной задачи. Но, как и в r2, дельта сама переписывает межшаговую проводку `process.yml` (§5.2) — контракт, по которому ветвятся все шаги ревью после `gate`. Поэтому разбор по дельте здесь не сводится к чтению трёх изменённых строк текста: нужно (1) закрыть находку r2 построчно, как и предписано форматом, и (2) проверить новую формулировку контракта против реального `process.yml` и существующих тестов — именно так r2 нашла свою находку не в новом тексте, а в столкновении нового текста со старым. Разделы, которых дельта не касается текстуально и чей контракт не меняется этой правкой (§1–§4, §5.1, §6, §7, §8 кроме связки со строкой 72, §9, §10.0, §10.1, §11, §12), не перепроверялись — унаследованы (см. ниже).
## Как проверялось
- `git diff 5fc235c0..3854fe72 -- docs/specs/510-mutants-on-candidate-and-review-waits-validate.md` — вся дельта, один хунк из 3 изменённых предложений в §5.2 (строки 69, 71, 72; строка 73 — только добавление «на этапе code», не меняющее её смысл).
- `git show 3854fe72` — сообщение коммита прочитано целиком, а не только дифф: «All step conditions now branch on `proceed` only (true = green or skipped, false = red/missing)» — это явная формулировка проектного решения, а не побочный эффект правки формулировок, и именно она проверялась.
- Находка r2 (единственная) сверена построчно с новым текстом (таблица ниже).
- Реальный `process.yml` (`grep -n "steps.reuse.outputs.reuse\|id: reuse\|id: gate\|Установить зависимости\|name: Review"`) — нынешние условия шагов «Установить зависимости», кэш/установка Chromium (пять мест) и шага `Review` (вызывающего модель) — везде `steps.rebase.outputs.conflict != 'true' && steps.reuse.outputs.reuse != 'true'`, не только `rebase.conflict != 'true'`, как упрощённо называет это ТЗ.
- `test/review-doc-guard.test.mjs:592-603` — существующий (не из этой задачи) тест «конвейер: зелёный вердикт применяется повторно без модели» жёстко требует `assert.match(reviewStep, /steps\.reuse\.outputs\.reuse != 'true'/, 'модель не вызывается при повторном применении')`. Этот тест в §11 ТЗ №510 назван как затрагиваемый файл, но ни разу не упомянут в §8 (плане тестов) как изменяемый или заменяемый.
- Перечитан весь фрагмент §5.2 целиком (строки 69–76), а не только изменённые предложения по отдельности — так же, как это делала r2 при поиске своей находки.
## Закрытие раунда r2
| Находка r2 | Чем закрыта | Где это видно |
|---|---|---|
| Medium 1 — три соседних предложения называли условие продолжения/возврата тремя разными именами (`result`/`proceed`), и буквальное `result != 'green'` для красного возврата (строка 71 r2) давало бы паразитный `S7→S6` на skip-ветке (spec/reuse) | **Частично.** Красный-возврат (строка 71) переписан на `steps.rebase.outputs.conflict != 'true' && steps.gate.outputs.proceed != 'true'` — на skip-ветке `proceed == 'true'`, шаг не срабатывает, паразитный возврат устранён. Формально устранена и текстовая нестыковка трёх формулировок: строка 69, строка 71 и строка 72 теперь одинаково используют `proceed`, а не смесь `result`/`proceed`. | `docs/specs/510-*.md:71` (красный-возврат — исправлено верно); `:69,72` (унификация терминов — сделана) |
| — та же находка, часть про строку 72 («все последующие шаги ревью») | **Не закрыта, закрыта неверно.** r2 явно предлагала для этой группы **оставить** `result == 'green'` буквально, потому что она (в отличие от красного-возврата) обязана оставаться заблокированной на skip-ветке — reuse обязан коротко замкнуть без реального ревью. r3 вместо этого заменила условие всей группы (`validated`, зависимости, Chromium, Claude, Review, публикация, решение, слияние, перестановка метки) на `proceed == 'true'`, а `proceed == 'true'` истинно и на skip-ветке (reuse) по определению той же строки 69 («`true` = green **или** skipped»). Это не formальность: см. находку ниже — это открывает шаг `Review` (вызов модели) и установку зависимостей/Chromium на reuse-ветке, где сейчас они не выполняются вовсе. | `docs/specs/510-*.md:72` (новая формулировка); противоречит реальному `process.yml` (условия шагов «Установить зависимости», кэш/установка Chromium, `Review`) и `test/review-doc-guard.test.mjs:596` |
Находка r2 закрыта только в своей «текстовой» части (три формулировки сведены к одному слову) — но одна из трёх сведена к неверному значению. Синтаксическая цель ревью r2 («одна переменная — одно условие») достигнута; содержательная («группа „продолжение ревью“ не должна срабатывать на skip-ветке») — нарушена этим же изменением.
## Находки
### Medium (в скоупе) — 1: унификация «всё через `proceed`» ломает reuse-fast-path (#499) — деплой-зависимости, Chromium и вызов модели получили бы условие, истинное и на ветке, где их сейчас не выполняют вовсе
**Файл:** `docs/specs/510-mutants-on-candidate-and-review-waits-validate.md`, §5.2, строки 69 и 72.
Строка 69 вводит абсолютное утверждение: «Единственная переменная, по которой ветвятся последующие шаги, — `proceed`». Строка 72 применяет это к группе «все последующие шаги ревью (`validated`, зависимости, Chromium, Claude, Review, публикация, решение, слияние, перестановка метки)» — их условие меняется на `steps.gate.outputs.proceed == 'true'` **вместо** нынешнего `steps.rebase.outputs.conflict != 'true'`.
Но нынешнее условие этих конкретных шагов в реальном `process.yml` — не `rebase.conflict != 'true'` в одиночку, а `steps.rebase.outputs.conflict != 'true' && steps.reuse.outputs.reuse != 'true'` (проверено чтением файла: «Установить зависимости», пять мест кэша/установки Chromium, шаг `Review`, вызывающий модель, — везде оба условия). ТЗ называет только первый конъюнкт как заменяемый и ни словом не упоминает второй — а коммит r3 («All step conditions now branch on `proceed` only») прямо формулирует замену как тотальную, не частичную.
Раз `proceed` по той же строке 69 истинен и на skip-ветке (`stage != code`, `reuse == true` или нет ветки — «`true` = green **или** skipped»), буквальная реализация строки 72 даёт: на reuse fast-path (`reuse == true`) условие `proceed == 'true'` истинно → шаги «Установить зависимости», кэш/установка Chromium и `Review` (вызов модели) **выполнятся**, хотя сегодня они не выполняются вовсе — в этом и была точка #499: «вердикт прошлого захода применяется повторно: модель не вызывается» (комментарий в `process.yml` перед шагом `reuse`). Конкретное воспроизведение: `test/review-doc-guard.test.mjs:592-603`, тест «конвейер: зелёный вердикт применяется повторно без модели», жёстко требует `steps.reuse.outputs.reuse != 'true'` именно в условии шага `Review` — при реализации §5.2 буквально по тексту r3 этот существующий тест либо ломается, либо реализация должна тихо отступить от буквального текста ТЗ и сохранить `&& steps.reuse.outputs.reuse != 'true'` вопреки заявленному «единственная переменная — `proceed`». Ни то, ни другое ТЗ не называет явно; `test/review-doc-guard.test.mjs` при этом перечислен в §11 как затрагиваемый файл, но конкретно эта проверка не упомянута в §8 ни как сохраняемая, ни как заменяемая.
Здесь же собственное противоречие внутри абзаца: строка 73 (не изменена по смыслу в этом раунде) описывает шаг `validated` как срабатывающий «при `proceed == 'true'` **на этапе code**» — то есть его поведение зависит не только от `proceed`, но и от `stage`, отдельной переменной. Заявление строки 69 «единственная переменная — `proceed`» опровергается уже следующим абзацем того же документа.
Воспроизведение (конкретный сценарий, не гипотетический): round задачи применяет reuse (зелёный вердикт прошлого захода переносится без вызова модели, ровно сценарий #437/#499, ради которого и заведён этот механизм) → `gate` пишет `proceed=true`, `result=skipped` (строка 69) → по буквальному тексту строки 72 шаг `Review` получает условие `proceed == 'true'` → модель вызывается повторно на том же дереве, за которое уже заплачен один заход, что и есть ровно тот перерасход, который #499 закрыл, только на новом месте.
**Чем закрывается в этом же ТЗ:** явно развести, как и предлагала r2, но с учётом факта, найденного в этом раунде — существующего `reuse` конъюнкта, о котором ТЗ не сказало ни слова:
- красный-возврат (строка 71) — `proceed != 'true'` (эта часть уже верна, менять не нужно);
- группа «продолжение ревью» (строка 72) — либо явно сохранить существующий `&& steps.reuse.outputs.reuse != 'true'` в дополнение к `proceed == 'true'` для тех конкретных шагов, что сегодня его несут (зависимости, Chromium, `Review`), либо (эквивалентно и проще для «одной переменной») зафиксировать, что `gate` пишет `proceed=false`, а не `true`, на reuse-ветке — но тогда нужно менять и красный-возврат (строка 71), который на этой ветке не должен сработать, то есть развести «reuse» как третье, отдельно поименованное значение, а не смешивать его с `skipped`-для-spec;
- строку 69 («Единственная переменная — `proceed`») — либо снять как неточную (строка 73 её же опровергает), либо явно ограничить область действия: «единственная переменная, добавляемая этим ТЗ» — не единственная вообще.
Правка — уточнение слов в тех же трёх местах плюс явное упоминание существующего `reuse`-конъюнкта, отдельный issue не заводится (Medium в скоупе, PROCESS.md §2.4).
## Что проверено и корректно
- Красный-возврат (строка 71) исправлен верно и именно так, как предлагала r2: `proceed != 'true'` не срабатывает на skip-ветке — паразитный `S7 → S6` для spec/reuse-заходов устранён.
- Формальная (лексическая) нестыковка трёх формулировок из r2 устранена: строки 69/71/72 больше не называют одно и то же разными именами.
- Остальной текст §5.2 (порядок шага `gate` относительно `reuse`/«Конфликт с dev», алгоритм `validate-gate.mjs`, §5.1, §5.3) дельтой не тронут и остаётся тем, что уже проверено в r1/r2 построчно против кода.
- AC1, AC3, AC4, AC5, AC6 дельтой этого раунда не задеты по существу — унаследованы.
## Чего не проверял
- `npx tsc --noEmit`, `npm test`, `npm run build`, `node scripts/check-docs.mjs` — не запускал: дельта r2→r3 — 4 вставки/4 удаления в одном `.md`-файле вне `src/**`/`scripts/**`/`test/**`, класс C (PROCESS.md §1), гейты неприменимы; то же самое было верно для r1 и r2, поведение не изменилось.
- Golden/смоки/perf/backend/инварианты модели — задача не трогает геометрию, рендер, состояние или бэкенд; не изменилось с r2.
- Права токена `HP_PROCESS_TOKEN`, риски §10.1 — не менялись дельтой, унаследованы из r1/r2.
- Реальная реализация `process.yml`/`validate-gate.mjs` — их нет, этап spec их не предполагает; находка выше проверена **чтением существующего** `process.yml` и существующего теста, не запуском.
## Унаследовано из r2
Без повторной проверки в этом раунде — дельта их не касается:
- Обязательные разделы §7.1 присутствуют и на своём месте — из SPEC-REVIEW-510-r1 (SHA `34bf81a1`) и подтверждено в SPEC-REVIEW-510-r2 (SHA `5fc235c0`).
- Скоуп/не-скоуп (§2/§3), включая границу с #479/#480/#499, и совместимость условия job `changed_mutants` (§4) с существующим механизмом `--changed` — из SPEC-REVIEW-510-r2, SHA `5fc235c0`; дельта этого раунда §4 не касается.
- Раздел §10.0 (UX/i18n/модель данных не затрагиваются) — из SPEC-REVIEW-510-r2, SHA `5fc235c0`; текст не менялся.
- Порядок шага `gate` относительно `material`/`reuse`/«Конфликт с dev» (§5.2, первое предложение) — сверен в r2 с реальными строками `process.yml` (414/430/445/485), дельтой r3 не тронут — из SPEC-REVIEW-510-r2, SHA `5fc235c0`.
- Алгоритм `scripts/validate-gate.mjs` (§5.1), включая закрытие Medium 2 из r1 («доказательство только при исполненных и зелёных job мутантов») — из SPEC-REVIEW-510-r1 (SHA `34bf81a1`) и SPEC-REVIEW-510-r2 (SHA `5fc235c0`), текст §5.1 дельтой r3 не тронут.
- §6 (слияние кандидата), §7 (правила хендоффа), §9 (откат), §10.1 (риски), §11 (затронутые файлы), §12 (предположения) — из SPEC-REVIEW-510-r1/r2, текст не менялся ни в r2, ни в r3.
- AC1, AC3, AC4, AC5, AC6 — из SPEC-REVIEW-510-r1/r2, способ доказательства не изменился.
## Вердикт
High: 0 · Medium (в скоупе): 1 · Low: 0.
Находка r2 закрыта только частично: красный-возврат исправлен верно, но формулировка «единственная переменная — `proceed`», применённая ко всей группе «продолжение ревью» (включая установку зависимостей, Chromium и вызов модели), конфликтует с реальным кодом `process.yml` (условия этих шагов сегодня требуют ещё и `steps.reuse.outputs.reuse != 'true'`) и с существующим тестом `test/review-doc-guard.test.mjs:596`, который жёстко фиксирует, что модель не вызывается на reuse-ветке (#499). Буквальная реализация текста r3 открывает ровно тот перерасход (повторный вызов модели на уже принятом дереве), ради устранения которого заведён #499. Это не гипотетика — сценарий воспроизведён по коду и существующему тесту, а не по документу самому по себе. Закрывается в этом же ТЗ уточнением трёх формулировок §5.2 с явным упоминанием `reuse`-конъюнкта.
**Вердикт: жёлтый.** Возврат в «ТЗ в работе» для уточнения §5.2 (строки 69, 72); далее — повторный цикл ревью по дельте (PROCESS.md §2.9/§2.10).
---
<!-- material-anchors: сгенерировано конвейером (#414) -->
## Материал раунда
- Ветка: `issue/510-mutants-on-candidate-and-review-gate`, коммит `3854fe72ac59` — ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет.
- Дерево материала: `45ab656b9462fc4f8a3a6f5eaad139d4b14d9f99`
```
git log --all --format='%H %T' | grep 45ab656b9462
```
- ТЗ `docs/specs/510-mutants-on-candidate-and-review-waits-validate.md`, блоб `66bd41844726bd574d4fe59b506cd1f4389c34f8`
```
git log --all --find-object=66bd41844726bd574d4fe59b506cd1f4389c34f8 -- docs/specs/510-mutants-on-candidate-and-review-waits-validate.md
```
- Вердикт конвейера: `yellow` · High 0