# Acceptance Criteria Verification

Canonical owner for the acceptance-criteria verification procedure run during the `pre_approval_gate`. Referenced from [Copilot PR Follow-up Skill](../copilot-pr-followup/SKILL.md#pre-approval-gate-contract).

## Procedure

<!-- rule: ACCEPT-CRITERIA-VERIFY-AND-REFLECT -->
`ACCEPT-CRITERIA-VERIFY-AND-REFLECT`: Before posting the `pre_approval_gate` comment, the agent MUST verify every acceptance criteria checklist item in the issue linked to this PR, and MUST reflect the verified items back into both the linked issue body and the PR body (exception: when the linked issue body is not the spec-of-record — under a lightweight session (tracker-backed or issue-less; the PR body is the spec) or a plan-file promotion session (the committed plan doc the PR body points to is the spec) — the step 5 issue-body mirroring is skipped; see the fork below):

> **Non-tracker spec-source fork:** when the linked issue body is not the spec-of-record, the fork covers three arms — lightweight (the PR body itself is the spec; splits on whether a backing issue exists) and plan-file promotion (a distinct local-planning origin whose committed plan doc, pointed to from the PR body, is the spec). Handle each arm as below.
>
> **Tracker-backed lightweight** (`--issue --lightweight`): a linked issue exists, so keep the `Closes #N` linkage and all issue-level tracking, but the **PR body**, not the issue body, is the canonical spec-of-record read for AC/DoD. Still resolve the linked issue for tracking, but read the AC/DoD/invariants from the PR body rather than the issue body (skip step 2's issue-body read for spec purposes). Run `node scripts/loop/validate-pr-body-spec.mjs --repo <owner/name> --pr <pr-number> --expected-issue <issue-number>` (reuses `@dev-loops/core/loop/issue-refinement-artifact`) to confirm the body carries the required invariants — it fails closed with a per-section `missing_*` reason if any is absent, in which case post the gate comment with verdict `blocked`. The PR body MUST carry the `Closes #N` linkage (or GitHub's other closing-keyword forms): `validate-pr-body-spec` fails closed with `missing_closing_issue_reference` without it, and with `closes_wrong_issue` when `--expected-issue` doesn't match. Then continue at step 3, extracting the **Acceptance criteria** checklist items from the PR body itself; step 5's issue-body AC-tick update is unnecessary since the issue body is not the spec surface (the PR body is ticked in step 6 as usual).
>
> **Issue-less lightweight** (`--lightweight` alone): no linked issue exists — by design, not a gap. There is nothing to resolve at step 1 and nothing to read or mirror at steps 2 and 5; skip all three. Read AC/DoD/invariants directly from the PR body and run `node scripts/loop/validate-pr-body-spec.mjs --repo <owner/name> --pr <pr-number> --no-issue`, which fails closed with a per-section `missing_*` reason for any absent invariant and with `unexpected_closing_issue_reference` if the body carries a closing reference to an issue that doesn't back it (a closing reference MUST NOT be present). On any validation failure, post the gate comment with verdict `blocked`. Step 1's "zero candidates → blocked" rule is scoped to tracker-backed sessions; for a deliberately issue-less session, zero closing references is the expected state, not a blocked condition. Continue at step 3 against the PR body, then step 4 and 6 as usual.
>
> **Plan-file promotion (P4) — a distinct local-planning origin, not a lightweight sub-arm:** no linked issue exists — similar to the issue-less lightweight arm, there is nothing to resolve at step 1 and nothing to read or mirror at steps 2 and 5; skip all three. AC/DoD live in the PR body, copied from the committed plan doc that is the spec-of-record; read them from the PR body as usual.
>
> The default issue-backed procedure below is unchanged.

1. **Resolve the linked issue number deterministically:** use `gh pr view <pr-number> --repo <owner/name> --json closingIssuesReferences,body` and apply this decision tree: if there is exactly one closing issue reference, use it; else if there is exactly one PR-body `Closes #N` / `Fixes #N` pattern, use it; otherwise (zero or multiple candidates), post the gate comment with verdict `blocked` (gate cannot complete deterministically) rather than guessing.

2. **Read the issue body:** `node scripts/github/view-issue.mjs --repo <owner/name> --issue <issue-number> --json body --jq '.issue.body'`

3. **Extract acceptance criteria.** Under #1951 (matrix on the issue, checklist on the PR) the authoritative acceptance criteria live in the issue's `## AC / DoD matrix` (the criterion column) and are projected onto the PR body as list-form checkboxes (`derivePrChecklistsFromIssueMatrix`). Read the acceptance criteria the PR must satisfy from the **PR body's** Acceptance criteria checklist (both `- [ ]` and `- [x]` items). When the issue body ALSO carries an interactive `## Acceptance criteria` checklist (a pre-#1951 issue), extract those too for the issue-side sync in step 5. Acceptance verification MUST NOT depend on the issue carrying duplicate interactive checkboxes: a matrix-representation issue has none to tick, and the gate reads the PR body's derived checklist. Ignore items from other sections (DoD, tasks, non-goals) that are not acceptance criteria.

4. **Verify each AC item** against the proposed changes on the current PR head.

5. **Sync the issue body and the PR body in one call:** after the verification is clean, tick each verified item's `- [ ]` → `- [x]` in **both** the linked issue body and the PR body with a single `node scripts/github/tick-verified-checkboxes.mjs --repo <owner/name> --issue <issue-number> --pr <pr-number> --verified <exact label>...`. It applies the issue tick via `scripts/github/edit-issue.mjs` (one `gh issue edit --body-file` update) and the PR tick via one `gh pr edit --body-file` update — exact-label match; only items actually verified are ticked; unverified or deferred items stay `- [ ]`; never blanket-checks, never unchecks; fail closed. Passing `--issue` and `--pr` together keeps the tracker's AC checkboxes honest in the same step when they exist. #1951: a matrix-representation issue has no interactive AC checkboxes, so the issue-side tick is a no-op (exact-label match finds nothing) and acceptance does NOT depend on it — the PR body's derived checklist is the ticked surface. Do not issue one edit per item.

6. **PR body tick (same call):** the `tick-verified-checkboxes.mjs` run above flips the PR body's verified `- [ ]` → `- [x]` via that single `gh pr edit --body-file` update, so the merged PR shows checked AC/DoD. This runs automatically as part of the `pre_approval_gate`, not by memory. (Run with `--pr` alone when there is no tracker-backed issue to sync — e.g. lightweight/issue-less sessions.)

7. **Deterministic pre-approval completeness block (#1877):** any unchecked `- [ ]` in the PR body's own Acceptance criteria / Definition of done checklists fails the `pre_approval_gate` closed — `upsert-checkpoint-verdict.mjs` refuses a `clean` verdict while `prBodyUncheckedAcItems`/`prBodyUncheckedDodItems` are non-empty, so a PR cannot reach approval with an open acceptance criterion. Scope: the coordination state populates those fields for a **tracker-backed ready PR with at least one deterministically-linked issue** — including when every linked-issue body fetch fails (the PR body is local, so the block stays armed) — but NOT for the lightweight/issue-less path (an explicit #1877 non-goal; the PR body is the spec-of-record there and the draft-exit `validate-pr-body-spec` check owns that surface), and not for a PR body with no resolvable linked issue. This composes with step 5: a box the gate could not verify stays unchecked (exact-label match, never blanket-checks) and therefore blocks. The boundary is explicit: this deterministic check enforces **completeness** (nothing left unchecked/forgotten), NOT **truthfulness** — a dishonestly-ticked `[x]` passes the mechanical check. Verifying that each `[x]` is real remains the reviewer/judge's responsibility (the `pr-checklist` angle and the judge's AC/DoD/non-goals relevance pass); both layers stay.

8. **Post the gate comment:** always post a `pre_approval_gate` comment (the checkpoint verdict comment contract requires a visible comment even for non-`clean` verdicts). Use verdict `clean` only when all AC items are verified; use verdict `findings_present` when any AC item is not satisfied and requires follow-up fixes; use verdict `blocked` when the gate cannot complete deterministically (for example no linked issue, ambiguous issue linkage, or the issue body is unavailable). In all cases include a note on AC verification status.

When the spec-of-record surface carries no acceptance-criteria items at all — no PR-body AC checklist AND (for a matrix-representation issue) no matrix rows to project — post the gate comment with verdict `findings_present` and note that fact explicitly rather than assuming satisfaction. #1951: a matrix-representation issue with no interactive issue-side AC checklist is NOT itself "no AC" — its criteria come from the matrix (projected onto the PR body); only a genuine absence of any AC source is findings_present.
