The gap
In JSON Schema 2020-12 minimum and exclusiveMinimum are independent keywords that both apply; a schema may legally declare both, and the effective bound is the tighter one — but the pair is not redundant, because they constrain differently.
ir.Constraints holds one bound per side, so annotation/constraints.go:259-270 reconciles them: the tighter is kept and the other is preserved as degraded_lowering.
Why it matters
The reconciliation is defensible, and the loser is preserved, so this is not silent loss. But a consumer comparing constraints across two versions sees only the survivor: a change to the dropped keyword reads as no change, and a change that swaps which keyword is tighter reads as a change of a different kind than the one that happened.
Suggested fix
Either carry both bounds per side (Minimum and ExclusiveMinimum rather than one slot plus a flag), or state in the Constraints field docs that a co-declared pair is lossy for comparison purposes so consumers know to read the Unmodeled entry.
Found by a study of dexpace/spaceapi adopting morphic IR as its input format (spaceapi#56), 2026-09-05. Related: #418 (non-emitter consumer), #419 (cross-revision identity).
The gap
In JSON Schema 2020-12
minimumandexclusiveMinimumare independent keywords that both apply; a schema may legally declare both, and the effective bound is the tighter one — but the pair is not redundant, because they constrain differently.ir.Constraintsholds one bound per side, soannotation/constraints.go:259-270reconciles them: the tighter is kept and the other is preserved asdegraded_lowering.Why it matters
The reconciliation is defensible, and the loser is preserved, so this is not silent loss. But a consumer comparing constraints across two versions sees only the survivor: a change to the dropped keyword reads as no change, and a change that swaps which keyword is tighter reads as a change of a different kind than the one that happened.
Suggested fix
Either carry both bounds per side (
MinimumandExclusiveMinimumrather than one slot plus a flag), or state in theConstraintsfield docs that a co-declared pair is lossy for comparison purposes so consumers know to read theUnmodeledentry.Found by a study of
dexpace/spaceapiadopting morphic IR as its input format (spaceapi#56), 2026-09-05. Related: #418 (non-emitter consumer), #419 (cross-revision identity).