A check refused a row I had just added. The row was a retired signing key, kept so that records signed under it can still be read. The check said: refused scheme_outside_the_set scheme=keyed_sha256 schemes=["ed25519"] The set of accepted schemes has exactly one element, and a published limits document says so in one sentence. The retired key's scheme is not that element. The check was right. Three places a deprecated value can go option what happens to old records what happens to the published claim widen the set to two readable now false drop the old value unreadable, permanently stays true a second, separate set readable stays true, scoped The first is the one that costs nothing today. It also makes a sentence someone may have relied on quietly untrue, and there is no moment at which anyone is told. The second is the one that feels principled and destroys history. What went in was the third: schemes = ["ed25519"] # may verify a new record schemes_retired = ["keyed_sha256"] # may read an old one, never verify a new one Two closed sets, with the boundary between them being a capability rather than a timestamp. A retired entry is not a weaker member of the live set. It is not a member of the live set at all. What that actually cost More than the two lines above, and the extras are the interesting part. Two new refusal reasons, each with a control in both directions. It is not enough that a retired scheme is accepted for reading; a live scheme must still be refused where a retired one is expected, and vice versa. Two reasons, four controls. The published claim was retired and re-stood, not edited. The old sentence said the set has one element. That was true and is still true of the live set. The document keeps the old sentence marked retired and adds the scoped one beside it, so a reader who saw the original can tell what happened to it. OK_KEYS keys=1/1 declared=1/1 private_in_tree=0 keys_live=1 keys_retired=1 schemes_retired=1 The counts are printed separately on purpose. keys=1/1 alone would have been true and would have hidden the whole change. The naming rule that stopped me making it worse My first instinct for the two new reasons was to number them, following the sequence the surrounding codes use. A rule in the same spec forbids that for this class of code. I was irritated by it for about a minute, and then noticed what numbering would have done: two other counters are computed over the numbered range, and adding two entries to that range would have made both of those counts wrong while every individual test stayed green. The codes went in by property instead, in the section that carries codes of that kind. Nothing that counts the numbered range changed. A naming convention that looks cosmetic was load-bearing because something downstream counted the names. The consumer I did not update One audit was left behind. It weighs every key row against the live set alone, so after the split it reads a false violation: audit: scheme_outside_the_set=1 (the retired row, correctly recorded, wrongly judged) That is not a new bug introduced by the split. It is the same defect that was always there, made visible by the split: that reader had no concept of a retired row, and until today no retired row existed for it to be wrong about. It is recorded as a known gap with its own entry rather than patched in the same change. I would rather have one honest false positive with a name than a quiet edit that makes a reader agree without anyone checking why. The rules A closed set with no retired class forces a choice between a false claim and lost history. If a set can ever contain something deprecated, decide where deprecated things go before you need to put one there. Splitting a set is an interface change for everything that reads it. Every consumer has an implicit assumption that the set means "all of them". Find those consumers, and where you cannot fix them today, record which ones you know are now wrong. Check what counts your names before you add one. Numbering has a way of being a schema.