Skip to main content

ADR-019 — Quality gates are executable contracts; defects are classified

canonical · tech-lead · updated 2026-09-07 · source

  • Status: Accepted (José, 2026-09-07)
  • Date: 2026-09-07
  • Owner: Tech Lead
  • Related issues: effort #1703 · subs #1704 (this ADR + quality-vocabulary.md) #1705 (gate command) #1706 (blocking entry check) #1707 (observable exit evidence) #1708 (severity labels) #1709 (bug form) #1710 (stale task form) · triggered by #1689 → #1692 · #1693/#1694 (the finding-extraction fix)

Context

Our review and merge criteria existed only as prose, so nothing verified them:

  • PR #1689 merged with four untriaged CodeRabbit findings, two of them real bugs — one directly undercutting a fix shipped in that same PR — while every declared gate read green.
  • Three rules stated a different "the gates" (risk-tiered-review.md, delegation-brief.md, react-style-guidelines.md), and none was checkable.
  • The entry check existed but was narrower than the rules claimed: .githooks/pre-push (#786) blocks a push on pnpm lint errors only — not typecheck, not the affected build, not the knowledge lint — so "the gates are green" was still unverifiable at push time. It is wired via core.hooksPath=.githooks, set by the root package.json prepare script, so it already applies to every worktree. (.git/hooks being empty is a red herring: core.hooksPath redirects. An earlier draft of this ADR wrongly said no hook was installed — corrected here.) Separately, the kit's own seam prePush in .claude/kit.config.json shipped {enabled:false, command:""} — declared and unused. The 16 .claude/hooks scripts are Claude Code hooks: they observe assistant tool calls only, never a human's or a delegated agent's push.
  • The upstream captain (cckit scripts/lib/captain.sh) classifies a PR CLEAN → merge from mergeable + mergeStateStatus + statusCheckRollup alone. Local typecheck/lint leave no trace and CodeRabbit's triage state is invisible there — only its own green check, which #1689 proved is not the same thing. An empty rollup collapses to NONECLEAN.
  • Defects had no classification: labels carry priority:p0–p3 + risk:* and no severity, and no bug form exists — so causal analysis is impossible and impact/urgency are one axis.

A standards review of our own mechanics supplied the missing definitions: ISO/IEC 20246:2017, ISO/IEC/IEEE 1044-2009, 24765, 29119-2/-3, 15288/12207, ISO 19011:2018, ISO 9000:2015, ISO/IEC 17021-1:2015. Two findings are load-bearing: "quality gate" has no ISO/IEEE definition (the normative concept is a decision gate = entry criteria + exit criteria + deciders), and severity and priority are independent attributes that 1044 deliberately keeps apart.

Decision

  1. One vocabulary, two families that never mix — engineering (issue/anomaly/defect/fault/ failure/severity/priority) vs audit (finding/nonconformity/major-minor/OFI). Canonical home: .claude/rules/quality-vocabulary.md. "Observation" is removed as a finding category (ISO 19011:2018 dropped it — an unbreached requirement is an OFI).
  2. Every gate names four elements: entry criteria · exit criteria · the decider · the waiver path. A gate nobody can fail is a milestone, not a gate.
  3. The repo gate becomes executable and observable. Entry criteria are enforced by widening the existing blocking pre-push hook from lint-only to the whole gate — a git hook gates humans and delegated agents identically, which a Claude Code hook cannot, and git push --no-verify bypasses any hook, so this is a fast equal-for-everyone check, never an unbypassable one. Exit criteria are enforced by evidence the merge step can read, so "the gates are green" stops being an assertion.
  4. Severity is a separate axis (sev:blocker|major|minor|trivial), and a defect closes on exactly one disposition from a closed set (fixed/rejected/duplicate/deferred/ not reproducible).
  5. It lands in tedos first, then goes upstream to cckit once proven — the pattern already used by coderabbit.sh and ds-audit.sh, and it keeps 1 effort = 1 PR.

Consequences

  • A push that fails the gate is rejected, and a merge without gate evidence is refused. Expect friction on the first pushes; the waiver exists for the genuine exception and records itself.
  • The three divergent "the gates" wordings become pointers to one rule (latest-touched wins).
  • Defect data becomes analysable (severity × disposition × detection point), which also feeds the builder-model corpus (#1080) — the same argument as the effort trace in ADR-006.
  • cap_policy_floor already implements the always-human floors (workflows, lockfile/graph, secrets, hold); this ADR does not rebuild them.
  • Cost: zero — rules, bash, labels and issue forms. No infrastructure, no API credits.

Alternatives considered

  • Document the criteria only, no enforcement — rejected: the rule already existed when #1689 merged. A criterion nobody checks is the exact failure mode.
  • Enforce at merge only — rejected: expensive review then gets spent on unready artefacts, which is precisely what ISO/IEC 20246 entry criteria exist to prevent.
  • Enforce at push only — rejected: #1689 was a merge failure; the code was fine.
  • Build it in cckit first — rejected: it would be designed in the repo where the incident did not happen, and every iteration would need a release plus an upgrade.
  • Corrective-action records with effectiveness verification (ISO 9001 10.2) and a review-type formality map (20246's ten types) — considered, deliberately deferred to a sibling effort.