Dónde va un valor obsoleto: separar el conjunto vivo del retirado sin perder historial
Un desarrollador cuenta cómo resolvió el encaje de una clave de firma retirada en un esquema de validación cerrado: no ensanchando el conjunto, sino creando uno segundo.

Un chequeo rechazó una fila que un desarrollador acababa de añadir. La fila era una clave de firma retirada, conservada para poder seguir leyendo los registros firmados con ella. El chequeo respondía con scheme_outside_the_set, scheme=keyed_sha256, schemes=["ed25519"]. El conjunto de esquemas aceptados tiene un único elemento y un documento de límites publicado lo dice en una frase. La clave retirada no pertenece a ese conjunto. El chequeo tenía razón.
Con eso sobre la mesa, había tres sitios donde meter un valor obsoleto. El primero, ensanchar el conjunto a dos: los registros antiguos se leen, pero la afirmación publicada pasa a ser falsa y nadie avisa en ningún momento. El segundo, tirar el valor viejo: el historial queda ilegible para siempre. El tercero, y el elegido, un conjunto segundo y separado: se mantiene la lectura y la afirmación sigue siendo verdad, pero acotada.
Separar por capacidad, no por fecha
El resultado son dos conjuntos cerrados, schemes = ["ed25519"] para verificar registros nuevos y schemes_retired = ["keyed_sha256"] para leer uno antiguo, nunca para verificar uno nuevo. La frontera entre ambos es una capacidad, no una marca temporal. Una entrada retirada no es un miembro débil del conjunto vivo: no es miembro del conjunto vivo, punto.
Lo caro no fueron las dos líneas de definición, sino lo que arrastran. Dos motivos de rechazo nuevos, cada uno con su control en las dos direcciones: no basta con que un esquema retirado se acepte para lectura, un esquema vivo tiene que seguir rechazándose donde se espera uno retirado, y al revés. Dos motivos, cuatro controles.
La afirmación publicada no se editó: se retiró y se volvió a sostener. La frase antigua decía que el conjunto tiene un elemento, era cierta y lo sigue siendo para el conjunto vivo. El documento conserva la frase marcada como retirada y añade al lado la versión acotada, para que quien viera la original sepa qué le pasó. Los contadores se imprimen por separado y a propósito: un keys=1/1 a secas habría sido cierto y habría escondido todo el cambio.
Lo que se quedó sin actualizar
Hay una consecuencia que el autor no arregló. Una auditoría pesa cada fila de clave contra el conjunto vivo y nada más, así que tras la división informa de una violación falsa: audit: scheme_outside_the_set=1. No es un fallo nuevo. Es el mismo defecto de siempre, que ahora se ve: ese lector no tenía concepto de fila retirada y hasta ahora no existía ninguna fila retirada sobre la que equivocarse. Queda registrado como laguna conocida, con su propia entrada, en lugar de parchearse en el mismo cambio.
Está también el detalle de los nombres. La primera idea fue numerar los dos códigos nuevos siguiendo la secuencia de los que ya había. Una regla del propio spec lo prohíbe para esa clase: hay otros dos contadores calculados sobre el rango numerado, y meter dos entradas más habría dejado mal ambos recuentos con todos los tests individuales en verde. Los códigos entraron por propiedad, en la sección que agrupa los de su tipo. Una convención de nombres que parece cosmética sostenía carga, porque algo aguas abajo contaba esos nombres.
El pendiente es ese lector de auditoría, que sigue dando un falso positivo con nombre en lugar de una edición silenciosa que haga coincidir a todo el mundo sin que nadie compruebe por qué. La parte reutilizable: dividir un conjunto es un cambio de interfaz para todo lo que lo lee, y cada consumidor asume en silencio que el conjunto significa "todos". Conviene localizarlos antes, y donde no se puedan arreglar hoy, dejar anotado cuáles ya están equivocados.

