# Validation policy

Canonical owner for validation requirements across all workflow families.

## Default validation

<!-- rule: VALIDATE-TARGETED-FIRST -->
`VALIDATE-TARGETED-FIRST`: Workers use `resolveTargetedValidation(changedPaths)` from `@dev-loops/core/loop/validation-classify`, which reads the repo's `.devloops` `validation.paths` map (no built-in layout), to run the narrowest deterministic checks for the changed paths and record the command and stable outcome. When every path matches a first-match rule with non-empty commands, the result is the sorted, deduplicated union of `commands` and `gateSuites` across surfaces. Its `commands` are local worker checks; each `gateSuites` entry is an explicit package-domain `--suite` for the gate artifact. Invalid, unmapped, empty-command or unsafe templated paths require full-validation ownership; an empty check is never a pass. Only `node <dev-loops-package-root>/cli/index.mjs gate resolve-validation` and, as the second sanctioned launcher, `loop validation-record` (`VALIDATE-LOCAL-RECORD-PRODUCER`, ADR 0170) may launch a local full-repository run, bound to the exact head SHA and requested profile. The `package.json` `verify` path also binds the pinned Bun 1.4.1 toolchain; `validation.fullCommand` runs via `sh -c` with no Bun pin, and `ci-only` runs nothing locally. Missing full evidence is incomplete, never permission to run `bun run verify` directly. The full `bun run verify` suite and CI aggregate keep their existing coverage; CI on the current head or sanctioned full evidence supplies full-repository evidence at the applicable gate. The `.devloops` `validation.mode` selects the full-validation authority (ADR 0137): `ci-only` (the consumer default) runs no local suite and records a `status: "complete"` artifact with `authority: "ci-authoritative"` when current-head CI is green, `failed` on red CI, and typed `incomplete` on pending, missing or wrong-head CI or a rollup with no SUCCESS check ("current-head CI ran no checks"), for every profile; `local` (opt-in) runs the declared `validation.fullCommand`, else the `package.json` `verify` script. In `local` mode, a repo without `package.json` serves a targeted request with the declared `validation.fullCommand`, and the artifact records one suite named `full`. The gate takes configured commands only from the default branch's `.devloops` `validation` block (read from `origin/HEAD`; the checkout's `workflow.baseBranch` never selects it, and a default branch with no `.devloops` file counts as an absent block, so `ci-only` applies), never from PR content or the PR-head checkout: a block in the checkout's own `.devloops` family file (mode, `fullCommand` or `paths`) that differs from the base block in `mode` or `fullCommand` resolves typed `incomplete` (config-source change needing review) and runs nothing; a `paths`-only difference is admitted (ADR 0192) and the gate reads only the base block's `paths`, and in `local` mode on the `package.json` `verify` path, a targeted request with differing `paths` runs the suites the base `paths` select for the changed paths in place of the requested `--suite` values; when the base `paths` select full-repository validation or select no gate suite for the changed paths, the gate runs the full-repository profile, as it does with no difference. The `sh -c` command runs without tokens in its environment. A `gateSuites` entry or command the gate consumes never comes from a checkout-only edit. `resolveTargetedValidation` itself reads the checkout's `.devloops` family file at the git repo root, so a worker's targeted self-check runs the checkout's declared command. `ci-only` with no CI at all stays `incomplete`, and `ci-only` resolves every profile through current-head CI. Node `>=24` packaged-consumer checks and npm registry/pack/publish/provenance checks remain required where their boundary is under test.

## Gate-specific requirements

| Gate | Validation required |
|---|---|
| `draft_gate` | CI green on current head, or the bounded zero-suite exception: first read and apply the [CI owner](copilot-ci-status-contract.md#zero-suite-local-validation-exception), including its unavailable CLI input. |
| `pre_approval_gate` | CI green on current head + resolved review threads + clean re-review |

## Coverage requirements

<!-- rule: VALIDATE-COVERAGE-THRESHOLD -->
`VALIDATE-COVERAGE-THRESHOLD`: Changed files SHOULD maintain ≥90% coverage for lines, statements, functions, and branches (not enforced by the shipped verify config; treat it as the working target), and non-trivial logic MUST be test-first.

## Coverage-request admission

<!-- rule: VALIDATE-COVERAGE-ADMISSION -->
`VALIDATE-COVERAGE-ADMISSION`: A request to add test coverage is actionable only when it names three things: the observable behavior or risk it protects, why the existing evidence fails to cover that behavior, and the cheapest authoritative seam that closes the gap. One authoritative test layer is the default. An additional unit, integration, CLI, package, or harness layer is justified only by a distinct boundary failure — argument parsing, transport, serialization, packaging, or actual harness routing — never by an equivalent permutation of an already-covered behavior. A demand that offers only a coverage percentage, only the absence of a test, only an equivalent permutation, only a generated-mirror duplicate, or only a harness label is non-actionable; the ≥90% target in VALIDATE-COVERAGE-THRESHOLD is diagnostic, not sufficient grounds for a finding. A representative check MAY stand in for equivalent error paths, but every independent uncertainty mechanism that could accept unsafe input stays covered. A coverage finding that fails this rule is dispositioned `reject`, so the existing judge act-list filter keeps it out of fixer dispatch; a repeated coverage demand stays `reject` until new evidence names a distinct behavior or boundary risk, compared against the prior-round judge ledgers rather than a new rejection registry. Demonstrated fail-open, security, acceptance-criterion, and public-contract defects remain actionable, and justified expansion that changes approved scope routes through the spec-authority / operator-approval path. The scope-drift signal is supporting evidence for this judgment, not a new gate.

## Head validation record

<!-- rule: VALIDATE-LOCAL-RECORD-PRODUCER -->
`VALIDATE-LOCAL-RECORD-PRODUCER`: `node <dev-loops-package-root>/cli/index.mjs loop validation-record --repo <owner/name> --head-sha <full SHA> --title <PR title>` is the `local` producer of the head-pinned validation record at `tmp/validation-records/<owner>/<repo>/<headSha>.json` (ADR 0170). It is a sanctioned local run launcher: it runs in the foreground with a bounded `--timeout` (default 540 s, at most 570 s, below `DEV-LOOP-PROBE-TIMEOUT-CEILING`) and has no detached mode and no completion file. It needs a clean worktree at the head, a base `validation` block with `mode: local` (`ci-only`, the default, selects the `ci` producer and is `incomplete`), a checkout `validation` block that matches the base block except in `paths` (ADR 0192), with suites selected from the base `paths`, and the pinned Bun, selects `targeted` only when every changed path maps to `gateSuites` and otherwise runs `verify`, and writes a failed record for a failing suite. Any other outcome, including a timeout, is `incomplete`: it writes no record and exits non-zero. No gate reads the record yet.

## Cross-references

- [Merge preconditions](merge-preconditions.md)
- [Stop conditions](stop-conditions.md)
- [Public Dev Loop Contract](public-dev-loop-contract.md)
