Gate outcomes carry a typed severity; an operator-configured strategy decides the CLI effect
Filename: 2026-08-13-6-gate-outcomes-carry-severity-operator-strategy-decides-effect.md
Status: Proposed
Date: 2026-08-13
Deciders: Operator (ATDD)
Technical Story: Resolves the fail-closed (ADR 2026-08-13-2) vs skip-and-proceed (ADR 2026-08-13-4) contradiction the dialectics pass surfaced. Instead of a per-gate boolean disposition, gate outcomes are typed by severity and the CLI effect is an operator policy.
Context and Problem Statement
The gate design produced two conflicting per-gate booleans: gates flip fail-closed (block on failure), but untrusted packs were to be skipped (proceed). Same un-run gate, opposite outcomes, decided by why it didn't run — and the "skip" branch silently no-ops security gates in CI. A single boolean cannot express "proceed, but degraded, and record it," which is the real operator need for many checks (a docs lint failing should not necessarily block a transition; a missing security gate should).
The codebase already has the ingredients, fragmented across five sites:
kernel.glossary_types.Severity (src/kernel/glossary_types.py:70), audit.models.Severity +
its threshold fail_on: Severity | None with severity <= fail_on_threshold
(src/specify_cli/audit/models.py:47,53,203), status.doctor.Severity
(src/specify_cli/status/doctor.py:26), the charter-lint SEVERITY_ORDER blocking ladder
(src/specify_cli/charter_runtime/lint/findings.py, consumed by analysis_report.py:338), and
the strictness literals (kernel.glossary_types.Strictness, runtime … schema.py:465). The
audit module is already exactly the "block above a threshold" pattern requested.
Decision Drivers
- One coherent model for "what happens when a check does not pass," across failed / degraded / informational / could-not-run outcomes.
- Operator owns strictness, not each gate author — mirrors linter/typechecker strictness config.
- Do not add a sixth Severity enum; converge on one canonical ladder.
Decision Outcome
A gate produces a typed outcome; the operator's error-handling strategy maps it to a CLI effect.
- Severity ladder (
ERROR_SEVERITY):BLOCKING>RECOVERABLE>WARN>INFO.RECOVERABLEmeans "the system may proceed in a degraded fashion" (the outcome is recorded/surfaced, the transition is not necessarily stopped). - Every gate outcome carries a severity — a check failure, and every could-not-run condition (untrusted publisher, crash, timeout, missing interpreter), maps to a severity. "Untrusted" is therefore not a special skip; it is one severity-bearing outcome among many.
- The operator selects an error-handling strategy in
.kittifyconfig, e.g.block_above(RECOVERABLE)— outcomes at or above the threshold block the transition; below it, the CLI proceeds (degraded) and records the finding. This is the existingauditfail_onthreshold pattern, generalised. - Canonical severity, not a new enum.
ERROR_SEVERITYis defined once (extend/rename the kernelSeverity— the layering-correct home, importable by doctrine) and the fragmented per-module Severity enums are migrated onto it. A gate schema references the canonical ladder. - A default strategy ships (recommended
block_above(RECOVERABLE)soBLOCKINGandRECOVERABLEstop a transition by default,WARN/INFOdo not) — chosen so security gates fail closed out of the box, and CI without operator config still blocks loudly rather than silently no-ops.
Consequences
Positive
- One knob for strictness; the fail-closed/skip contradiction dissolves — the operator's threshold decides, uniformly, regardless of why a gate didn't pass.
- Consolidates five drifting Severity definitions onto one ladder (a debt-reduction win).
RECOVERABLEexpresses "proceed degraded," which no boolean could.
Negative
- Consolidating the existing Severity enums is a cross-cutting refactor with its own blast radius (audit, status/doctor, charter-lint, glossary) — must be sequenced carefully or scoped as a precursor.
- A permissive operator strategy can weaken security gates — the default must fail closed, and the effective strategy should be surfaced (so "why did this pass?" is answerable).
Neutral
- The strategy grammar (
block_above(threshold)vs a per-severity action map) is a config-schema detail (open Q).
Open questions (for scoping / a later pass)
- Extend/rename
kernel.glossary_types.Severity, or mint a new canonicalERROR_SEVERITYand migrate the others onto it? - Strategy grammar: single
block_above(threshold), or a per-severity action map ({BLOCKING: block, RECOVERABLE: block, WARN: warn, INFO: log})? - Is severity gate-declared, outcome-declared (the check emits it), or both (gate caps it)?
- Does the Severity consolidation ship as a precursor mission, or inside the gate mission?