diff --git a/PROCESS.md b/PROCESS.md
index 3146dda4..a1bf8552 100644
--- a/PROCESS.md
+++ b/PROCESS.md
@@ -933,6 +933,28 @@ S7-code-review → код-ревью → слияние в dev → S8-merged л
ветки и пушем автор мог запушить коммит, и слепой `--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`: вердикт
+ к другому диффу не применим, §7.2), публикация кандидата в ветку задачи, ожидание
+ зелёного Validate **на этом SHA** и только затем push в `dev` с lease на ту
+ вершину, поверх которой кандидат собран. Отклонённый lease — `dev` двинулся снова
+ — новая попытка; после третьей — `S6-in-progress` с комментарием;
+- красный Validate на кандидате или прогон, не появившийся за три минуты, —
+ `S6-in-progress` с ссылкой; `S8-merged` ставится только после push.
+
+Проверка кандидата — обычный Validate ветки: лёгкий набор плюс диффозависимые
+гейты. Тяжёлые гейты остаются за кандидатом релиза (#479): слияние не превращает
+каждое движение `dev` в двадцатиминутный прогон, а проверяет ровно то, что
+проверил бы пуш той же дельты.
+
Поэтому зелёное код-ревью с неудавшимся слиянием ведёт не в `S8-merged`, а в
`S6-in-progress`: работа действительно вернулась к автору, только осталась не
правка кода, а ребейз. Вердикт при этом в силе, переделывать нечего. После ребейза
diff --git a/docs/TESTING.md b/docs/TESTING.md
index 46ac2e51..d649214e 100644
--- a/docs/TESTING.md
+++ b/docs/TESTING.md
@@ -33,18 +33,65 @@
`--shard=i/4`), перед стабильным релизом и по понедельникам. Дешёвая половина
идёт с юнитами: `test/mutation-gate.test.mjs`. Локально для дельты задачи —
`node scripts/mutation-gate.mjs --changed origin/dev..HEAD`: гоняются только
-мутанты, чьи patch-файлы или файлы гарда задеты диффом (#332, #475). Бандл
-собирается только мутантам с браузерным гвардом; компиляция тестов в worktree
-стартует с тёплого `test-build/` основного дерева.
+мутанты, чьи patch-файлы или **входы гарда** задеты диффом (#332, #475, #492).
+Входы гарда — это не только файлы, названные в команде: обёртка
+(`scripts/*-guard.mjs`) объявляет запускаемые тесты в `export const
+GUARD_INPUTS` (умолчание `backend-test-guard.mjs` —
+`tests_backend/test_ha_import_export.py`, действует без третьего аргумента), а
+от каждого файла гарда берётся замыкание импортов и путей: смок тянет
+`demo/serve.mjs`, compat-хелперы и фикстуры, pytest-модуль — `conftest.py`.
+`src/**` в замыкание не входит — это сторона патча (§6.4 ТЗ #492). Дифф,
+трогающий сам реестр, дополнительно отбирает добавленные и изменённые
+определения относительно реестра базы (`git show :scripts/mutation-gate.mjs`).
+Бандл собирается только мутантам с браузерным гвардом; компиляция тестов в
+worktree стартует с тёплого `test-build/` основного дерева.
В CI `changed_mutants` добавляет `--ledger=<файл>` — журнал пойманных
свидетелей (#481): после каждого пойманного мутанта в файл пишется отпечаток
-его входов (файлы патча и гарда, объявление мутанта; строка версии продукта
-нормализована), и мутант с тем же отпечатком в следующем прогоне не гоняется.
+его входов (файлы патча, все входы гарда по замыканию выше, объявление мутанта;
+строка версии продукта нормализована), и мутант с тем же отпечатком в следующем
+прогоне не гоняется.
Журнал живёт в кэше Actions по шарду, сохраняется при любом исходе шага, так
что отменённый пуш или таймаут не пропадают даром. Полный прогон и `--id`
журнал не читают; `--ledger` без `--changed` — ошибка.
+## Manifest входов: какие job запускать и что хешировать (#492)
+
+Один модуль, `scripts/check-inputs.mjs`, объявляет каждую проверку Validate
+(`preflight`, `frontend`, `changed_mutants`, `integration`, `smoke`, `golden`,
+`performance_smoke`, `backend`) через корни и точки входа, а остальное
+вычисляет: от точек входа берётся замыкание — импорты транзитивно, строковые
+пути как листья, каталог по строке — все текстовые файлы под ним. Из этого
+manifest читают и `classify-changes.mjs` (job `changes`: job запускается,
+если дифф задел хотя бы один её вход), и `gate-reuse.mjs` (ключ реюза =
+хеш содержимого всех входов job). Два места не могут разойтись: до #492 у
+бэкенда ключ не знал relay, converter и schema, а golden/perf — `serve.mjs`,
+`demo.html` и compat-хелперов.
+
+Правила, которые стоит знать:
+
+- `validate.yml` — вход toolchain каждой job: правка workflow гоняет всё;
+- `src/**` — вход только браузерных job; backend от UI не зависит, а
+ `custom_components/houseplan/manifest.json` (версия) держит правило «кандидат
+ релиза прогоняет всё» и для него;
+- **неизвестный исполняемый вход** — файл под `scripts/`, `demo/`, `test/`,
+ `tests_backend/`, `.github/`, `custom_components/`, `src/`, которого нет в
+ manifest ни одной проверки, — расширяет прогон до полного набора и называется
+ в summary. Лист покрытия (`node scripts/check-inputs.mjs --coverage`,
+ `test/check-inputs.test.mjs`) требует, чтобы каждый такой файл был чьим-то
+ входом либо стоял в `NOT_AN_INPUT` с причиной: новый скрипт без записи —
+ красный юнит, не вечное расширение прогонов;
+- `--check=` печатает входы, `--why=<файл>` — цепочку, по которой файл
+ стал входом.
+
+Отрицательные пробы (`test/gate-reuse.test.mjs`, `test/classify-changes.test.mjs`,
+`test/check-inputs.test.mjs`) держат представителей каждой категории входов и
+обратную пробу для UI ↔ backend; мутанты `manifest-drops-workflow-input`,
+`classify-unknown-input-is-unaffected`, `reuse-backend-hashes-ui`,
+`guard-inputs-ignore-wrapper-defaults`, `registry-diff-not-selected`,
+`merge-pushes-unvalidated-candidate`, `merge-ignores-lease-rejection`,
+`nightly-does-not-wait` держат сам протокол.
+
Чистые Python-контракты канонизации, которым не нужен Home Assistant, находятся
в `tests_backend/test_coordinate_canonicalization_pure.py`. Они обязаны реально
исполняться в локальном `pytest tests_backend`, а не исчезать за module-level