Files
houseplan-card/PROCESS.md
Claude 696f5a789f docs(hygiene): сократить вход агента, у правила — один дом (#680)
Волна 3 эпика #674. AGENTS.md 650 → 187 строк: карта пакета, маршрут чтения,
правило №1, классы и треки одной строкой со ссылками, трейлеры, рабочие
деревья, хендофф и ожидание вердикта; пересказы PROCESS.md — ссылками на
разделы. Неверный список «Gate jobs» снят (списки jobs не копируются в прозу,
шапка PROCESS.md). Правила, жившие только в AGENTS, получили дом: жёлтый
вердикт при выполненных AC — PROCESS §2.7; свежесть бандла, съёмка только в
Linux (#455, HP_ALLOW_FOREIGN_CAPTURE) и смоки из AC до S7 (#151) —
TESTING.md; причуда демо-стенда и среда-зависимый smoke_opening_measure —
DEVELOPMENT › Smoke tests; отказ публикации без `Release:` и при несвежем
отпечатке бандла, отмена Validate новым пушем, кандидат беты не
promotion-only, fail-closed реестра Labs — DEVELOPMENT; предупреждение и
ошибка свежести скриншотов — CONTRIBUTING.

PROCESS.md: §13 (внедрение с открытым ⏳), §14 (блок со ссылкой на
несуществующий docs/PROCESS.md) и §7.3 (история) удалены. Ссылки «§7.2» на
правило полного разбора после ребейза ведут в §2.10, на сверку SHA перед
выводом — в §2.7; то же в сообщениях scripts/branch-state.mjs,
merge-candidate.mjs, review-doc-guard.mjs, pre-push-gate.mjs, в промпте
_process.yml и TESTING.md. Число `any` в прозе → `node scripts/no-new-any.mjs
--total` (новый режим, юнит-тест; было «1034 в 49 файлах», сейчас 862 в 52),
дата-число замороженного списка якорей монолита снято. Устаревшая команда
пересъёмки скриншотов в §8 заменена ссылкой на действующий путь.

STATUS.md 113 → 61 строка: сгенерированный снимок, текущий цикл и девять
строк решений; Workflow, CI, Toolchain, Tests, Scope, open items и политика
документации — ссылками (PROCESS §2.6, DEVELOPMENT › Release, TESTING);
локали en/ru/de/fr; закрытые «coverage, mypy strict» сняты.

DEVELOPMENT.md: file-sync и «Reproducible scripts» (прототип) удалены;
раздел Release — единственный дом релизной механики: введение, правила
тела стабильного релиза (#328, release:notes), шаг continuity:screencast,
источники версии по release-contract. CONTRIBUTING: ссылка на Release вместо
пересказа, замеры клона без чисел. TESTING: any-гейт — ссылкой на PROCESS §8.

entry-cost: автор 11 125 → 5 407 слов, ревьюер 8 464 → 4 285.

Issue: #680
User-Visible: no
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
2026-09-27 22:33:03 +03:00

1374 lines
124 KiB
Markdown
Raw Permalink 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.
# Процесс работы над House Plan
> **Статус документа: канон** (редакция 2026-08-13, ролевое уточнение
> 2026-09-13). Решения владельца, на
> которых он стоит: прямые коммиты в `dev` **без PR** · канон статуса — **метки**,
> имена английские · лёгкий трек **включён** · автор и ревьюер — независимые
> агенты/сессии · любой агент может взять любую роль · инфраструктурные задачи
> входят в общий флоу сразу на `S7-code-review`.
>
> **Область действия:** обязателен для владельца и для любого агента. Читается
> сразу после `docs/SCOPE.md` и `AGENTS.md`, до `docs/STATUS.md`. Живёт в
> репозитории: до августа 2026 канон лежал только в папке владельца, и свежий клон
> его не содержал вовсе.
>
> **Ролевые конспекты** (#634): `docs/process/AUTHOR.md` и
> `docs/process/REVIEWER.md` — выжимки этого файла со ссылками на его разделы.
> Автор и ревьюер входят через них (порядок чтения по роли — `AGENTS.md`) и
> открывают раздел канона, когда пункт конспекта касается текущего шага.
> Правил конспекты не добавляют; при расхождении побеждает этот файл. Ссылки и
> ключевые формулировки конспектов сверяет `test/process-digests.test.mjs`,
> поэтому правка формулировки здесь правит и конспект тем же коммитом.
>
> **Приоритет источников.** Канонический бэклог — GitHub Issues; статус живёт в
> метках и больше нигде: Project v2 не используется. При расхождении
> документации с GitHub побеждает GitHub. Этот файл — единственный полный канон
> процесса в репозитории; `AGENTS.md` — его короткое обязательное резюме, а
> внешний `CODEX-RUNBOOK.md` — только маршрутизатор к канону и историческим
> инструкциям. Текущие версии, состояние конкретных issue, runtime pins и списки
> jobs не копируются в производную прозу: они читаются из своих исполняемых
> источников.
> При расхождении этого документа с `.github/workflows/*.yml` и `scripts/*`
> побеждает **фактическая автоматизация**: она исполняется, а описание — нет.
> Расхождение при этом не игнорируется, а заводится issue с меткой `process`.
>
> При расхождении процесса и привычки побеждает процесс.
---
## 1. Основное правило
**Изменение продуктового кода без issue запрещено.** Код меняется только тогда,
когда issue существует и находится в статусе «Готово к разработке» или дальше.
Исключения — только §11, и каждое оставляет след.
Правило работает лишь при точной границе «продуктового кода», иначе спор
переносится на границу:
| Класс | Что входит | Нужен ли issue |
|---|---|---|
| **A. Продукт** | `src/**`, `custom_components/houseplan/**/*.py`, `manifest.json`, `hacs.json`, `src/i18n/*.json`, `custom_components/**/translations/*` | **Да, обязательно.** Только из «Готово к разработке» или дальше |
| **B. Гейты и инструменты** | `test/**`, `tests_backend/**`, `demo/**`, `scripts/**`, весь `.github/**`, `.githooks/**`, `rollup.config.mjs`, `tsconfig*.json`, `package.json`, `package-lock.json`, `pytest.ini`, `.gitignore`, `.gitattributes` | **Да.** Может использовать issue того изменения, которое покрывает; самостоятельная работа над гейтом получает свой issue (тип `tech-debt`) |
| **C. Документация** | `docs/**`, `README*`, `CHANGELOG*`, `AGENTS.md`, `CONTRIBUTING.md`, `PROCESS*.md`, `LICENSE`, `(CODE\|SPEC)-REVIEW-*.md` | Документирование A/B в том же коммите — часть DoD своего issue. Самостоятельная работа над документацией — свой issue |
| **D. Сгенерированное** | `dist/**`, `custom_components/houseplan/frontend/**`, `demo/golden/baselines/**` (копия стенда `demo/srv/assets/houseplan-card.js` с #255 не коммитится вовсе) | Никогда не меняется само по себе. Коммит **только** класса D допустим лишь как релизный промоушен или как принятие эталонов с доказательством ревью. **Бандл** (`dist/**`, `custom_components/houseplan/frontend/**`) с #657 меняет только коммит с трейлером `Release:` — кандидат беты или релиза (`npm run bundle:release`); в обычной задаче закоммиченный бандл законно отстаёт от исходников, сборка в коммит не идёт (`npm run bundle:clean`). Судит `validate-commit-provenance.mjs` — хук `commit-msg` и история в CI |
Практический смысл таблицы: «я только поправил тест» и «я только пересобрал
бандл» перестают быть лазейками.
Классы неупорядочены, но при пересечении путей **D сильнее A**: собранный бандл
лежит внутри `custom_components/houseplan/frontend/`, и без этого правила он
считался бы продуктовым исходником.
**Инфраструктурная задача использует ускоренный вход в общий флоу** (решение
владельца 2026-09-13, issue #562). Признак механический: **ни одного файла класса
A**. Любой агент может сразу реализовать её по issue и в ветке
`issue/<NN>-<slug>`, без аналитики, ТЗ, ревью ТЗ и статусов `S1`…`S6`. Когда
материал готов, локальные гейты зелёные и ветка запушена, исполнитель ставит
`S7-code-review`. Дальше действует тот же контроллер, что для продуктового кода:
зелёное ревью сливает проверенный материал в `dev` и ставит `S8-merged`, а
замечания или неудавшееся слияние возвращают задачу в `S6-in-progress`; после
исправлений она снова идёт в `S7-code-review`.
Отсутствие ТЗ не означает отсутствие проверки. Для инфраструктуры обязательны
issue, терминальные трейлеры, соразмерные изменению зелёные гейты, хендофф с
доказательствами и независимое код-ревью. Модель или имя агента процессом не
предписываются.
Задача, задевающая класс A хотя бы одним файлом, инфраструктурной **не
является** и идёт полным флоу. «В основном инфраструктурная» не бывает: иначе
это дорога, по которой продуктовые правки минуют ревью. Признак задан через
класс файлов, а не через самоощущение исполнителя, именно поэтому.
---
## 2. Жизненный цикл
Восемь рабочих статусов и два служебных. Полный маршрут ниже относится к
продуктовым задачам. Фазы тестирования в цикле сознательно
**нет**: найденные позже дефекты заводятся отдельными issue и проходят цикл
заново. Issue закрывается после выпуска беты.
```
S1-new → S2-analysis → S3-spec → S4-spec-review ⟲ → S5-ready →
→ S6-in-progress → S7-code-review ⟲ → S8-merged → закрыт при выпуске беты
служебные: blocked (поверх статуса) rejected (закрыт)
⟲ — возврат на правки, не более 4 циклов (§4), на лёгком и коротком треке 2
короткий трек (`trivial`, §5.1) идёт S2-analysis → S5-ready, минуя S3 и S4
инфраструктурный трек (§1): без S → S7-code-review ⟲ S6-in-progress → S8-merged
```
Переходы `S4-spec-review` и `S7-code-review` выполняются **автоматически**: метка
порождает событие, событие запускает ревью (§10.4). Остальные ставит исполнитель.
### 2.1 Новое — заведение задачи
- **Кто:** любой — владелец, агент, пользователь (Telegram, GitHub).
- **Вход:** проблема в пользовательских терминах; как проявляется или зачем нужно.
Решение **не требуется** и не приветствуется.
- **Запрещено:** ставить приоритет, оценивать, писать ТЗ, начинать код.
### 2.2 Аналитика и оценка
Задача разбирается — и разобранная **сама идёт дальше**. Умолчание изменено
решением владельца 2026-08-14: раньше аналитика ждала подтверждения по каждому
пункту, и большинство ожиданий ничего не меняло — issue в основном описаны
однозначно.
- **Кто:** агент-аналитик. Владелец не утверждает переход — он правит асинхронно.
- **Чек-лист**, результат — комментарием в issue:
1. дубликаты проверены (ссылки на похожие issue);
2. в скоупе по `docs/SCOPE.md` и `docs/TOUCH-SUPPORT.md`;
3. **пользовательская ценность 1–10** и **ценность для разработки** — что
упрощает или разблокирует;
4. **сложность и риск 1–10** — трудоёмкость плюс вероятность задеть смежное;
5. приоритет **P1/P2/P3**;
6. тип: баг / фича / техдолг;
7. затронутые поверхности (модули, диалоги, бэкенд, i18n);
8. трек — **по умолчанию `small`** (§5). Если задача идёт полным треком,
называется критерий §5, который она не проходит: «обычный трек» без
названного критерия обоснованием не является.
- **Оценки и приоритет ставятся метками сразу, согласие не запрашивается.**
Комментарий аналитики — уведомление, а не запрос: **молчание владельца —
согласие**, несогласие он выражает правкой меток или комментарием, и это не
останавливает работу. Право отклонить задачу (`rejected`) остаётся за
владельцем на любой стадии.
- **Вопросов владельцу на этом этапе нет.** Единственный класс вопросов, который
вообще задаётся владельцу, — продуктовые (§7.1: что человек видит или делает,
объём видимых изменений), и их место — этап ТЗ, пачкой, с вариантами по
умолчанию и `blocked`. Вопрос, который можно отложить до ТЗ, не задаётся в
аналитике; вопрос, не блокирующий написание ТЗ, не задаётся вовсе — вместо
него в ТЗ пишется блок принятых предположений.
- **Выход:** `S3-spec` — переход выполняет сам аналитик, не дожидаясь ответа.
Либо, при явном конфликте со `SCOPE.md`, — предложение отклонить с причиной:
это единственный случай, когда аналитика останавливается и ждёт владельца.
### 2.3 ТЗ в работе — написание ТЗ
- **Кто:** автор ТЗ, назначает себя. Статус означает «занято».
- **Артефакт:** **тело issue**, раздел `## ТЗ` (решение владельца 2026-09-10,
#517). Файл в `docs/specs/` не создаётся ни на одном треке: каталог — архив
ТЗ до этой даты, и задачи, у которых файл уже есть, доживают по старой схеме.
Доказуемость («вердикт вынесен на этом тексте») держит конвейер: в блок якорей
документа ревью пишется `sha256` нормализованного тела, и правка ТЗ после
зелёного ревью ТЗ приходит ревьюеру кода находкой, а не тишиной.
- **Выход:** полная первая редакция по §7.
### 2.4 ТЗ на ревью
- **Ревьюер ≠ автор.** Ревьюер получает issue и ТЗ, без устных пояснений автора.
Его задача — не согласиться, а найти, где ТЗ не выполнимо или не проверяемо.
- **Артефакт:** `docs/reviews/SPEC-REVIEW-<NN>-r<N>.md`, вердикт
зелёный / жёлтый / красный. Лёгкий трек — комментарий в issue.
- **High-находки блокируют.** Medium **в скоупе задачи** чинится в текущем
issue: без High это жёлтый вердикт, автор правит ТЗ, фикс проходит повторный
цикл. Medium **вне скоупа** — отдельный issue: чужой скоуп в этой задаче не
правится. «Оставили в тексте ревью» не считается закрытием ни для одной
(решение владельца 2026-08-19, #202: отдельный issue дороже правки на месте).
Low либо правится, либо снимается решением ревьюера с записью.
- **Выход:** «Готово к разработке» либо возврат в «ТЗ в работе» — не более
4 циклов (§4). Второй и последующие циклы разбираются по дельте (§2.10).
### 2.5 Готово к разработке (DoR)
Не работа, а **очередь**: единственный статус, из которого можно трогать код.
Все пункты обязательны:
- ТЗ существует, ревью ТЗ зелёное, ссылки issue ↔ ТЗ на месте;
- **AC1…ACn** — пронумерованные проверяемые критерии приёмки; у каждого указано,
чем он доказывается: `unit` / `backend` / `smoke` / `golden` / «ревью кода»;
- перечислены затронутые файлы и модули;
- i18n: ключи en + ru перечислены;
- миграция и compatibility-поля решены по `docs/CONFIG-COMPATIBILITY.md`;
- влияние на производительность и бюджеты названо (или явно «нет»);
- влияние на touch по `docs/TOUCH-SUPPORT.md` (View и киоск — блокирующие);
- release-артефакты названы: changelog RU+EN, документация, golden/скриншоты,
performance/security — либо явное «нет»;
- **откат**: как выключить или вернуть назад (флаг Labs, обратная миграция);
- открытых продуктовых вопросов нет; риски перечислены.
Если хоть один пункт не выполнен — статус не «Готово к разработке», как бы ни
хотелось начать.
### 2.6 В разработке — реализация
- **Занятие (claim):** назначить себя, поставить метку, комментарий
«Взял: <роль> · сессия <id> · ветка `issue/<NN>-<slug>`».
- **WIP-лимиты:** не более **1** issue в «В разработке» на исполнителя, не более
**3** одновременно на цикл релиза, не более **2** в «Код-ревью».
- **Трассируемость:** ветка `issue/<NN>-<slug>`; каждый коммит несёт трейлеры
`Issue: #<NN>` и `User-Visible: yes|no`.
- **Автотесты — часть реализации, а не отдельная фаза.** Каждый AC, помеченный
`unit`/`backend`/`smoke`/`golden`, получает свою проверку здесь же.
«Тестирование вне жизненного цикла» означает отсутствие фазы ручного
тестирования, а не отсутствие тестов.
- **Приёмка проверяет результат для человека, а не строки реализации.** Для
изменённой поверхности автор выбирает обычный сценарий и самый рискованный
применимый соседний случай; у каждого должен быть наблюдаемый oracle — что
пользователь видит, может сделать или что система отказывается делать. При
выборе случаев коротко пройти шесть классов риска: async (порядок, отмена,
устаревший ответ); данные и права (пусто, нет связи, несколько источников,
отказ); геометрия (границы, стыки, трансформации); визуал (промежуточный кадр,
тема, zoom/DPR); объём данных и performance; host/input (HA, кэш, mouse/touch/
keyboard). Неприменимое так и отмечается; проверка имени метода или строки
исходника пользовательским oracle не считается.
- **Скоуп не расширяется.** Найденное по пути становится новым issue в «Новое».
Если находка блокирует — текущий issue уходит в «Заблокировано» со ссылкой.
Попутных правок «раз уж я здесь» не бывает.
- **Документация — в том же коммите,** что и поведение: changelog RU+EN для
пользовательского, `STATUS.md` для состояния, `DEVELOPMENT.md` для новых
грабель, `ARCHITECTURE.md` для дизайна.
- **Выход:** локальный гейт зелёный (§8), хендофф-комментарий (§7.2).
### 2.7 Код-ревью
- **Ревьюер ≠ исполнитель**, свежая сессия без контекста реализации.
- **Артефакт:** `docs/reviews/CODE-REVIEW-<tag|NN>-r<N>.md` в действующем
формате: скоуп, как проверялось (таблица гейтов с результатами), находки
High/Medium/Low с воспроизведением, что проверено и корректно, чего не проверял.
- **Ревьюер отвечает за полноту доказательств AC, а не заменяет их исполнение.**
Каждый AC либо доказан автотестом — и ревьюер убедился, что **тест умеет
падать**, — либо разобран по коду с явной записью «проверено чтением, не
исполнением». Чтение кода выявляет риски, но не доказывает наблюдаемый
пользовательский результат; если соразмерный исполнимый oracle возможен, его
отсутствие — находка. Ревьюер отдельно сверяет применимые классы риска из
§2.6 и не выдаёт запуск гейта за проверку сценария, которого в гейте нет.
- **Защитный AC доказывается таблицей «чем краснеет» (#435).** Для каждого AC,
заявляющего защиту — валидация, гард, лимит, отказ, инвариант, — в документе
ревью обязательна строка из трёх столбцов: **AC · чем доказан** (точная
команда или имя теста) **· чем краснеет** — мутация, снятая защита или
отрицательная проба, с результатом прогона. Пустой третий столбец — находка
Medium, а не примечание.
«Тест умеет падать» без названной мутации и её вывода доказательством не
является. Аудит v1.71.0-beta.1 нашёл пять контрактов #51 и #423, где тест
оставался зелёным на снятой защите; все пять прошли код-ревью как доказанные,
а два теста были записаны в закрытие coverage-ratchet под именами, обещавшими
то, чего они не проверяли (#430).
Мутант в `scripts/mutation-gate.mjs` обязателен, когда защита живёт в
продуктовом коде и проверяется дорогим гейтом (смок, бэкенд, golden): там
ревьюер не воспроизведёт отрицательный прогон второй раз. Для чистых юнитов
достаточно прогона со снятой защитой, приведённого в документе.
Новый мутант с browser-smoke guard допустим только когда инвариант нельзя
доказать без браузера: `because` обязан назвать конкретную зависимость от
DOM/CSS paint, измеренной геометрии, trusted pointer/lifecycle или browser
wall-time, а id — попасть в размеченный реестр
`docs/testing-notes/mutation-browser-guards.md`. `mutation-gate --check`
показывает число browser guards против лимита 200, краснеет при росте выше
лимита и предупреждает о любом id без browser-обоснования; ревьюер проверяет
не только наличие строки, но и невозможность более дешёвого `node --test`.
Считаются **защитные AC без названного свидетеля**, а не мутанты на
подсистему: у #421 мутанты были, и дыра всё равно проехала. «Сколько мутантов
принесла задача» остаётся признаком — у #423 их ноль, и именно у #423 нашёлся
тест, спрашивавший регулярку, находит ли она подстроку, которую сам же и
вырезал.
Правило не распространяется на AC, не заявляющие защиту (расположение, текст,
формат вывода): там свидетель — обычное сравнение ожидаемого с фактическим, и
третий столбец превратился бы в ритуал. И не отменяет «проверено чтением»:
тогда во втором столбце стоит «чтением», а не имя теста, и читатель ревью
видит разницу.
- **High блокируют.** Medium **в скоупе задачи** чинится в текущем issue:
без High это жёлтый вердикт и возврат автору, фикс проходит повторный цикл.
Medium **вне скоупа** — отдельный issue (#202). Жёлтый вердикт законен и
тогда, когда все AC выполнены, если изменение не решает заявленный сценарий
или ухудшает соседний.
- **Вердикт привязан к SHA (#312).** Все числа и факты отчёта сверяются с
`git rev-parse HEAD` непосредственно перед подведением итогов, а не с SHA,
зафиксированным в начале разбора: во время ревью в ветку может прилететь
fix-up. Серверный стопор — шаг слияния конвейера сверяет вершину ветки с
SHA материала ревью (допустим только собственный doc-коммит публикации
поверх) и при расхождении отменяет слияние с возвратом в `S6-in-progress`.
- **Контракты по монолиту — исполнением, не regex по тексту (#624).**
Новое утверждение о `src/houseplan-card.ts` или
`src/houseplan-editor-runtime.ts` доказывается экспортом функции и её
вызовом в `test-build`, а не поиском строки в исходнике: текстовый якорь
краснеет на переносе метода без единой регрессии, и это делает вынос дороже,
чем оставить монолит как есть. Список тестов, читающих монолит как текст,
заморожен (`FROZEN_TEXT_ANCHOR_TESTS` в `test/monolith-text-anchors.test.mjs`)
и может только уменьшаться; новое имя в нём — находка ревью, а не запись в
список. Связность монолита измеряется шестью числами
(`scripts/monolith-metrics.mjs`: делегаты, члены порта, `host.`, приватные
члены порта и харнесса, байты `dist/`), база — `scripts/monolith-baseline.json`;
гейт `npm run lint:unused` (в `gate:small` и Validate после сборки) красит
рост любого из них и любой мёртвый код по `noUnusedLocals` вне порта и
харнесса. Снижение фиксируется тем же коммитом
(`node scripts/unused-locals-gate.mjs --update`); рост — только с записью в
issue задачи и правкой базы в том же коммите.
- **Смок входит в сценарий через публичную поверхность (#629).** DOM с
контрактными хуками, события HA и фикстуры, тестовый фасад `window.__hpTest`
(`docs/TESTING.md`, «Тестовый фасад и приватное состояние»). Приватное поле
карточки — только для чтения в ассертах. Новая запись в него без
`// private-ok: <конкретная причина>` — находка ревью, даже если гейт
`no-new-private-writes` её не увидел (мутация через вызов, запись через
псевдоним).
- **Выход:** очередь на пре-релиз либо возврат в «В разработке», не более
4 циклов (§4). Второй и последующие циклы разбираются по дельте (§2.10).
### 2.8 Закрытие после выпуска беты
- **Вход:** изменение вошло в опубликованную бету/RC, CI Validate зелёный на
**точном SHA** тега (промоушен-правило: ни одна фича не попадает в стабильный
релиз, не побывав в бете).
- **Закрывает** релиз-менеджер, не исполнитель. Комментарий закрытия: тег беты,
ссылка на прогон CI, ссылка на бюллетень changelog.
- **Стабильный релиз статусов не двигает** — issue уже закрыты; релизный коммит
promotion-only, changelog ссылается на закрытые issue.
- **Что приходит потом:** дефект, найденный на стенде, дома или пользователем, —
**новый issue** типа «баг» со ссылкой на исходный. Исходный не переоткрывается.
### 2.9 Заблокировано / Отклонено
- **Заблокировано:** обязательна ссылка на блокирующий issue или внешнюю причину
и дата пересмотра. Без причины статус не ставится.
- **Отклонено:** закрытие с записанной причиной (вне скоупа, дубликат, цена не
оправдана). Тихое закрытие без причины запрещено.
### 2.10 Повторный раунд ревью — объём по дельте
Решение владельца 2026-08-19 (issue #214). Относится и к ревью ТЗ, и к
код-ревью, начиная со второго цикла.
**Предмет повторного раунда — дельта, а не задача целиком.** Раньше объём
разбора не был оговорён, промпт ревьюера для всех раундов был одинаковым, и
повторный цикл заново выводил продуктовую рамку и перепроверял AC, которых
правка не касалась: r2 по #150 стоил полного прогона конвейера ради одной
строки в тестовой фикстуре.
Порядок:
1. найти вердикт предыдущего раунда и **материал, на котором он получен**.
Материал объявлен блоком «Материал раунда» в конце документа предыдущего
раунда: конвейер дописывает туда SHA ветки, **дерево** материала и **блоб**
каждого ТЗ вместе с командами поиска (issue #416). Блок машинный — править
его руками не нужно и не следует;
2. объявить дельту: `git diff <тот SHA>..HEAD` для кода, дифф файла ТЗ либо тела
issue для этапа ТЗ.
**Если SHA не резолвится — это не находка, а обычное дело.** Ветку задачи
между раундами перебазируют, сквошат или удаляют, и SHA умирает: по корпусу
ревью таких объявлений 98 из 804. Материал в этом случае берётся по якорям,
которые ребейз не меняет, потому что адресуются содержимым:
```
git log --all --format='%H %T' | grep <дерево>
git log --all --find-object=<блоб> -- <путь к ТЗ>
```
Находкой остаётся другое: **SHA, мёртвый уже в момент публикации отчёта** —
он означает, что значение сняли до `amend` или `rebase` и не сверили перед
выводом, как требует §2.7 («Вердикт привязан к SHA»). Это отличие не
теоретическое: на #403 оба источника, автор и ревьюер, независимо назвали один
и тот же осиротевший SHA, и следующий раунд восстанавливал коммит по
содержимому диффа руками (issue #413). Конвейер теперь такую публикацию останавливает сам;
3. по каждой находке предыдущего раунда показать, **чем именно она закрыта** —
строкой кода или текста, а не заявлением автора;
4. заново проверять только те AC, чьё доказательство дельта задевает;
5. **раздел «Унаследовано из r<N−1>»** обязателен: что принято без повторной
проверки, со ссылкой на документ того раунда и его материал. Без перечня
сокращение превращается в молчаливое доверие.
**Индекс документов ревью** — `docs/reviews/INDEX.md` (#635): одна строка на
документ — issue, этап, раунд, вердикт, число High/Medium, заголовки находок,
файлы из находок (искать по имени файла: `grep form-kit docs/reviews/INDEX.md`).
Файл генерируется `node scripts/reviews-index.mjs` и пересобирается **только
коммитами, идущими в `dev`** (#657, решение 1б): слиянием кандидата —
после ребейза, а если `dev` не двигался, то поверх материала перед
fast-forward (`--commit-if-stale`, коммит класса C) — и публикацией документа ревью ТЗ
прямо в `dev`. В ветке задачи индекс не пересобирается — ни при приведении к
dev, ни при публикации документа код-ревью: иначе две параллельные задачи
конфликтуют на нём по построению. Руками не правится. Конфликт ребейза, в котором **все** пути —
`INDEX.md`, отказом не считается (#643): `scripts/rebase-generated.mjs`
пересобирает индекс по каталогу на остановке и продолжает ребейз — так делают
приведение к dev, слияние кандидата и авторский `rebase-on-dev.mjs`; индекс
вместе с любым другим путём — прежний отказ с перечнем файлов. Шаг Validate
«индекс ревью совпадает с каталогом» красит push в `dev`, где `INDEX.md`
расходится с каталогом (на issue-ветках не судится: их переписывает конвейер).
Правка `docs/reviews/`
руками — перенос в `legacy/`, удаление — сопровождается
`node scripts/reviews-index.mjs` в том же коммите. Прежде чем брать
задачу по подсистеме, стоит прочитать её строки в индексе: что находили и чем
закрывали — там, а не в тысяче файлов. Уроки, пережившие свою задачу,
собираются в `docs/LESSONS.md` с датой и ссылкой на источник.
**Хранение документов** (решение владельца 23.09, #635): в `docs/reviews/`
лежат все раунды всех задач текущей линии — предыдущие раунды нужны ссылкам
«Унаследовано из r<N−1>» и якорям материала. При стабильном релизе документы
задач, вошедших в него (`RELEASE-MEMBERSHIP.json` линии), переносятся в
`legacy/reviews/<vX.Y.Z>/` одним коммитом класса C; индекс пересобирается и
перечисляет только живые. Перенос — часть чеклиста стабильного релиза, не
отдельная задача.
Дешёвые гейты (`typecheck`, `test`, `build` с проверкой целостности сборки, `bundle-policy --verify`) гоняются в
каждом раунде: код изменился, а стоят они минуты. Тяжёлые — по дельте (§10.2).
**Разбор остаётся полным**, если дельта не локальна: ребейз на ушедший вперёд
`dev` (после ребейза это другой код, §10.4), смена контракта поведения, задета
новая подсистема, либо объём дельты сопоставим с исходной задачей.
Сокращается объём **разбора, а не строгость**: правка по замечанию способна
сломать AC, который предыдущий раунд признал выполненным — так появилась
регрессия #102. Граница не «только находки», а «находки плюс всё, до чего
дотягивается дельта».
---
## 3. Правила
Продолжение черновика владельца. Каждое правило проверяемо — глазами или машиной.
1. **Никаких изменений в код, если нет issue** и он не помечен «Готово к
разработке» или дальше.
2. **Issue не может быть взят в разработку**, пока у него нет ТЗ с зелёным ревью,
пронумерованных AC с указанием доказательства и назначенного исполнителя.
3. **Issue не может быть взят дважды.** Занятие фиксируется назначением, меткой и
комментарием с именем ветки. У одного исполнителя одновременно не более одного
issue в разработке.
4. **Статус меняется до действия, а не после.** Взял — поставил метку; отдал на
ревью — поставил метку. Метка, поставленная задним числом, — дефект процесса.
5. **Ровно одна метка статуса** на issue в любой момент. Ноль или две — дефект,
еженедельная гигиена его показывает.
6. **Автор не ревьюит своё** — ни ТЗ, ни код. Никто не переводит свою работу через
ревью-гейт.
7. **Ревью возвращает не более 4 раз.** Пятый заход — решение владельца: разделить,
отклонить или арбитраж (§4).
8. **High блокирует. Medium в скоупе чинится в текущем issue** (без High —
жёлтый вердикт и повторный цикл); Medium вне скоупа становится отдельным
issue (#202). Low либо правится, либо снимается решением ревьюера с записью
в документе.
9. **Скоуп не расширяется.** Всё найденное вне ТЗ — новый issue, а не попутная
правка. Блокирующая находка отправляет текущий issue в «Заблокировано».
10. **Каждый коммит класса A и B несёт трейлер `Issue: #NN`**, ветка называется
`issue/NN-slug`, а `User-Visible: yes` требует правок в **обоих** changelog в
том же коммите. После `cherry-pick -x` служебная строка `(cherry picked
from ...)` должна оставаться выше финального блока трейлеров: перед push
проверяем порядок через `git show -s --format=full HEAD`.
11. **Документация — в том же коммите, что поведение.** Отдельным «допишу потом»
коммитом документация не бывает.
12. **Сгенерированное не коммитится само по себе.** Только релизный промоушен или
принятие эталонов с доказательством ревью: URL Linux CI run либо хеш
аттестованного WSL-артефакта.
13. **Golden-эталоны принимаются только** `npm run golden:accept -- --reviewed` по
полному Linux-артефакту: либо GitHub CI, либо `npm run golden:wsl:capture` в
WSL/ext4 с clean опубликованным SHA и машинно-проверяемым паспортом. Второй
путь убирает только первый ожидаемо красный CI-прогон; полный GitHub Validate
на точном SHA коммита с эталонами остаётся обязательным. Принятие ради
зелёного CI — нарушение процесса.
14. **Issue закрывается после выпуска беты** с зелёным CI на точном SHA. Не
раньше, не «по факту наличия кода», не исполнителем.
15. **Закрытый issue не переоткрывается.** Новый дефект — новый issue со ссылкой.
16. **Стабильный релиз — promotion-only:** версии, сгенерированные бандлы,
changelog и release-метаданные. Продуктового кода там нет.
17. **История `dev` не перезаписывается.** На неё ссылаются теги. Нарушение
исправляется следующим коммитом плюс issue с меткой `process` — не
force-push'ем.
18. **AC доказывает автотест или запись ревьюера.** Фразы «проверил локально, всё
работает» в процессе не существует: либо тест, который умеет падать, либо
честное «проверено чтением, не исполнением».
19. **Параллельных бэклогов нет.** Планы, разборы и приоритеты живут в issue;
файловые отчёты — разовые и датированные.
20. **Аварийный хотфикс — только решением владельца** и только по §11.2.
---
## 4. Лимит циклов ревью: 4
Оба ревью-гейта возвращают задачу на правки не более **4 раз**.
- **Что считается циклом:** отправка на ревью → вердикт с блокирующими находками
→ возврат. Уточняющий вопрос без вердикта циклом не считается.
- **Зелёный вердикт цикла не образует** и бюджет не тратит (решение владельца
2026-08-20, issue #227): он ничего не вернул на правки. Практический случай —
зелёное ревью, слияние которого не удалось: конвейер сам предписывает ребейз и
возврат метки, и этот заход не должен наказываться. Раньше счётчик считал все
вердикты подряд, и на #225 последовательность жёлтый → зелёный → ребейз дала
`review-4` на задаче с зелёным ревью и зелёным CI.
- **Заход и цикл — разные величины.** Заход — сколько раз ревью отработало; он
виден в имени документа (`-r1`, `-r2`, …) и нужен, чтобы два документа не
затёрли друг друга. Цикл — единица бюджета §4. Заходов законно бывает больше,
чем циклов, поэтому порог проверки 7 в `scripts/process-gate.mjs` выше лимита
циклов (шесть документов = четыре цикла плюс два ребейза).
- Метка `review-4` ставится, когда исчерпан **бюджет циклов**; конвейер снимать
её не вправе — это решение владельца. Если бюджет пересчитан и оказался ниже
лимита, конвейер сообщает пересчёт, но метку не трогает.
- **Исчерпание лимита — не «пятая попытка», а разбор.** Задача уходит владельцу,
решение одно из трёх:
1. **разделить** — issue закрывается как «заменён», вместо него 2–3 меньших с
ясным скоупом (частый настоящий диагноз: ТЗ было слишком большим);
2. **отклонить** — цена решения оказалась выше ценности;
3. **арбитраж владельца** — владелец фиксирует решение в issue, оно принимается
как есть; несогласие ревьюера записывается, но не блокирует.
- **Граница между «циклом» и «новым багом»:** до закрытия беты находка ревьюера —
возврат на правки; после закрытия — новый issue. Иначе лимит 4 обходится
заведением issue вместо возврата.
- Для лёгкого трека лимит ревью ТЗ — **2** цикла: задача на три часа, которую
переписывают трижды, лёгкой не была.
---
## 5. Лёгкий трек (метка `small`) — путь по умолчанию
Умолчание изменено решением владельца 2026-08-27, issue #338. Прежде полный трек
был бесплатен, а выбор лёгкого требовал обоснования. Фактическая цена: **2.9
ревью-документа на задачу** в среднем и до шести на одну issue (#329, #316,
#290) — при том что Medium-находки всё равно чинятся в той же задаче, без
отдельного цикла.
**Порог не изменился.** Критерии ниже те же и по-прежнему обязательны все
одновременно. Изменилась сторона доказательства: теперь обосновывается не выбор
лёгкого трека, а отказ от него — в `S2-analysis` называется критерий, который
задача не проходит. Полный трек остаётся тем, чем был, для геометрии, миграций
конфига и публичных контрактов: там критерии нарушаются сами, и назвать
нарушенный несложно.
Инверсия умолчания не отменяет ничего из §5 ниже и ничего из §4: бюджет четырёх
циклов, арбитраж владельца, обязательность ТЗ на полном треке и правило «ревью
до мержа» остаются как были. Меняется только стоимость пути по умолчанию.
**Критерии — все одновременно; нарушенный называется явно:**
- сложность и риск ≤ 3;
- одна поверхность (один диалог, один модуль, один эндпоинт);
- нет миграции конфига и новых compatibility-полей;
- нет нового UX-контракта — меняется поведение в рамках уже описанного;
- нет влияния на производительность и на touch-контракт.
**Что упрощается:**
- ТЗ короче: проблема · контракт · AC1…ACn с доказательством · откат
(в теле issue, как и на полном треке с 2026-09-10);
- ревью ТЗ — комментарий второго агента, отдельный документ не нужен;
- лимит ревью ТЗ — 2 цикла.
**Что не упрощается:** issue, оценка, статусы, трейлеры коммитов, changelog,
**код-ревью и его документ**, закрытие после беты. Код-ревью не пропускается:
оно проверяет скоуп, риски и качество доказательств, но не заменяет исполнение
тестов. Единственное исключение из повторного ревью — починка упавшего
предрелизного гейта, §11.4.
Если по ходу выясняется, что критерий нарушен (появилась миграция, задело второй
модуль) — метка `small` снимается, issue возвращается в `S3-spec` и получает
полное ТЗ в теле issue по §7.1. Это не провал, это ранняя диагностика.
### 5.1 Короткий трек (метка `trivial`)
Решение владельца 2026-08-13, issue #128. Лёгкий трек делает ТЗ дешёвым; короткий
обходится без него совсем.
**Маршрут:** `S1-new` → `S2-analysis` → `S5-ready` → `S6-in-progress` →
`S7-code-review` → `S8-merged`. Стадии `S3-spec` и `S4-spec-review` пропускаются.
`S2-analysis` остаётся: это комментарий, а не прогон CI, и именно там владелец
решает приоритет и ценность. AC пишет автор в теле issue при переводе в
`S5-ready` — до перехода, иначе ревьюеру нечего будет сверять.
**Критерии, все обязательны:**
- тип `bug`;
- правка ограничена одной поверхностью, нового UX-контракта нет;
- нет миграции конфига, новых ключей i18n, влияния на перф и touch;
- AC выражаются тремя проверяемыми утверждениями или меньше;
- **ожидаемое поведение уже зафиксировано** — в `docs/USER-GUIDE.ru.md`, в
каноническом документе подсистемы либо однозначно в самом отчёте. Решать нечего.
Если есть что решать, это `S3-spec`, и никакая экономия этого не отменяет.
Метка ставится в `S2-analysis` вместе с остальными оценками, одним комментарием,
где владелец утверждает и приоритет.
**Что не упрощается:** issue, оценка, статусы, трейлеры, changelog и **код-ревью**.
Лимит циклов код-ревью — 2, как на лёгком треке.
Если по ходу выясняется, что критерий нарушен, метка снимается и issue уходит в
`S3-spec` за нормальным ТЗ. Как и на лёгком треке, это не провал, а ранняя
диагностика.
**Чем этот трек опасен.** Он убирает единственное место, где решение проверялось
до написания кода. Признак «решать нечего» держит всю конструкцию, и его нельзя
подтверждать ощущением — только ссылкой на уже зафиксированное поведение.
---
## 6. Роли
Один агент может исполнять несколько ролей в разных issue, но **не две роли в
одном артефакте**.
| Роль | Делает | Не имеет права |
|---|---|---|
| Аналитик | разбор, оценки, поверхности | окончательно ставить приоритет |
| Автор ТЗ | раздел `## ТЗ` в теле issue | ревьюить своё ТЗ |
| Ревьюер ТЗ | `docs/reviews/SPEC-REVIEW-NN-rN.md` | править ТЗ вместо автора |
| Разработчик | код, автотесты, документация, changelog | ревьюить свой код, принимать golden |
| Ревьюер кода | `docs/reviews/CODE-REVIEW-*-rN.md`, проверка AC | править продуктовый код |
| Релиз-менеджер | пре-релиз, стабильный релиз, закрытие issue | добавлять код в релизный коммит |
| Владелец | приоритет, ценность, скоуп, отклонение, арбитраж, хотфикс | — |
**Правило разделения:** ревьюер работает состязательно. Ему передаётся тег или
диапазон коммитов и ТЗ — не рассказ автора о том, как всё хорошо.
**Роли не закреплены за моделями или именами агентов** (решение владельца
2026-09-13, issue #562). Codex, Claude или любой другой доступный агент может быть
аналитиком, автором ТЗ, разработчиком, автором инфраструктурной задачи или
релиз-инженером по прямой команде владельца.
Разделение относится к артефакту: **автор и ревьюер — разные агенты/сессии**.
Модель может совпадать, но ревьюер начинает без контекста реализации и не ставит
вердикт собственной работе. Ревью ТЗ и код-ревью также идут в независимых
сессиях: ревьюер кода не должен приходить с контекстом обсуждения ТЗ.
Владелец сохраняет исключительные решения о приоритете, ценности, продуктовом
скоупе, отклонении, арбитраже, закрытии issue и команде на выпуск.
---
## 7. Артефакты и трассируемость
### 7.1 Цепочка
```
issue #NN
↔ ТЗ тело issue, раздел `## ТЗ` (хеш тела — в якорях ревью)
↔ ревью ТЗ docs/reviews/SPEC-REVIEW-NN-rN.md (или комментарий при `small`)
↔ ветка issue/NN-slug
↔ коммиты трейлеры Issue: #NN · User-Visible: yes|no
↔ ревью кода docs/reviews/CODE-REVIEW-<tag|NN>-rN.md
↔ changelog бюллетень RU+EN со ссылкой на #NN
↔ бета тег, зелёный CI на точном SHA → закрытие
```
Обязательные разделы ТЗ: **сценарий** · **что человек увидит до и после** ·
проблема · скоуп и **не-скоуп** · контракт поведения · UX · модель данных и
миграция · i18n · критерии приёмки AC1…ACn с указанием доказательства · план
автотестов · риски · откат · release-артефакты.
Два первых раздела — продуктовые, и они идут первыми не случайно. **Сценарий:**
какая персона (`docs/SCOPE.md`), на какой поверхности, в какой момент это
встретит. **Что человек увидит:** одной фразой, без терминов реализации. ТЗ,
которое не может ответить на эти два вопроса, описывает работу, а не изменение
продукта.
**Размытое место не додумывается, а выносится владельцу.** Догадка, записанная
как факт, — худший вид дефекта: она проходит ревью, потому что выглядит решением.
Но спрашивать обо всём нельзя: владелец один, и анкета из двадцати пунктов хуже
угадывания. Порог такой (решение владельца 2026-08-13).
**Владельцу задаются только продуктовые вопросы** — что человек видит или делает
и какой объём видимых изменений входит в этот issue. Поведение в пограничном
случае; какая из персон важнее в конфликте; что считать приемлемой деградацией;
относится ли смежное поведение сюда или становится отдельной задачей.
**Всё, чего пользователь не наблюдает, агенты решают сами** либо согласовывают
между собой: где хранится состояние, в каком модуле стоит гвард, именование,
раскладка файлов, стратегия тестов, механика миграции. Решение записывается явным
блоком в конце ТЗ — «принято предположительно, поменять свободно», и ревьюер
вправе его оспорить. Технический спор автора и ревьюера решается вердиктом, а не
владельцем; до него он доходит только при исчерпании лимита циклов (§4).
**Смешанный вопрос делится, а не эскалируется целиком.** «Где живёт это
состояние» — техническое. «Переживает ли оно перезагрузку страницы и общее ли оно
для всех экранов» — продуктовое.
Вопросы задаются **одним комментарием, пачкой**, каждый в форме: что неясно ·
что изменится от ответа · **предлагаемый вариант по умолчанию**. Вопрос с готовым
вариантом стоит владельцу пяти секунд, вопрос без него — пяти минут. Пока ждём
ответа, issue остаётся в `S3-spec` и получает `blocked`: статус не подменяется,
`blocked` его дополняет, иначе конвейер считает задачу в работе, а она стоит.
### 7.2 Шаблоны комментариев
Короткие и однообразные, чтобы читались и человеком, и машиной.
- **Аналитика:** `Оценка: ценность N/10 · сложность N/10 · P<1-3> · тип ·
поверхности: … · дубликаты: … · лёгкий трек: да/нет`
- **Занятие:** `Взял: <роль> · сессия <id> · ветка issue/NN-slug`
- **Хендофф:** `Сделано: … · Файлы: … · Гейты: <команда → результат> ·
НЕ сделано: … · Риски: … · Следующий статус: … · Новые issue: #…`
- **Вердикт ревью:** `Вердикт: зелёный/жёлтый/красный · заход r<N> ·
блокирующих циклов K/<лимит> · High: N · Medium: N → в задаче | #… ·
Документ: docs/reviews/…`
(«→ #…» — только у Medium вне скоупа; находки в скоупе возвращаются автору.
Заход — номер прогона ревью, K — израсходованный бюджет §4: зелёные вердикты
его не тратят, поэтому заход и K расходятся, #227)
- **Закрытие:** `Выпущено в <тег беты> · CI: <ссылка> · Changelog: <ссылка>`
**Вперёд двигает только зелёный вердикт.** Жёлтый и красный возвращают автору;
разница между ними содержательна для человека, но не для маршрута. Первая
редакция конвейера (§10.4) пропускала жёлтый при `High: 0`, и первый же живой
прогон показал, почему это неверно: жёлтый там означал, что AC описывает неверное
изменение контракта — реализовать такое ТЗ значило бы сделать ошибку по инструкции.
---
## 8. Гейты
**Локальный гейт перед выходом из «В разработке»** — минимальный набор,
покрывающий изменённые поверхности (действующее правило владельца):
```
npx tsc --noEmit
npm test
npm run build && node scripts/bundle-policy.mjs --verify HEAD
# сборка цела; копии сверяются только на кандидате (#657).
# Копия стенда — `npm run bundle:sync` (#255); перед коммитом `npm run bundle:clean`
node scripts/smoke-select.mjs --base origin/dev --head HEAD # какие смоки относятся к диффу
node demo/smoke_<целевые>.mjs
node scripts/no-new-any.mjs --base origin/dev --head HEAD # новый код не добавляет any
npm run golden:verify # если менялся визуал
node scripts/check-docs.mjs # если менялся src/**
node scripts/model-invariants.mjs --config <экспорт> # если правилась геометрия или ссылки
python -m pytest tests_backend -q # py3.14 как в CI (npm run toolchain:check), если менялся бэкенд
npx tsc -p tsconfig.junction-parity.json && node scripts/fix-test-build.mjs \
&& python tests_backend/junction_parity.py --build-dir=test-build/junction-parity
# если менялось одно из зеркал junction limits
```
**Новый код не добавляет `any`** (#342). Явного `any` в `src/**` — сотни
вхождений (`node scripts/no-new-any.mjs --total`); перетипизировать это одним
заходом — месяц риска ради нуля пользовательской ценности, поэтому долг
снимается при плановом извлечении подсистем (#425, прежний #34), а не разовой
заменой. Гейт `scripts/no-new-any.mjs` судит
**только добавленные строки**: существующий долг на нетронутой строке законен,
правка строки со старым `any` — новая ответственность. Исключение объявляется на
той же строке, `// any-ok: <конкретная причина>`; голый маркер и причины вида
«todo» не проходят. Текст разбирается парсером TypeScript, поэтому слово «any» в
комментарии, строке или идентификаторе ложных срабатываний не даёт.
**Объём гейтов на код-ревью соразмерен задаче** (issue #127). Всегда:
`typecheck`, `npm test`, `npm run build` с `bundle-policy --verify` (копии сверяются на кандидате, #657), а при
любом diff'е по `src/**` — ещё и `node scripts/check-docs.mjs`. По
необходимости, определяемой diff'ом и AC: браузерные смоки (сколько их —
считает `ls demo/smoke_*.mjs | wc -l`, вшитое число здесь трижды отставало от
дерева; прогон всех уместен только когда задача задевает всё; какие относятся к
диффу, печатает
`node scripts/smoke-select.mjs --base origin/dev --head HEAD`, и его вывод
прикладывается к ревью вместе с решением по каждой строке), `golden:verify` при изменении видимого
результата, `pytest tests_backend` при правках в Python, performance-профили при
названном в AC влиянии. **Полные наборы — предрелизный гейт, а не гейт ревью.**
Скриншоты снимаются **только** джобой `Docs screenshots` (`workflow_dispatch`) и
принимаются локально: `npm run docs:accept -- --reviewed --from=<распакованный
артефакт>` (#246). Съёмка на своей машине даёт байтово другой PNG при том же
кадре, и набор из «не того» браузера переписывает все десять файлов без единого
содержательного изменения. Приёмка отказывает, если кандидат снят не с этого
дерева, не тем капчуром, не называет свой Chromium или неполон; коммит делает
человек.
Когда правка `src/**` кадров не меняет — а это большинство правок — CI-цикл не
нужен (#512): `npm run docs:accept -- --identical` снимает кадры локально,
декодирует оба набора в Chromium и при нуле отличающихся пикселей во всех
кадрах обновляет только отпечаток исходников в `screenshots.json`; байты
закоммиченных PNG, их sha, браузер и упаковщик съёмки остаются прежними. Хотя бы
один отличающийся пиксель — отказ с перечнем кадров и штатный путь через артефакт.
`check-docs` стоит в обязательной части не по важности, а по механике: отпечаток
скриншотов документации считается по всему `src/**`, поэтому **любая** правка
фронтенда делает его устаревшим. Выборка «по diff и AC» здесь не работает — diff
всегда попадает, и решать нечего. Цена пропуска измерена: скриншоты не
пересняли в #230 и #234, и `dev` стоял с красным job `docs`, пока это не нашли
при следующей задаче (#237). Пересъёмка — по двум абзацам выше, коммит
вместе с задачей.
**Перф-смок в Validate зависит от диффа** (#473). Два glow-профиля
гоняются всегда; при правке `src/iso-*` добавляется `large-house-isometric-v1`,
при правке `src/live-*`, `src/render-*`, `houseplan-render-lifecycle.ts`,
`houseplan-card.ts` — `large-house-interaction-v1`, оба по три образца против
абсолютных потолков `hardMaxMs` полных профилей (`budgets-*-smoke.json`).
Это гейт на «в разы», а не «на проценты»: регрессия #160 (первый кадр 9 870 мс
против потолка 3 500) ловится ещё в ревью, а не предрелизным гейтом под тегом.
Классификацию делает `scripts/classify-changes.mjs`, набор профилей входит в
ключ переиспользования `performance_smoke`. Ревьюер по-прежнему принимает
зелёный Validate на SHA как подтверждение дешёвых гейтов — смок его часть.
Условие честности такого сужения: ревьюер обязан перечислить, какие гейты прогнал,
какие нет и почему. Непрогнанный гейт становится видимым решением, а не молчаливым
пропуском.
**Одно число — один источник.** Любая величина, которую пользователь видит
дважды — превью против записи, подпись против площади, подсветка инструмента
против сохранённого значения, — обязана считаться в одном месте. Три дефекта
подряд имели ровно эту причину: #234 (резинка показывала 12 см, запись хранила
24), #233 (подпись мерила по осевым линиям, площадь рядом — по полу) и способ,
которым #234 нашли (подсветка «Толщины» врала согласованно с записью). Ревьюер
отвечает на вопрос прямо: какое число в этом диффе видно дважды и один ли у него
источник. Механическая часть правила закреплена тестом
`test/single-source-numbers.test.mjs` — строку с единицей измерения собирает
только канонический форматтер; смысловая часть остаётся за ревью.
**Гейт беты** (условие закрытия issue): CI Validate зелёный на точном SHA тега.
Часть гейтов запускается только здесь, то есть **после** пройденного код-ревью.
Упавший предрелизный гейт автор чинит и повторно прогоняет; зелёный прогон
достаточен для продолжения релиза, повторное код-ревью не требуется — §11.4.
**Гейт стабильного релиза:** полный локальный прогон плюс Validate и Full
Performance зелёные на точном SHA, плюс зелёный E2E на реальном Home Assistant:
`release.yml` сам запускает `e2e.yml` в `houseplan-e2e` на SHA кандидата и ждёт
его зелёного (#514, #540); установочные ассеты публикуются только после всех
гейтов и только этим workflow — релиз, опубликованный руками, возвращается в
черновик до их прохождения (#540); статусов issue не касается.
---
## 9. Метки — канонический статус
Статус читается из меток: их видно в списке issue, их читает любой токен с
доступом к Issues, и по ним же работает конвейер — смена метки порождает событие
(§10.4). **Project v2 не используется** (решение владельца 2026-08-14): второе
представление статуса рядом с метками требовало отдельного скоупа токена,
синхронизации и внимания, а давало вид доски. Два источника одного факта
расходятся — это уже случалось с колонкой «Статус ТЗ» в `docs/specs/README.md`.
**Имена меток английские** (решение владельца 2026-08-12). Русские имена в этом
документе были только на бумаге; репозиторий с самого начала жил на английских.
| Метка | Статус |
|---|---|
| `S1-new` | Новое, не разобрано |
| `S2-analysis` | Аналитика и оценка |
| `S3-spec` | ТЗ в работе |
| `S4-spec-review` | ТЗ на ревью |
| `S5-ready` | Готово к разработке — единственный статус, из которого можно начать трогать код |
| `S6-in-progress` | В разработке, занято исполнителем |
| `S7-code-review` | Код-ревью |
| `S8-merged` | Ревью пройдено, код в `dev`, ждёт беты. Issue закрывается пачкой при выпуске |
| `blocked` | Ждём внешнего или владельца, **поверх** статусной метки |
| `rejected` | Отклонено, issue закрыт |
Модификаторы: `small` (лёгкий трек, сложность ≤3), `trivial` (короткий трек,
§5.1), `hotfix`, `process`, `review-4`; приоритет `P1`/`P2`/`P3`; тип `bug`/`feature`/`tech-debt`.
Тематические метки (`polish`, `infra`, `tests`, `docs`, `security`, `vacuum`)
ортогональны процессу.
Инварианты: продуктовая задача в процессе несёт **ровно одну `S*`-метку**.
Инфраструктурная задача может не иметь `S*` во время первоначальной реализации;
с первого `S7-code-review` на неё действует тот же инвариант ровно одной метки.
Закрытый issue статусных меток не несёт; `blocked` не заменяет статус, а дополняет
его.
**Чужой issue берётся в работу так же, как свой — после явного решения
владельца** (решение владельца 2026-08-13, уточнено в тот же день). Репозиторий
публичный, отчёты заводят и посторонние; проверка стоит **на входе**, а не на
каждом шаге.
Для продуктовой задачи входом служит присвоение первой статусной метки. Для
инфраструктурной — явное назначение владельцем; до готовности к первому
код-ревью она может оставаться без `S*`. Как только продуктовая задача вошла в
полный маршрут либо инфраструктурная получила `S7-code-review`, **кто её завёл,
дальше не имеет значения** — статусы, ревью и лимиты работают одинаково.
Присвоение метки и есть то самое явное решение, причём проверенное платформой:
метки может ставить только тот, у кого есть право записи в репозиторий. Прежняя
редакция требовала переоформлять чужой отчёт своим issue со ссылкой на исходный;
это оказалось работой впустую — на #123 к моменту отказа ТЗ уже было написано.
`S8-merged` появился позже остальных и закрывает разрыв, который раньше
закрывался памятью человека: код принят, но бета ещё не вышла, и issue закрывать
рано. Без него принятая задача либо висела в `S7-code-review`, либо закрывалась
досрочно.
---
## 10. Механизация при прямых коммитах в `dev`
Решение владельца — работать без PR. Значит, GitHub не может ничего заблокировать
на своей стороне: **основной гейт переезжает на клиента, CI остаётся страховкой.**
### 10.1 Хуки, которые невозможно забыть поставить
`.githooks/` в репозитории, `core.hooksPath` выставляется автоматически при
установке зависимостей:
```json
"scripts": { "prepare": "node scripts/install-hooks.mjs" }
```
`npm ci` вызывает `prepare` сам — значит, хуки появляются в каждом окружении,
включая свежий контейнер облачного агента, без отдельного шага в инструкции.
- **`commit-msg`** — есть, работает. Отклоняет коммит без терминального
`Issue: #NN`, требует ровно один `User-Visible: yes|no`, а для коммитов,
трогающих `demo/golden/baselines/**`, — `Release:` плюс ровно один источник:
`Baseline-Reviewed: <URL GitHub run>` либо
`Baseline-Reviewed-Local: sha256:<хеш аттестации>`. Локальный хеш обязан
совпадать с `localAttestation.sha256` в принятом индексе.
Реализация — `scripts/validate-commit-provenance.mjs`, тот же скрипт вызывается
job `provenance` в `validate.yml`.
- **`pre-push`** — есть, работает. Прогоняет `scripts/process-gate.mjs` по каждому
пушимому ref и останавливает push при нарушении. Это и есть блокирующий гейт
вместо PR. Удаление ветки и теги пропускаются: в первом случае проверять нечего,
во втором коммит уже проверен, когда его пушили. Для новой ветки диапазон
считается от `merge-base` с `origin/dev`, а не от начала истории — иначе в него
попали бы все нарушения, совершённые до появления гейта.
При возврате `main` в `dev` диапазон merge-коммита содержит второй родитель —
уже опубликованные в `main` коммиты с закрытыми issue. Для destination `dev`
общий скрипт pre-push/CI исключает только SHA, доказанно достижимые из
`origin/main`; сам merge и новые post-merge коммиты остаются под всеми
проверками. На `main`, beta/issue-ветки и обычный push в `dev` это исключение
не распространяется (issue #155).
Проверка статуса issue требует `gh`, поэтому при его отсутствии хук печатает
предупреждение и выполняет только офлайн-часть. Это сознательная уступка: хук,
который не работает в самолёте, отключают целиком, а строгий проход всё равно
делает CI.
**Хук обязан быть исполняемым, и это тише всего ломается.** Git **молча** не
запускает файл без бита `+x`: гейт сообщает об успехе тем, что его нет. Проверено
на настоящем push — при `644` от гейта ноль строк и push проходит, при `755` он
останавливается.
Через GitHub API режим не выставляется: файл, отправленный так, приезжает
`100644`. Поэтому `scripts/install-hooks.mjs` восстанавливает бит при каждой
установке зависимостей, а `assertHookMode` дополнительно проверяет бит
`.githooks/commit-msg` в индексе. Правится вручную:
`git update-index --chmod=+x .githooks/<хук>`.
### 10.2 Что проверяет `process-gate.mjs`
Реализовано, `scripts/process-gate.mjs`, issue #105. Офлайн, без GitHub API:
1. трейлер `Issue: #NN` у каждого коммита класса A/B, допускается несколько;
2. имя ветки `issue/NN-slug` соответствует трейлерам;
3. у класса A есть ТЗ: раздел `## ТЗ` или хотя бы один `AC1` в теле issue —
либо архивный `docs/specs/NN-*.md` у задачи до 2026-09-10. Офлайн тела нет,
и проверка молчит; с `--issues` — предупреждение (настоящий рубеж — ревью ТЗ).
Добавление нового файла в `docs/specs/**` тоже предупреждение: каталог
заморожен (#517);
4. `User-Visible: yes` → правки в обоих changelog в том же коммите;
5. коммит только класса D невалиден без `Release: vX.Y.Z`,
`Baseline-Reviewed: <ссылка на прогон CI>` либо
`Baseline-Reviewed-Local: sha256:<хеш аттестации>`;
6. релизный коммит не содержит изменений в `src/` и `custom_components/**/*.py`;
7. документов ревью на один issue не больше четырёх (`-r1`…`-r4`).
С токеном GitHub:
8. `--issues` тянет каждый упомянутый issue и требует метку из
{`S5-ready`, `S6-in-progress`, `S7-code-review`, `S8-merged`}; закрытый,
недоступный или помеченный `blocked` — отказ (**fail closed**).
Три оговорки к проверке 8 выяснились при реализации.
**`S8-merged` входит в множество**, хотя по смыслу задача уже принята. Причина
механическая: конвейер (§10.4) сливает ветку в `dev` **раньше**, чем ставит метку,
Validate стартует от этого push и успевает прочитать issue уже в `S8-merged`.
Строгое множество красило бы каждую принятую задачу. Локальная строгость
возвращается флагом `--no-merged`.
**Статус спрашивается только у коммитов класса A/B.** Правило №1 говорит о
продуктовом коде и инструментах, а не о документации. Иначе краснел бы каждый
документ ревью: он ложится в ветку задачи, пока та в `S4-spec-review` или
`S7-code-review`, то есть заведомо вне рабочего множества.
**При продвижении в `main` не перепроверяются коммиты, уже достижимые из
prerelease-тега.** После выпуска беты их issue по §2.8 должны быть закрыты, а
stable fast-forward снова включает эти коммиты в диапазон `old-main..candidate`.
Pre-push передаёт целевую remote ref через `--target-ref`, а Validate — через
`TARGET_REF`; оба исключают только уже опубликованную prerelease-историю. Любой
post-beta коммит остаётся в проверке и по закрытому issue отклоняется fail-closed.
Не реализовано и остаётся долгом:
9. `npm run release:prerelease -- --issues=…` не проверяет, есть ли у issue
зелёный вердикт код-ревью.
Закрытие вошедших issue автоматизировано (#120, #547). При старте беты текущая
очередь S8 служит только списком кандидатов: `RELEASE-MEMBERSHIP.json` оставляет
из неё лишь номера с доказанным `Issue: #NN` в Git-диапазоне зафиксированного
SHA. Manifest публикуется и входит в `SHA256SUMS`; свежая очередь S8 после
публикации не перечитывается. Общий для workflow и локальной команды bookkeeping
идемпотентно добавляет один маркированный release-комментарий, снимает все
статусные метки и закрывает issue. Поэтому повтор после сбоя продолжает manifest,
даже если метка уже снята или release уже public; автор issue на membership не
влияет.
### 10.3 Страховка и разбор
- **`process-gate.mjs` — job `process-gate` в `validate.yml`**, без `needs`:
краснеет сам и не роняет остальные. При прямом push проверка догоняющая: код уже
в `dev`, CI краснеет после. Это принятая цена отказа от PR: `pre-push` ловит
нарушение до отправки, а этот job — то, что прошло мимо хука, включая
`--no-verify` и окружение без установленных зависимостей.
- **Нарушение не откатывается force-push'ем** (правило 17): исправляющий коммит
плюс issue с меткой `process`. Починить надо проверку, а не только симптом.
- **Еженедельная гигиена** (workflow): issue в `S1-new` дольше 14 дней и в
`S6-in-progress` дольше 7; issue класса A в `S5-ready` без ТЗ; issue с нулём или
двумя `S*`-метками; коммиты без трейлера за неделю — **цель 0**; rework rate и
число issue, дошедших до `review-4`; **баги, заведённые после закрытия беты** —
прямая цена отказа от фазы тестирования.
### 10.4 Событийный конвейер: метка как триггер
`.github/workflows/process.yml` (тело — `_process.yml`, #623), issue #114. Смена статусной метки — не запись в
журнал, а **сообщение**: она порождает событие, событие запускает следующий шаг.
```
S4-spec-review → ревью ТЗ → S5-ready либо возврат в S3-spec
S7-code-review → код-ревью → слияние в dev → S8-merged либо возврат в S6-in-progress
```
Текущая техническая реализация независимого ревьюера —
`anthropics/claude-code-action`; это деталь автоматизации, а не закрепление роли
или вида задач за Claude. Ревьюер читает `docs/SCOPE.md`, `AGENTS.md`,
конспект `docs/process/REVIEWER.md` с разделами этого документа по его ссылкам
(#634) и тело issue, публикует разбор комментарием, заводит issue на
Medium-находки вне скоупа задачи (#202), кладёт документ в `docs/reviews/` ветки
задачи и возвращает вердикт структурированным JSON. **Метку переставляет отдельная
детерминированная стадия по вердикту, а не модель.**
Четыре вещи, без которых конвейер молча не работает:
1. метки переставляет **PAT**, а не `GITHUB_TOKEN`: GitHub намеренно не порождает
события от `GITHUB_TOKEN`, чтобы не было циклов, и цепочка обрывалась бы после
первого шага без ошибок в логах;
2. для события `issues` GitHub берёт workflow только из **ветки по умолчанию**
(`main`), независимо от содержимого `dev`. Поэтому в `main` лежит тонкий
`process.yml` — триггер, run-name, потолок прав, — а тело `_process.yml` он
вызывает по ссылке `@dev` (#623). Правило ниже, «Workflow из ветки по
умолчанию», — общее для всех таких файлов;
3. слияние в `dev` происходит **до** простановки `S8-merged`, иначе метка врёт в
промежутке — она утверждает, что код в `dev`;
4. многострочный текст внутри `run:` — только через heredoc: строка с нулевым
отступом обрывает блок YAML, и скрипт обрезается без ошибки парсера.
**Workflow из ветки по умолчанию: тонкий файл и тело из `dev`** (#623). Для
событий `issues`, `schedule` и `workflow_run` GitHub исполняет workflow из
`main`. Таких файлов шесть: `process.yml`, `process-resume.yml`,
`process-reconcile.yml`, `mutation-gate.yml`, `nightly.yml`,
`process-metrics.yml`. Каждый — тонкий вызывающий: триггеры, run-name,
права, concurrency и одна job `uses:
Matysh/houseplan-card/.github/workflows/_<имя>.yml@dev` с `secrets: inherit`.
Тело `_<имя>.yml` читается из `dev` в момент запуска, поэтому **правка
конвейера — один коммит в `dev`**, зеркало в `main` и возврат `main` в `dev`
перед промоушеном не нужны. Потолок прав вызывающей job равен объединению
прав job тела: вызываемый workflow права только сужает, и каждая job тела
получает прежний минимум (#556). Тонкий файл меняется, только когда меняются
триггеры, входы ручного запуска или потолок прав; тогда он зеркалится в
`main`, и preflight `workflow_sync` в `validate.yml` держит копии равными —
сверяются ровно эти шесть файлов, список держит
`test/default-branch-workflows.test.mjs`. `performance.yml` в список не входит:
по расписанию он судит `main` собственным телом из `main`.
**Ревью не начинается на красном коде** (#510). После фиксации материала конвейер
запускает Validate с мутантами по диффу на этом SHA (`scripts/validate-gate.mjs`:
`workflow_dispatch validate.yml -f mutants=true`). **Ждёт его не раннер, а событие**
(#636): подготовка убеждается, что dispatch встал на материал, кладёт запечатанный
маркер ожидания `review-pending-…` и завершается; по завершении Validate
`process-resume.yml` (`workflow_run`) переставляет метку `S7-code-review`, и новый
прогон конвейера находит завершённый dispatch сразу. Страховка на потерянное
событие — `process-reconcile.yml`: успешный прогон подготовки с маркером и уже
завершённым Validate он будит повторной меткой, без маркера — как прежде, только
диагностика. Будить без маркера нельзя: это второй вызов модели. Красный или
пропавший прогон возвращает задачу в `S6-in-progress` с комментарием и ссылкой —
код никто не читал, цикл ревью не израсходован. Мутанты по диффу вообще бегут
только по явному запросу: на кандидате ревью, кандидате слияния (#492) — оба
диспатчат Validate с `mutants=true` — и на PR, где Validate единственный сигнал;
обычный push обходится дешёвыми гейтами (~3 минуты). За 08–09.09 мутанты на
каждом промежуточном пуше стоили 48 из 56 часов job-минут Validate и в основном
отменялись следующим пушем. **Кандидат беты (`Release:`), `full=true` и ночь
мутантов не запрашивают** (#601, решение владельца 20.09): мутационный гейт
проверяет тесты, а не продукт (#513), к бете каждая задача прогнана им дважды —
на ревью и на слитом после ребейза кандидате, — а ночью идёт полный реестр
(`mutation-gate.yml`, 00:43 UTC). Релизный гейт (#541) требует полного Validate,
но не mutant-jobs; для ревью и слияния шесть исполненных mutant-jobs остаются
обязательными.
Ожидание gates, работа модели и публикация/интеграция — три независимых jobs
(#551) с отдельными бюджетами 55, 45 и 55 минут. Поэтому долгий Validate не
съедает время модели (с #636 — и не занимает раннер: до этого подготовка спала
≈ 28 минут на раунд при 10–12 минутах работы модели), а ожидание кандидата после
зелёного вердикта не обрывает готовый review. Между jobs передаётся запечатанный artifact: run/attempt, issue,
этап, раунд, branch, SHA/tree материала, якоря ТЗ и результат Validate. Получатель
сверяет полный набор файлов, SHA-256 и все поля с outputs предыдущей стадии;
неполный, чужой или устаревший результат fail-closed не публикуется и не разрешает
merge. Timeout/cancel/failure называет конкретную стадию и оставляет метку на месте;
если модель не запускалась, цикл ревью не расходуется. Длительности всех трёх
стадий печатаются отдельной таблицей в summary прогона.
Каждый раунд ревью платит только за то, что в нём изменилось (#518). Свидетель
судится по **области своего якоря** — строкам патча плюс сорок строк с каждой
стороны (`ANCHOR_RADIUS_LINES`): и в отпечатке журнала (#481), и в отборе по
диффу, который читает ханки `git diff --unified=0`. Сторона гарда осталась
файловой: у гарда якоря нет. Неоднозначный якорь и непрочитанные ханки дают
прежний широкий ответ — незнание не доказательство. Приближение того же класса,
что и сам отбор по диффу; нижняя граница — ночной полный гейт (#513). Шард
считает свой план до установки окружения и при пустом плане не платит за
npm ci, Python и Chromium, оставаясь исполненной job: доказательство гейта
требует успешной job, а не пропущенной.
**Один хендофф — один пуш.** Перед пушем — локальный `node scripts/process-gate.mjs
--issues` при доступном `gh` (хук без `gh` статус issue не проверяет и молчит);
после `S7-code-review` в ветку не пушить, пока не пришёл вердикт или возврат: пуш
поверх идущего ревью отменяет его и стоит 10–20 минут раннера, а после фиксации
материала — ещё и слияние (#312). `S7` ставится один раз на заход, не после
каждого фикса CI: красный Validate конвейер вернёт сам.
**Автор обязан дождаться вердикта, а не заканчивать сессию.** Ревью идёт от десяти
минут до сорока пяти. Отчёт «передал на ревью» останавливает конвейер там, где он
мог идти сам: вердикт придёт, а подхватить его будет некому. У агента нет часов —
он существует только в момент своего хода, поэтому ожидание это опрос: раз в 90
секунд, не более 110 попыток (запас на три независимых бюджета #551) —
`node scripts/wait-verdict.mjs --issue NN` делает его
детерминированно и говорит только при смене состояния (#496). Смотреть на метку, а
не на комментарий: метка и есть состояние. Комментарии конвейера до последнего
применения `S4`/`S7` считаются историческим baseline, а уже опубликованный исход
текущего раунда доставляется сразу при первом опросе (#546). При `blocked` не
ждать — задача ждёт владельца.
**После прогона ревью метка меняется всегда.** Инвариант появился не сразу: первая
редакция при конфликте слияния оставляла метку на месте, и это оказалось тупиком —
автор ждёт смену метки, метка не менялась, и он тридцать раз опрашивал впустую,
чтобы отчитаться «лимит исчерпан» при зелёном вердикте. Состояние, из которого
никто не может выйти и о котором никто не узнает, для конвейера хуже громкой
ошибки.
**Ветка приводится к `dev` до ревью, а не после** (#257). Раньше ревью читало ветку
как есть, а слияние делало ребейз — проверенный SHA и слитый SHA были разными
коммитами. Пока расхождение с `dev` текстовое, ребейз упирается в конфликт и это
видно; смысловое расхождение git склеивает молча, и в `dev` уезжает комбинация,
которую ревьюер не читал. Именно так пришёл регресс #234. Шаг перед ревью делает
одно из трёх:
- ветка уже содержит весь `dev` — ничего;
- отстала и ребейзится чисто — ребейз, `push --force-with-lease`, ревью по
приведённому состоянию. Факт ребейза передаётся в промпт, чтобы сработало
правило §2.10 о полном разборе вместо дельты;
- конфликт — возврат в `S6-in-progress` **до** запуска ревью. Цикл при этом не
расходуется: код никто не читал, вердикта нет.
Проверка стоит до ревью не только ради совпадения SHA. Конфликт всё равно вернул бы
задачу, но обнаруживался он после сорока пяти минут работы ревьюера и потраченных
лимитов подписки, хотя виден за пять секунд до них.
`--force-with-lease` здесь обязателен с явным ожидаемым значением: между чтением
ветки и пушем автор мог запушить коммит, и слепой `--force` потерял бы его молча.
Расхождение lease — падение прогона, а не предупреждение.
**В `dev` уезжает точный кандидат, и только проверенный** (#492,
`scripts/merge-candidate.mjs`). Ревью длится десятки минут, `dev` за это время
двигается; ребейз после вердикта даёт дерево, которого никто не видел, — а чистый
ребейз ничего не доказывает: соседняя правка в `dev` меняет поведение без единого
конфликта. Шаг слияния поэтому:
- сверяет вершину ветки с материалом ревью (#312) — иначе `S6-in-progress`;
- если `dev` не двигался — push с `--force-with-lease` на текущую вершину;
- если двигался — ребейз (конфликт — `S6-in-progress`, как раньше), сравнение
patch-id проверенного и получившегося диффа (различие — `S7-code-review`: вердикт
к другому диффу не применим, §2.10), публикация кандидата в ветку задачи, запуск
Validate с мутантами на ней (#510) и ожидание зелёного dispatch-прогона **на этом
SHA** — push-прогон мутантов не несёт — и только затем push в `dev` с lease на ту
вершину, поверх которой кандидат собран. Отклонённый lease — `dev` двинулся снова
— новая попытка; после третьей — `S6-in-progress` с комментарием;
- красный Validate на кандидате или прогон, не появившийся за три минуты, —
`S6-in-progress` с ссылкой; `S8-merged` ставится только после push.
Проверка кандидата — обычный Validate ветки: лёгкий набор плюс диффозависимые
гейты. Тяжёлые гейты остаются за кандидатом релиза (#479): слияние не превращает
каждое движение `dev` в двадцатиминутный прогон, а проверяет ровно то, что
проверил бы пуш той же дельты.
Поэтому зелёное код-ревью с неудавшимся слиянием ведёт не в `S8-merged`, а в
`S6-in-progress`: работа действительно вернулась к автору, только осталась не
правка кода, а ребейз. Вердикт при этом в силе, переделывать нечего. После ребейза
метка `S7-code-review` возвращается и ревью идёт заново — не формальность:
после ребейза на ушедший вперёд `dev` это другой код.
Если метка не сменилась, значит упал сам прогон, а не работа: смотреть логи и
сообщать владельцу, а не продолжать опрос.
**Очередь S4/S7 сверяется отдельным bounded controller (#555).** Workflow
`process-reconcile.yml` раз в полчаса делает один снимок открытых задач и
завершается — активного polling одинакового состояния и вызова модели на каждый
тик нет. Он сопоставляет последнюю постановку `S4`/`S7` с run по номеру задачи и
этапу; если стадия успела подготовить материал, дополнительно проверяет
запечатанные run/attempt, SHA/tree, список блобов ТЗ и номер раунда. Текущий
`blocked`, `review-4` или снятая review-метка всегда сильнее старого события.
Автоматически и не более одного раза повторно применяется только сама review-метка после доказанно
потерянного события либо transient-исхода до получения запечатанного результата
(`cancelled`, `timed_out`, `stale`, `startup_failure`, `skipped`). Это не применяет
вердикт и не расходует цикл: обычный конвейер заново читает актуальные labels и
материал. Здоровый running run не трогается. Failure guard, неизвестная связь,
чужой material/stage/attempt, success без смены метки и сбой после появления
`review-result`, а также потеря повторного события получают один дедуплицированный диагностический комментарий и
эскалацию человеку — второй вызов модели или S8 по догадке запрещены. Перед любой
записью controller перечитывает состояние; итог каждого прохода публикуется как
`houseplan-process-reconcile/v1` artifact.
**Конвейер — идемпотентный контроллер, а событие лишь будит его** (#499). Guard
читает метки issue текущими, а не из снимка события: прогон мог простоять в очереди,
пока владелец снял метку — отозванный запрос не исполняется, и комментария об этом
нет. Конвейер запускают только `S4-spec-review` и `S7-code-review`; остальные метки
не создают ни одной job и не входят в concurrency-группу issue — прежде любая
посторонняя метка вытесняла ожидающий запуск ревью. Зелёный вердикт применяется
повторно **без вызова модели**, если последний документ этапа несёт записанный
конвейером вердикт `green` с High 0 и дерево материала не изменилось ни в одном
файле вне `docs/reviews/**` (сравнивает `git diff` по содержимому). Ребейз, правка
теста, фикстуры или ТЗ дают отличие дерева и полный разбор — правило §2.10 не
ослабляется, оно просто не касается дерева, которое уже читали.
Цикл считается **по этапу**: вердикт по ТЗ не расходует бюджет код-ревью. Раньше
считались все вердикты подряд, и первое код-ревью #89 получило `r2/4`.
---
## 11. Исключения
### 11.1 Лёгкий трек
См. §5 — это не исключение из правила №1, а более дешёвый путь по тем же статусам.
### 11.2 Аварийный хотфикс (метка `hotfix`, решение владельца)
Разрешено писать код до появления issue. Обязательно:
- issue создан в **той же сессии до коммита**, метка `hotfix`;
- ТЗ «как сделано» + раздел «почему нельзя было ждать»;
- в течение 24 часов задача ретроспективно проходит код-ревью;
- аварийность названа явно в релизном хендоффе.
### 11.3 Гигиена репозитория
Механические изменения без изменения поведения (форматирование, мёртвые файлы)
идут под квартальный umbrella-issue «Гигиена репозитория»; каждый коммит
ссылается на него. Трассируемость 1:1 сохраняется.
### 11.4 Починка предрелизных гейтов без повторного код-ревью
Решение владельца 2026-08-13.
В цикле реализации гоняется только лёгкий набор — typecheck, unit, build (§8).
Golden, браузерные смоки, performance и полный HA-харнесс запускаются перед бетой,
то есть **после** того, как код-ревью пройдено и issue в `S8-merged`. Часть
проблем физически не может быть найдена раньше.
**Если предрелизный гейт упал, автор правит, повторно прогоняет упавшее, и
зелёного прогона достаточно, чтобы релиз продолжился.** Issue остаётся в
`S8-merged` и на повторное код-ревью не отправляется.
Причина: полный цикл ревью в момент выпуска стоит дороже, чем риск, который он
здесь снимает. Гейт уже назвал дефект точно, а исправление проверяется тем же
гейтом — то есть проверка объективна и не зависит от чьего-либо суждения.
**Что при этом обязательно:**
- прогон упавшего гейта записан в issue: **точная команда и её результат**.
«Verified» без команды доказательством не является (§8);
- трейлеры на коммите как обычно, `Issue: #NN` того же issue;
- при `User-Visible: yes` — правки в оба changelog в том же коммите;
- эталоны golden принимаются только через `npm run golden:accept -- --reviewed`
на полном Linux-артефакте GitHub CI либо полном аттестованном WSL-артефакте;
после локальной приёмки полный GitHub Validate на точном финальном SHA всё
равно обязателен. «Чтобы гейт позеленел» основанием не является.
**Границы, за которыми исключение не действует.** Оно про починку названного
гейтом дефекта, а не про продолжение разработки под видом починки. Правка идёт
обычным путём — новым issue либо возвратом в `S6-in-progress` — если она:
- меняет контракт поведения или добавляет пользователю что-то новое;
- задевает подсистему, которой в исходной задаче не было;
- по объёму сопоставима с самой задачей;
- меняет сам гейт вместо кода — правка теста, чтобы он перестал падать, это не
починка, а сокрытие. Исключение — когда дефект **в фикстуре** и это доказано
разбором, как на #89: солнце на азимуте 180° и единственное окно на северной
стене, поэтому луч честно не строился.
Границу определяет автор, и здесь процесс сознательно отдаёт ему то, что в
остальных местах не доверяет — оценку собственной работы. Плата за скорость в
единственной точке, где цикл ревью стоит дороже всего. Компенсируется тем, что
запись в issue публична и релиз-менеджер видит, что именно было сделано перед
выпуском.
Это исключение из правила «код-ревью не пропускается никогда» (§5, §7.1) —
единственное, и относится только к окну между `S8-merged` и выпуском.
### 11.5 Независимое ревью линии перед стабильным релизом
Решение владельца 2026-09-25, issue #638.
**Зачем.** Инкрементальное ревью судит дифф задачи против её ТЗ, а не
поверхность против пользователя. Пять раундов по эпику #591 не нашли того, что
нашли четыре независимых ревью перед аудитом 22.09: невидимый после крестика HA
диалог (#607), кламп по символу (#608), маршруты робота при импорте (#611),
детерминированный отказ релизного гейта (#619). Ни ветка `ha-dialog`, ни
посимвольный ввод не входили ни в один AC.
**Шаг.** Перед каждым стабильным релизом — одно ревью поверхностей, изменённых
всей линией бет, «с нуля»:
- **вход** — issue линии, доказанные трейлерами `Issue: #NN` в диапазоне
«прошлый стабильный тег..кандидат» (тот же построитель и та же схема, что
`RELEASE-MEMBERSHIP.json` беты, #547; метка S8 доказательством не является),
и изменённые продуктовые файлы. Собирает их
`scripts/release-review.mjs prepare`;
- **без ТЗ и без документов раундов**: основа суждения — `docs/SCOPE.md` и
`docs/USER-GUIDE.ru.md`. Проверка исполнением: бандл, стенд и смоки, пиннутая
фикстура `ha-dialog` (#505) там, где есть диалоги, посимвольный ввод,
настоящие Escape и крестик; для каждой поверхности — обычный сценарий и самый
рискованный соседний (§2.6, шесть классов риска);
- **выход** — `docs/reviews/RELEASE-REVIEW-vX.Y.Z.md` в `dev`, находки
High/Medium/Low с воспроизведением.
**Исполнитель** — модель в CI, `.github/workflows/release-review.yml`: три job
(вход, модель, публикация), модель без единого права на запись, документ, его
машинный блок, индекс и коммит — детерминированный шаг. Независимость
обеспечена построением: сессия свежая, ТЗ и раунды ей не даются.
**Выпуск не блокирует.** `release.yml` ставит ревью в очередь job
`independent-review` сразу после закрепления SHA кандидата — параллельно
гейтам; ни один job выпуска от него не зависит, его отказ — предупреждение.
Документ — рекомендация: владелец берёт находки в работу (issue в очередь
следующей беты) либо оставляет без действий. Автоматически находки в issue
не превращаются.
Повторный запуск на тот же тег модель не тратит, если документ уже в `dev`
(`force=true` — переснять). Ручной запуск:
`gh workflow run release-review.yml --ref dev -f tag=vX.Y.Z [-f candidate=<sha>]`.
Беты шаг пропускают. Первый прогон — линия v1.78.0.
### 11.6 Повторные Validate на одном SHA перед релизом
Решение владельца 2026-09-26, issue #656.
Релизный гейт рассматривает прогоны одного SHA от нового к старому. Отменённый
прогон и proof, который не соответствует запрошенной политике (например,
лёгкий вместо полного), вердиктом не являются: гейт проходит мимо них к
следующему совместимому proof. **Среди совместимых полных прогонов решает
новейший.** Поэтому поздний полный `failed`, `missing` или `pending` блокирует
более ранний зелёный proof; новый полный зелёный прогон может обновить старый
красный.
Это fail-closed правило. Content-addressed proof доказывает, что конкретный
прогон относится к кандидату, но не даёт старому зелёному прогону права скрыть
более позднюю проверку той же политики, которая нашла отказ. Чтобы продолжить
выпуск после такого отказа, исправляют причину и получают новый совместимый
полный зелёный прогон на том же финальном SHA либо на новом SHA кандидата.
---
## 12. Запрещено
- код без issue или из статуса раньше «Готово к разработке»;
- ТЗ, написанное после кода (кроме §11.2, и тогда с пометкой «как сделано»);
- ревью своей работы; перевод своей работы через ревью-гейт;
- пятый цикл ревью вместо разбора по §4;
- заведение issue вместо возврата на правки, чтобы обойти лимит циклов;
- принятие golden-эталонов ради зелёного CI или по частичному артефакту;
- закрытие issue до выпуска беты с зелёным CI;
- переоткрытие закрытого issue вместо нового бага;
- Medium-находки, оставленные как TODO в документе ревью: в скоупе — чинятся
в текущем issue, вне скоупа — становятся отдельным (#202);
- **параллельные бэклоги** в файлах (`BACKLOG-*.md`, «планы» в docs);
- ревью-документы вне репозитория;
- попутные правки «раз уж я здесь»;
- фича или материальное изменение поведения в стабильном релиз-коммите;
- force-push в `dev`;
- ручное копирование на домашний инстанс.
**Нарушение процесса — тоже issue** (метка `process`): если правило удалось
нарушить незаметно, виновата проверка.