Two-sided ratchets with zero slack made parallel tasks conflict on shared numbers, recompute them after every rebase and hit a ceiling because a neighbour merged first (#689 after #691). - Core lines (test/core-file-budget.test.mjs): a branch may grow up to CORE_BAND = 50 lines over the beta ceiling; shrinking no longer fails it. - Bundle graphs (bundle-budget.mjs): initial View and lazy graphs fail only above ceiling + 2 000 B; below the ceiling is not a branch finding. The absolute INITIAL_VIEW_GZIP_BUDGET stays the wall. - Monolith numbers (monolith-metrics.mjs, unused-locals-gate.mjs): METRIC_BANDS — 5 for delegates, port members and privates, 25 for host. refs, 2 000 B for dist/; a lower number is reported, not failed. - Browser mutation guards: 200 is a guideline — mutation-gate --check warns above it instead of failing; every guard still needs its reason line. - scripts/ratchets.mjs: `report [--warn]` and `tighten` — on the beta candidate the release manager sets every ceiling to the fact in one commit; release:prerelease prints loose ceilings as a warning. Canon: PROCESS.md §3 (browser guards, monolith numbers) and §8 «Храповики»; docs/TESTING.md. Issue: #699 User-Visible: no Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
143 KiB
Процесс работы над House Plan
Статус документа: канон (редакция 2026-08-13, ролевое уточнение 2026-09-13). Решения владельца, на которых он стоит: прямые коммиты в
devбез PR · канон статуса — метки, имена английские · трекиship/show/askзадаёт метка владельца (#695) · автор и ревьюер — независимые агенты/сессии · любой агент может взять любую роль · инфраструктурные задачи входят в общий флоу сразу наS7-code-review.Область действия: обязателен для владельца и для любого агента. Целиком его читает тот, кто правит конвейер, гейты или сам процесс, — сразу после
docs/SCOPE.mdиAGENTS.md, доdocs/STATUS.md; автор и ревьюер входят через ролевые конспекты ниже и открывают раздел канона по ссылке (#634, #701). Живёт в репозитории: до августа 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иCLAUDE.mdпапки владельца — только маршрутизаторы к канону и историческим инструкциям: правил в них нет, и в замер цены входа (scripts/entry-cost.mjs) они не входят — CI их не видит (#701). Текущие версии, состояние конкретных 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), на треке `show` 2
трек `show` (§5) идёт S2-analysis → S5-ready, минуя S3 и S4
трек `ship` (§5) идёт S1-new → S5-ready, минуя S2, 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:
- дубликаты проверены (ссылки на похожие issue);
- в скоупе по
docs/SCOPE.mdиdocs/TOUCH-SUPPORT.md; - пользовательская ценность 1–10 и ценность для разработки — что упрощает или разблокирует;
- сложность и риск 1–10 — трудоёмкость плюс вероятность задеть смежное;
- приоритет P1/P2/P3;
- тип: баг / фича / техдолг;
- затронутые поверхности (модули, диалоги, бэкенд, i18n);
- трек — метка
track:ship,track:showилиtrack:ask(§5), по умолчаниюtrack:show. Дляtrack:askназывается критерий §5, которого задача не проходит;track:shipпредлагается, когда правка описывается одним предложением. Метка владельца главнее предложения аналитика.
- Оценки и приоритет ставятся метками сразу, согласие не запрашивается.
Комментарий аналитики — уведомление, а не запрос: молчание владельца —
согласие, несогласие он выражает правкой меток или комментарием, и это не
останавливает работу. Право отклонить задачу (
rejected) остаётся за владельцем на любой стадии. - Вопросов владельцу на этом этапе нет. Единственный класс вопросов, который
вообще задаётся владельцу, — продуктовые (§7.1: что человек видит или делает,
объём видимых изменений), и их место — этап ТЗ, пачкой, с вариантами по
умолчанию и
blocked. Вопрос, который можно отложить до ТЗ, не задаётся в аналитике; вопрос, не блокирующий написание ТЗ, не задаётся вовсе — вместо него в ТЗ пишется блок принятых предположений. - Выход: на
track:ask—S3-spec, наtrack:show—S5-readyпосле AC в теле issue (§5); переход выполняет сам аналитик, не дожидаясь ответа. Либо, при явном конфликте соSCOPE.md, — предложение отклонить с причиной: это единственный случай, когда аналитика останавливается и ждёт владельца.
2.3 ТЗ в работе — написание ТЗ
- Кто: автор ТЗ, назначает себя. Статус означает «занято».
- Артефакт: тело issue, раздел
## ТЗ(решение владельца 2026-09-10, #517). Файл вdocs/specs/не создаётся ни на одном треке: каталог — архив ТЗ до этой даты, и задачи, у которых файл уже есть, доживают по старой схеме. Доказуемость («вердикт вынесен на этом тексте») держит конвейер: в блок якорей документа ревью пишетсяsha256нормализованного тела, и правка ТЗ после зелёного ревью ТЗ приходит ревьюеру кода находкой, а не тишиной. - Выход: полная первая редакция по §7.
2.4 ТЗ на ревью
- Ревьюер ≠ автор. Ревьюер получает issue и ТЗ, без устных пояснений автора. Его задача — не согласиться, а найти, где ТЗ не выполнимо или не проверяемо.
- Артефакт:
docs/reviews/SPEC-REVIEW-<NN>-r<N>.md, вердикт зелёный / жёлтый / красный. Ревью ТЗ проходит только трекask(§5). - High-находки блокируют. Medium в скоупе задачи чинится в текущем issue: без High это жёлтый вердикт, автор правит ТЗ, фикс проходит повторный цикл. Medium вне скоупа — отдельный issue: чужой скоуп в этой задаче не правится. «Оставили в тексте ревью» не считается закрытием ни для одной (решение владельца 2026-08-19, #202: отдельный issue дороже правки на месте). Low либо правится, либо снимается решением ревьюера с записью.
- Выход: «Готово к разработке» либо возврат в «ТЗ в работе» — не более 4 циклов (§4). Второй и последующие циклы разбираются по дельте (§2.10).
2.5 Готово к разработке (DoR)
Не работа, а очередь: единственный статус, из которого можно трогать код. Все пункты обязательны:
- ТЗ существует, на
track:askревью ТЗ зелёное, ссылки issue ↔ ТЗ на месте. НаshipиshowТЗ — в объёме §5, а пункты ниже закрываются одним словом «нет»: критерии трека их исключают, иначе этоtrack:ask; - 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): назначить себя, поставить метку, комментарий
«Взял: <роль> · сессия · ветка
issue/<NN>-<slug>». - WIP-лимиты: не более 1 issue в «В разработке» на исполнителя, не более 3 одновременно на цикл релиза, не более 2 в «Код-ревью».
- Трассируемость: ветка
issue/<NN>-<slug>; каждый коммит с файлами классов A, B или D несёт трейлерыIssue: #<NN>иUser-Visible: yes|no; коммит только из документации — без трейлеров (§3 п.10, #701). - Автотесты — часть реализации, а не отдельная фаза. Каждый 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-registry.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 и предупреждает — не краснеет (#699) — при росте выше него и о любом 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 после сборки) красит рост любого из них сверх полосы (#699: делегаты, члены и приватные порта и харнесса — 5,host.— 25, байтыdist/— 2 000) и любой мёртвый код поnoUnusedLocalsвне порта и харнесса. Снижение ветку не красит — базу до факта опускает бета (§8, «Храповики»); рост сверх полосы — только с записью в 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 стоил полного прогона конвейера ради одной строки в тестовой фикстуре.
Порядок:
-
найти вердикт предыдущего раунда и материал, на котором он получен. Материал объявлен блоком «Материал раунда» в конце документа предыдущего раунда: конвейер дописывает туда SHA ветки, дерево материала и блоб каждого ТЗ вместе с командами поиска (issue #416). Блок машинный — править его руками не нужно и не следует;
-
объявить дельту:
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). Конвейер теперь такую публикацию останавливает сам; -
по каждой находке предыдущего раунда показать, чем именно она закрыта — строкой кода или текста, а не заявлением автора;
-
заново проверять только те AC, чьё доказательство дельта задевает;
-
раздел «Унаследовано из 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 руками не правится никогда. Конфликт ребейза, в котором все пути —
INDEX.md, отказом не считается (#643): scripts/rebase-generated.mjs
пересобирает индекс по каталогу на остановке и продолжает ребейз — так делают
приведение к dev, слияние кандидата и авторский rebase-on-dev.mjs; индекс
вместе с любым другим путём — прежний отказ с перечнем файлов. Ещё два общих
файла не конфликтуют по смыслу (#698): записи ченджлогов в ## Unreleased
объединяет встроенный драйвер merge=union (.gitattributes), а на конфликте в
scripts/monolith-baseline.json ребейз берёт сторону dev — числа
объединённого дерева судит гейт связности монолита (npm run lint:unused) на
Validate кандидата. Сегодня пять чисел исходника он судит точно, байты dist/ —
с полосой 2 000 Б; поэтому сторона dev в базе краснеет, если задача сама
меняла эти числа, — это прежний возврат автору, только после Validate, а не до
ревью. Полоса у всех шести чисел — #699. Ни ченджлоги, ни база не входят в
patch-id кандидата слияния: вердикт к работе задачи остаётся в силе. Шаг 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>» и якорям материала. При стабильном релизе документы
задач, вошедших в него, переносятся в legacy/reviews/<vX.Y.Z>/ одним
коммитом класса C; индекс пересобирается и перечисляет только живые. Перенос —
часть чеклиста стабильного релиза, не отдельная задача, и делает его
node scripts/reviews-archive.mjs --through=vX.Y.Z (без --apply — только
план, #682). Членство — трейлеры Issue: #NN в диапазоне линии, как у
манифеста беты и ревью линии (§11.5); метка не доказательство. Уточнения:
задача с трейлером и после тега остаётся в docs/reviews/ целиком — её раунды
ещё продолжаются; задача с трейлерами в двух выпущенных линиях уезжает целиком
в последнюю; закрытая без выпуска (как #522) уезжает с линией, где лёг её
документ — коммит документа несёт трейлер; RELEASE-REVIEW-vX.Y.Z.md уходит в
каталог своего тега, поэтому перенос делается после публикации ревью линии.
Относительные ссылки в перенесённых документах и в соседях, которые на них
ссылаются, инструмент переписывает сам; --check-links печатает оставшиеся
битые.
Перенос идёт при пустой очереди S7-code-review: ребейз чужой ветки иначе
упрётся в перемещённый каталог.
Дешёвые гейты (typecheck, test, build с проверкой целостности сборки, bundle-policy --verify) гоняются в
каждом раунде: код изменился, а стоят они минуты. Тяжёлые — по дельте (§10.2).
Разбор остаётся полным, если дельта не локальна: ребейз на ушедший вперёд
dev (после ребейза это другой код, §10.4), смена контракта поведения, задета
новая подсистема, либо объём дельты сопоставим с исходной задачей.
Сокращается объём разбора, а не строгость: правка по замечанию способна сломать AC, который предыдущий раунд признал выполненным — так появилась регрессия #102. Граница не «только находки», а «находки плюс всё, до чего дотягивается дельта».
3. Правила
Продолжение черновика владельца. Каждое правило проверяемо — глазами или машиной.
- Никаких изменений в код, если нет issue и он не помечен «Готово к разработке» или дальше.
- Issue не может быть взят в разработку, пока у него нет ТЗ в объёме трека
(§5: на
track:ask— с зелёным ревью ТЗ; наshow— до трёх AC; наship— строка «что меняется и чем проверить»), доказательства для каждого AC и назначенного исполнителя. Инфраструктурная задача ТЗ не пишет (§1). - Issue не может быть взят дважды. Занятие фиксируется назначением, меткой и комментарием с именем ветки. У одного исполнителя одновременно не более одного issue в разработке.
- Статус меняется до действия, а не после. Взял — поставил метку; отдал на ревью — поставил метку. Метка, поставленная задним числом, — дефект процесса.
- Ровно одна метка статуса на issue в любой момент. Ноль или две — дефект, еженедельная гигиена его показывает.
- Автор не ревьюит своё — ни ТЗ, ни код. Никто не переводит свою работу через ревью-гейт.
- Ревью возвращает не более 4 раз. Пятый заход — решение владельца: разделить, отклонить или арбитраж (§4).
- High блокирует. Medium в скоупе чинится в текущем issue (без High — жёлтый вердикт и повторный цикл); Medium вне скоупа становится отдельным issue (#202). Low либо правится, либо снимается решением ревьюера с записью в документе.
- Скоуп не расширяется. Всё найденное вне ТЗ — новый issue, а не попутная правка. Блокирующая находка отправляет текущий issue в «Заблокировано».
- Каждый коммит класса A и B несёт трейлер
Issue: #NN, ветка называетсяissue/NN-slug, аUser-Visible: yesтребует правок в обоих changelog в том же коммите. Документационный коммит — только файлы класса C — трейлеров не требует (#701, какskip issueу CPython); хукcommit-msg, CI иprocess-gateсудят одинаково. Коммит класса D несётRelease:(п.12). Послеcherry-pick -xслужебная строка(cherry picked from ...)должна оставаться выше финального блока трейлеров: перед push проверяем порядок черезgit show -s --format=full HEAD. - Документация — в том же коммите, что поведение. Отдельным «допишу потом» коммитом документация не бывает.
- Сгенерированное не коммитится само по себе. Только релизный промоушен или
принятие эталонов с доказательством ревью: трейлер
Release:и ровно один источник — URL Linux CI run (Baseline-Reviewed:) либо хеш аттестованного WSL-артефакта (Baseline-Reviewed-Local:), §10.1. - Golden-эталоны принимаются только
npm run golden:accept -- --reviewedпо полному Linux-артефакту: либо GitHub CI, либоnpm run golden:wsl:captureв WSL/ext4 с clean опубликованным SHA и машинно-проверяемым паспортом. Второй путь убирает только первый ожидаемо красный CI-прогон; полный GitHub Validate на точном SHA коммита с эталонами остаётся обязательным. Принятие ради зелёного CI — нарушение процесса. Где принимаются эталоны — §8: наdevодним коммитом на бету, в задаче — только с меткойci:golden(#697). - Issue закрывается после выпуска беты с зелёным CI на точном SHA. Не раньше, не «по факту наличия кода», не исполнителем.
- Закрытый issue не переоткрывается. Новый дефект — новый issue со ссылкой.
- Стабильный релиз — promotion-only: версии, сгенерированные бандлы, changelog и release-метаданные. Продуктового кода там нет.
- История
devне перезаписывается. На неё ссылаются теги. Нарушение исправляется следующим коммитом плюс issue с меткойprocess— не force-push'ем. - AC доказывает автотест или запись ревьюера. Фразы «проверил локально, всё работает» в процессе не существует: либо тест, который умеет падать, либо честное «проверено чтением, не исполнением».
- Параллельных бэклогов нет. Планы, разборы и приоритеты живут в issue; файловые отчёты — разовые и датированные.
- Аварийный хотфикс — только решением владельца и только по §11.2.
4. Лимит циклов ревью: 4
Оба ревью-гейта возвращают задачу на правки не более 4 раз.
- Что считается циклом: отправка на ревью → вердикт с блокирующими находками → возврат. Уточняющий вопрос без вердикта циклом не считается.
- Зелёный вердикт цикла не образует и бюджет не тратит (решение владельца
2026-08-20, issue #227): он ничего не вернул на правки. Практический случай —
зелёное ревью, слияние которого не удалось: конвейер сам предписывает ребейз и
возврат метки, и этот заход не должен наказываться. Раньше счётчик считал все
вердикты подряд, и на #225 последовательность жёлтый → зелёный → ребейз дала
review-4на задаче с зелёным ревью и зелёным CI. - Заход и цикл — разные величины. Заход — сколько раз ревью отработало; он
виден в имени документа (
-r1,-r2, …) и нужен, чтобы два документа не затёрли друг друга. Цикл — единица бюджета §4. Заходов законно бывает больше, чем циклов, поэтому порог проверки №7 вscripts/process-gate.mjs(REVIEW_DOC_LIMIT— шесть документов одного вида на issue) выше лимита циклов: четыре цикла плюс два ребейза. - Метка
review-4ставится, когда исчерпан бюджет циклов; конвейер снимать её не вправе — это решение владельца. Если бюджет пересчитан и оказался ниже лимита, конвейер сообщает пересчёт, но метку не трогает. - Исчерпание лимита — не «пятая попытка», а разбор. Задача уходит владельцу,
решение одно из трёх:
- разделить — issue закрывается как «заменён», вместо него 2–3 меньших с ясным скоупом (частый настоящий диагноз: ТЗ было слишком большим);
- отклонить — цена решения оказалась выше ценности;
- арбитраж владельца — владелец фиксирует решение в issue, оно принимается как есть; несогласие ревьюера записывается, но не блокирует.
- Граница между «циклом» и «новым багом»: до закрытия беты находка ревьюера — возврат на правки; после закрытия — новый issue. Иначе лимит 4 обходится заведением issue вместо возврата.
- На треке
showлимит код-ревью — 2 цикла, ревью ТЗ на нём нет (§5): задача на три часа, которую переписывают трижды, лёгкой не была.
5. Треки ship, show, ask — метка владельца
Решение владельца 2026-09-28, issue #695. Разбор 85 закрытых задач #600–#691
показал, что прежний лёгкий трек (small) стоил почти столько же, сколько
полный: медиана 115 мин и 12 событий против 102 мин и 13. Дешевле был только
короткий (trivial) — 41 мин и 5 событий. Трек определялся формальными
критериями, и у владельца не было метки, чтобы задать его самому.
Трек задаёт метка track:ship, track:show или track:ask. Метка владельца
главнее критериев: критерии ниже — подсказка аналитика, а не приговор.
- Аналитик предлагает трек в «Оценке» (§7.2) и ставит метку. По умолчанию —
track:show. - Владелец ставит или меняет любую из трёх меток в любой момент; его метка окончательна.
- Повысить трек (
ship→show→ask) вправе любой агент, с причиной в комментарии. Понизить — только владелец. - Трек пересматривается, когда владелец снял усложнявший пункт (#688): аналитик предлагает понижение, решает владелец.
track:ship |
track:show |
track:ask |
|
|---|---|---|---|
| Для чего | документация, текст, очевидная правка в несколько строк, CSS-мелочь | баг и полировка в рамках описанного поведения; инфраструктура (§1) по умолчанию | геометрия, миграции конфига, публичные контракты, перф и touch, новый UX-контракт |
| Маршрут | S1-new → S5-ready → S6 → S7 → S8 |
S1-new → S2-analysis → S5-ready → S6 → S7 → S8 |
полный, §2 |
| ТЗ | строка «что меняется и чем проверить» в теле issue под ## ТЗ |
«Оценка» (§7.2) и до трёх AC в теле issue под ## ТЗ |
полное ТЗ по §7.1 |
| Ревью ТЗ | нет | нет | да, лимит 4 цикла |
| Локальный гейт | npm run gate:small |
npm run gate:small плюс смоуки smoke-select и AC |
§8 |
| Код-ревью | до слияния нет; пакетное ревью диапазона перед бетой | модель: корректность и AC | документ ревью, как в §2.7 |
| Лимит циклов код-ревью | — | 2 | 4 |
Рамки track:ship механические: дифф src/** не больше 30 строк, без новых
файлов в src/**, без ключей i18n, без полей конфига и без Python. Выход за
рамки переводит задачу в track:show. Рамки и слияние ship без ревью модели
проверяет и исполняет конвейер (§10.4, #696); код ship читает пакетное ревью
диапазона перед бетой (§11.7).
Подсказка аналитику. track:show уместен, когда выполнено всё сразу:
- сложность и риск ≤ 3;
- одна поверхность (один диалог, один модуль, один эндпоинт);
- нет миграции конфига и новых compatibility-полей;
- нет нового UX-контракта — меняется поведение в рамках уже описанного;
- нет влияния на производительность и на touch-контракт;
- ожидаемое поведение уже зафиксировано — в
docs/USER-GUIDE.ru.md, в каноническом документе подсистемы либо однозначно в самом отчёте. Решать нечего.
Невыполненный пункт — повод предложить track:ask с названным критерием.
track:ship — когда правку можно описать одним предложением и она укладывается
в рамки выше.
Чем опасны show и ship. Они убирают место, где решение проверялось до
написания кода. Признак «решать нечего» держит всю конструкцию, и его нельзя
подтверждать ощущением — только ссылкой на уже зафиксированное поведение. Если по
ходу выясняется, что решать есть что, трек повышается до ask: issue уходит в
S3-spec и получает полное ТЗ в теле issue по §7.1. Это не провал, а ранняя
диагностика.
Что не меняется ни на одном треке: issue и правило №1 (§1), трейлеры
коммитов, changelog для видимого изменения, зелёный Validate на точном SHA тега
беты (§8), гейты стабильного релиза. Качество держится на dev и на кандидате
беты, а не на каждой ветке.
5.1 Метки тяжёлых проверок и прежние метки
Тяжёлые проверки заказываются метками на любом треке — ставит владелец или автор с причиной в комментарии:
| Метка | Что включает |
|---|---|
ci:full |
полный Validate на ветке задачи |
ci:golden |
golden на ветке и приёмку сдвинутых кадров в самой задаче |
ci:mutants |
мутанты по диффу на кандидате ревью и слияния |
Как конвейер читает метки, описывает §10.4: ci:mutants (#696), ci:full и
ci:golden — dispatch Validate с full=true на материале ревью (#697).
Прежние метки. trivial и small читаются как track:show; продуктовая
задача без трековой метки — как track:ask; инфраструктурная задача (§1) без
трековой метки — как track:show. Новые задачи получают только track:*.
6. Роли
Один агент может исполнять несколько ролей в разных issue, но не две роли в одном артефакте.
| Роль | Делает | Не имеет права |
|---|---|---|
| Аналитик | разбор, оценки, поверхности | окончательно ставить приоритет |
| Автор ТЗ | раздел ## ТЗ в теле issue |
ревьюить своё ТЗ |
| Ревьюер ТЗ | docs/reviews/SPEC-REVIEW-NN-rN.md |
править ТЗ вместо автора |
| Разработчик | код, автотесты, документация, changelog | ревьюить свой код; принимать golden — кроме сдвига своей задачи с меткой ci:golden по §3 п.13 (§8, #697) |
| Ревьюер кода | 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 (только `track:ask`)
↔ ветка 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> · тип · поверхности: … · дубликаты: … · трек: ship/show/ask (причина) - Занятие:
Взял: <роль> · сессия <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. Гейты
Локальный гейт перед выходом из «В разработке» — минимальный набор,
покрывающий изменённые поверхности (действующее правило владельца).
Обязательную часть исполняет одна команда, npm run gate:small
(scripts/gate-small.mjs — единственный источник её состава, #701): сборка с
typecheck, юниты, целостность и бюджет бандла, no-new-any, запрет синхронного
чтения layout в render, запрет записи смоков в приватное состояние,
lint:unused и вывод smoke-select. Команды ниже — то же самое по отдельности
плюс то, что по диффу и AC:
npm run gate:small # обязательная часть одной командой
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 # только с меткой ci:golden (#697)
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).
Свежесть скриншотов документации — не гейт задачи (#697, ниже). По
необходимости, определяемой diff'ом и AC: браузерные смоки (сколько их —
считает ls demo/smoke_*.mjs | wc -l, вшитое число здесь трижды отставало от
дерева; прогон всех уместен только когда задача задевает всё; какие относятся к
диффу, печатает
node scripts/smoke-select.mjs --base origin/dev --head HEAD, и его вывод
прикладывается к ревью вместе с решением по каждой строке), golden:verify при метке ci:golden, pytest tests_backend при правках в Python, performance-профили при
названном в AC влиянии. Полные наборы — предрелизный гейт, а не гейт ревью.
Скриншоты снимаются только каноническим прогоном в CI — beta-derived.yml
(съёмка и приёмка одним коммитом бота на dev, #697) или Docs screenshots
(workflow_dispatch, только артефакт) с приёмкой вручную: npm run docs:accept -- --reviewed --from=<распакованный артефакт> (#246). Ручной путь остаётся
релиз-менеджеру, если бот недоступен. Съёмка на своей машине даёт байтово другой PNG при том же
кадре, и набор из «не того» браузера переписывает все кадры (их число — в
demo/docs/screenshots.mjs) без единого
содержательного изменения. Приёмка отказывает, если кандидат снят не с этого
дерева, не тем капчуром, не называет свой Chromium или неполон; коммит бота
проверяет релиз-менеджер, коммит ручной приёмки делает человек.
Когда правка src/** кадров не меняет — а это большинство правок — CI-цикл не
нужен (#512): npm run docs:accept -- --identical снимает кадры локально,
декодирует оба набора в Chromium и при нуле отличающихся пикселей во всех
кадрах обновляет только отпечаток исходников в screenshots.json; байты
закоммиченных PNG, их sha, браузер и упаковщик съёмки остаются прежними. Хотя бы
один отличающийся пиксель — отказ с перечнем кадров и штатный путь через артефакт.
Производные артефакты — на dev, один коммит на бету (#697, решение
владельца 2026-09-28). Отпечаток скриншотов документации считается по всему
src/**, поэтому любая правка фронтенда делает его устаревшим. Пока его
коммитила каждая задача, docs/images/screenshots.json правили 70 раз за 14 дней,
и две параллельные задачи конфликтовали на нём гарантированно. Golden
оплачивала следующая задача: сдвиг, влитый одной, всплывал у другой (#687 →
#685, #688 → #689). Теперь:
- ветки задач не коммитят
docs/images/**иdemo/golden/baselines/**. На ветке свежесть скриншотов — предупреждение preflight, а golden не идёт; - перед кандидатом беты
beta-derived.ymlодним коммитом бота обновляет наdevотпечаток и кадры (съёмка тем же каноном, приёмкаdocs:accept --reviewed) и эталоны golden из артефакта полного Validate наdev(golden:accept --reviewed,Release:иBaseline-Reviewed:в коммите). Изменившийся кадр или сцена принимается, только если назван во входах workflow; необъявленная разница — отказ с перечнем. Коммит проверяет релиз-менеджер; - задача, которая меняет визуал намеренно, ставит
ci:golden: конвейер прогоняет полный набор на материале ревью, и сдвинутые кадры задача принимает сама — по §3 п.13; Release:на ветке задачи тяжёлый набор не включает (classify-changes.mjs): тяжёлое запускаютci:fullиci:golden, трейлерRelease:наdevи ночной прогон.
Строгая свежесть скриншотов по-прежнему обязательна на кандидате беты
(publish-prerelease.yml, check-docs --screenshots=strict).
Храповики — полоса над потолком беты (#699, решение владельца 2026-09-28).
Строки двух ядер (test/core-file-budget.test.mjs), gzip стартового и ленивых
графов (scripts/bundle-budget.mjs) и числа связности монолита
(scripts/monolith-baseline.json) судятся одним правилом: потолок — факт
последней беты; ветка задачи краснеет, только если вышла выше потолка больше
чем на полосу (ядро — 50 строк, графы и dist/ — 2 000 Б, числа монолита — по
METRIC_BANDS), а снижение её не красит. Прежде храповики были двусторонними с
нулевым запасом: параллельные задачи конфликтовали на общих числах, пересчитывали
их после ребейза и упирались в потолок, потому что перед ними влили чужую (#689
после #691). Вторая сторона храповика живёт на бете: релиз-менеджер на
кандидате выполняет node scripts/ratchets.mjs tighten — потолки опускаются
(и поднимаются на принятый линией рост) до факта одним коммитом вместе с
кандидатом; npm run release:prerelease печатает рыхлые потолки
предупреждением. Абсолютный бюджет стартового графа
(INITIAL_VIEW_GZIP_BUDGET) остаётся стеной.
Перф-смок в 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 закрыт |
Модификаторы: трек track:ship/track:show/track:ask (§5), тяжёлые проверки
ci:full/ci:golden/ci:mutants (§5.1), hotfix, process, review-4;
приоритет P1/P2/P3; тип bug/feature/tech-debt. Прежние small и
trivial читаются как track:show (§5.1).
Тематические метки (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 выставляется автоматически при
установке зависимостей:
"scripts": { "prepare": "node scripts/install-hooks.mjs" }
npm ci вызывает prepare сам — значит, хуки появляются в каждом окружении,
включая свежий контейнер облачного агента, без отдельного шага в инструкции.
-
commit-msg— есть, работает. Отклоняет коммит без терминальногоIssue: #NN, требует ровно одинUser-Visible: yes|no(коммит только из файлов класса C — без трейлеров, §3 п.10, #701), а для коммитов, трогающихdemo/golden/baselines/**, —Release:плюс ровно один источник:Baseline-Reviewed: <URL GitHub run>либоBaseline-Reviewed-Local: sha256:<хеш аттестации>. Локальный хеш обязан совпадать сlocalAttestation.sha256в принятом индексе. Реализация —scripts/validate-commit-provenance.mjs, тот же скрипт вызывается jobprovenanceвvalidate.yml. -
pre-push— есть, работает. Прогоняетscripts/process-gate.mjsпо каждому пушимому ref и останавливает push при нарушении. Это и есть блокирующий гейт вместо PR. Удаление ветки и теги пропускаются: в первом случае проверять нечего, во втором коммит уже проверен, когда его пушили. Для новой ветки диапазон считается отmerge-baseсorigin/dev, а не от начала истории — иначе в него попали бы все нарушения, совершённые до появления гейта.При возврате
mainвdevдиапазон merge-коммита содержит второй родитель — уже опубликованные вmainкоммиты с закрытыми issue. Для destinationdevобщий скрипт 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:
- трейлер
Issue: #NNу каждого коммита класса A/B, допускается несколько; - имя ветки
issue/NN-slugсоответствует трейлерам; - у класса A есть ТЗ: раздел
## ТЗили хотя бы одинAC1в теле issue — либо архивныйdocs/specs/NN-*.mdу задачи до 2026-09-10. Офлайн тела нет, и проверка молчит; с--issues— предупреждение (настоящий рубеж — ревью ТЗ). Добавление нового файла вdocs/specs/**тоже предупреждение: каталог заморожен (#517); User-Visible: yes→ правки в обоих changelog в том же коммите;- коммит только класса D невалиден без
Release: vX.Y.Z,Baseline-Reviewed: <ссылка на прогон CI>либоBaseline-Reviewed-Local: sha256:<хеш аттестации>; - релизный коммит не содержит изменений в
src/иcustom_components/**/*.py; - документов ревью одного вида (
SPEC-REVIEW,CODE-REVIEW) на один issue не больше шести (-r1…-r6,REVIEW_DOC_LIMIT): четыре цикла плюс два ребейза (§4).
С токеном GitHub:
--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, то есть заведомо вне рабочего множества.
Инфраструктурный диапазон статуса не требует (#562, §1): если в диапазоне нет
ни одного файла класса A, задача ещё не вошла в поток — она войдёт в него сразу на
S7-code-review, — и отсутствие S-метки не отказ. Признак механический, по
диффу, а не по метке infra.
При продвижении в 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.
Не реализовано и остаётся долгом:
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— jobprocess-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. Метку переставляет отдельная
детерминированная стадия по вердикту, а не модель.
Четыре вещи, без которых конвейер молча не работает:
- метки переставляет PAT, а не
GITHUB_TOKEN: GitHub намеренно не порождает события отGITHUB_TOKEN, чтобы не было циклов, и цепочка обрывалась бы после первого шага без ошибок в логах; - для события
issuesGitHub берёт workflow только из ветки по умолчанию (main), независимо от содержимогоdev. Поэтому вmainлежит тонкийprocess.yml— триггер, run-name, потолок прав, — а тело_process.ymlон вызывает по ссылке@dev(#623). Правило ниже, «Workflow из ветки по умолчанию», — общее для всех таких файлов; - слияние в
devпроисходит до простановкиS8-merged, иначе метка врёт в промежутке — она утверждает, что код вdev; - многострочный текст внутри
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. Расхождение красит push в dev и
заводит одно issue владельцу ([workflow-sync]), а на ветке задачи —
предупреждение в сводке (#700): к её изменению оно отношения не имеет, и чинит
его тот, кто зеркалит в main. Так же судятся внешние ссылки документации
(check-docs --external=warn на ветках issue/*): упавший чужой сайт не
возвращает задачу. performance.yml в список не входит:
по расписанию он судит main собственным телом из main.
Цена захода зависит от трека (#696, решение владельца 2026-09-28). Трек
снимает scripts/process-track.mjs в стадии подготовки — по текущим меткам и
диффу от merge-base с dev, до ребейза. Прежние метки читаются по §5.1:
инфраструктурная задача без трековой метки — show, продуктовая — ask.
ship |
show |
ask |
|
|---|---|---|---|
| Validate на материале | лёгкий; годится завершённый push-прогон на этом SHA | лёгкий, как у ship |
с мутантами по диффу, dispatch |
| Ребейз до ревью | нет, если git merge-tree с dev чистый |
нет, если чистый | да (#257) |
| Ревью модели | нет — пакетное ревью перед бетой (§11.7) | корректность и AC; окружение — по нужде | полное |
| Проверка кандидата слияния | ждёт push-прогон кандидата; dispatch — только если его нет | как у ship |
dispatch с мутантами |
Метка ci:mutants возвращает мутанты по диффу на любом треке. Рамки ship
(§5) проверяет тот же шаг; выход за них — комментарий в issue и замена
track:ship на track:show в этом же заходе. Слияние ship оставляет в issue
комментарий с машинным маркером hp:ship-merge. Это не вердикт ревью и так себя
не называет; по маркеру пакетное ревью находит задачи диапазона. Повторно
применимый зелёный вердикт (#499) главнее ship: код уже прочитан.
В dev по-прежнему уезжает только SHA с зелёным Validate. Без ребейза до ревью
кандидат собирается один раз, при слиянии, и лёгкий Validate проходит там.
Ориентир — данные #695: small стоил медиану 115 минут и 12 событий, trivial —
41 минуту и 5 событий; show целится в уровень trivial.
Ревью show судит корректность и AC. Medium — дефект поведения, который
увидит пользователь, или невыполненный AC. Бухгалтерия — нет мутанта или записи
в реестре, нечувствительный тест на побочный вызов, формулировка в документе —
Low и цикла не открывает. Отсутствие мутантов по диффу на show не находка:
полный реестр гоняется ночью. Ревью show не ставит Chromium, если тело issue
не называет смоук или браузер. Ревью ТЗ не ставит ни npm ci, ни браузер: кода
оно не исполняет.
Ревью не начинается на красном коде (#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) — на треке ask. show
и ship с чистым слиянием ребейзятся один раз, при слиянии (#696, выше). Раньше ревью читало ветку
как есть, а слияние делало ребейз — проверенный 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 на ту вершину, поверх которой кандидат собран. Наshow/shipмутантов нет, и лёгкий Validate кандидата уже запустил сам push в ветку: слияние ждёт этот push-прогон, а dispatch шлёт, только если его нет за три минуты (#696). Отклонённый lease —devдвинулся снова — новая попытка; после третьей —S6-in-progressс комментарием; - красный Validate на кандидате или прогон, не появившийся за три минуты, —
S6-in-progressс ссылкой;S8-mergedставится только после push; - после push ветка задачи удаляется с lease на влитую вершину (#702): коммит, прилетевший после слияния, её сохраняет, и комментарий об этом говорит. Влитая ветка никому не нужна, а агент, ищущий ветку по номеру, иначе может взять устаревшую.
Проверка кандидата — обычный 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 — треки ship и show не исключения из правила №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 всё равно обязателен. «Чтобы гейт позеленел» основанием не является. Сдвиг, который влили задачи линии, принимается наdevодним коммитом на бету —beta-derived.ymlс объявленными сценами (§8, #697), а не задачей, которая наткнулась на него следующей.
Границы, за которыми исключение не действует. Оно про починку названного
гейтом дефекта, а не про продолжение разработки под видом починки. Правка идёт
обычным путём — новым issue либо возвратом в S6-in-progress — если она:
- меняет контракт поведения или добавляет пользователю что-то новое;
- задевает подсистему, которой в исходной задаче не было;
- по объёму сопоставима с самой задачей;
- меняет сам гейт вместо кода — правка теста, чтобы он перестал падать, это не починка, а сокрытие. Исключение — когда дефект в фикстуре и это доказано разбором, как на #89: солнце на азимуте 180° и единственное окно на северной стене, поэтому луч честно не строился.
Границу определяет автор, и здесь процесс сознательно отдаёт ему то, что в остальных местах не доверяет — оценку собственной работы. Плата за скорость в единственной точке, где цикл ревью стоит дороже всего. Компенсируется тем, что запись в issue публична и релиз-менеджер видит, что именно было сделано перед выпуском.
Это исключение из правила «каждое изменение проходит код-ревью» (§5, §7.1).
Второе такое место — трек ship (§5): его ревью не отменяется, а переносится
на пакетное ревью диапазона перед бетой. Здесь же исключение относится только к
окну между 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 кандидата.
11.7 Пакетное ревью ship перед бетой
Решение владельца 2026-09-28, issue #696.
Зачем. track:ship сливается без ревью модели (§5, §10.4): правка в
механических рамках и зелёный лёгкий Validate. Прочитать её код обязан кто-то
до того, как она уйдёт пользователям беты. Одна сессия на все ship-задачи
диапазона дешевле ревью на каждую, а рамки ship держат объём малым.
Шаг. Перед публикацией беты — ship-review.yml
(gh workflow run ship-review.yml --ref dev -f tag=vX.Y.Z-beta.N):
- вход — issue из трейлеров
Issue: #NNв диапазоне «прошлый тег..кандидат» (тот же построитель, чтоRELEASE-MEMBERSHIP.json, #547), из них — ship: с маркеромhp:ship-mergeв комментариях или с меткойtrack:ship. Собираетscripts/ship-review.mjs prepare; ship-задач нет — модель не запускается; - суждение — по строке ТЗ каждой задачи и её коммитам: делает ли код
заявленное и только его, не ломает ли соседнее, не вышла ли правка из ship по
смыслу. Правила ревьюера —
docs/process/REVIEWER.md, «Пакетное ревью ship»; - выход —
docs/reviews/SHIP-REVIEW-<тег>.mdвdevс машинным блоком: задачи и счёт High/Medium/Low. У модели нет прав записи, документ публикует детерминированный шаг.
Гейт беты. ship-review.mjs check стоит в обоих путях публикации —
publish-prerelease.yml и npm run release:prerelease (включая --check).
Если в диапазоне есть ship-задачи, документ обязан лежать в кандидате или в
dev, покрывать их все и не нести High. Задача, слитая после ревью, требует
пересъёмки (-f force=true). High чинится отдельной задачей, затем ревью
переснимается. Medium и Low решает владелец, как в §11.5.
12. Запрещено
- код без issue или из статуса раньше «Готово к разработке»;
- ТЗ, написанное после кода (кроме §11.2, и тогда с пометкой «как сделано»);
- ревью своей работы; перевод своей работы через ревью-гейт;
- пятый цикл ревью вместо разбора по §4;
- заведение issue вместо возврата на правки, чтобы обойти лимит циклов;
- принятие golden-эталонов ради зелёного CI или по частичному артефакту;
- закрытие issue до выпуска беты с зелёным CI;
- переоткрытие закрытого issue вместо нового бага;
- Medium-находки, оставленные как TODO в документе ревью: в скоупе — чинятся в текущем issue, вне скоупа — становятся отдельным (#202);
- параллельные бэклоги в файлах (
BACKLOG-*.md, «планы» в docs); - ревью-документы вне репозитория;
- попутные правки «раз уж я здесь»;
- фича или материальное изменение поведения в стабильном релиз-коммите;
- force-push в
dev; - ручное копирование на домашний инстанс.
Нарушение процесса — тоже issue (метка process): если правило удалось
нарушить незаметно, виновата проверка.