024-Schema severity is per-constraint, fail by default
Type: ../types/adr.md · Status: accepted
Status: accepted Date: 2026-06-04
Context
The validator (commonplace.lib.validation) decided schema-violation severity from the instance path where the error landed: a hardcoded _FAIL_PATHS set (("frontmatter","description"|"tags"|"type")) failed; everything else — every body pattern, every required heading — warned. Two problems:
- Path collisions make per-constraint severity inexpressible. Many distinct constraints report at the same path. Every body rule in the review schema lands at
("body",)— the**Write agency:**requirement, themixed-form ban, the dead**Read-back timing:**ban — and all headingcontainschecks land at("headings",)._FAIL_PATHScannot tell them apart, so "make this rule fail" is unrepresentable regardless of how the set is edited. - It is operationally misleading. A review missing required structure (no
## Write-side placement, missingsource-tier, a legacymixedvalue) still reportedOverall: PASS (N warnings), even though the spec says required sections are enforced. A cleanPASSdid not mean conformance.
Severity is really per-constraint author knowledge: whether breaking a rule should block depends on what the rule means, which its author knows when writing it. Inferring it from error location is a lossy proxy. A survey of rule-based checkers (JSON Schema/ajv, Spectral, vacuum, Redocly, ESLint, SARIF, Schematron) converges on one invariant — severity keyed to a stable rule identifier, separate from matching logic — whether inline or in an external config layer. Raw JSON Schema has no native severity, so adding one is adopting a standard, not inventing one.
Decision
A constraint's severity is authored on the constraint, and the schema fails by default.
- Default schema-violation severity is
fail; a constraint opts down towarnexplicitly (_DEFAULT_SCHEMA_SEVERITY = "fail", with aseverity: warnannotation read offerror.schema). Softness is the marked case. - The opt-down is keyed by an optional stable
ruleIdon the constraint, so a rule can be re-levelled, referenced, or overridden without touching the matching logic. _FAIL_PATHS(instance-path severity) is removed.
Consequences
- All note types now hard-fail schema violations unless the constraint marks itself
warn. A cleanOverall: PASSagain means structural conformance. - Zero blast radius on flip. The corpus audit found 3 schema-derived warnings total (one index
minItemsrule), so fail-by-default broke nothing existing — it bites future violations, which is the point. The one rule was removed rather than softened. Fail-by-default stays cheap only while the corpus is kept clean; audit before flipping, don't assume. - The required-section / token contract for
agent-memory-system-review(write-side placement,**Read-back:**verdict,source-tier, no legacymixed) is now genuinely enforced, not advisory. - Deferred follow-ons: an external override map for severity, and folding the hand-coded (non-schema) checks into the same per-rule severity model.
- The principle generalises: any rule-based checker that centralises severity away from the rule, or infers it from error location, hits the same collision and blast-radius problems; the fix is severity on the identified rule, fail by default.