Expediente 63/0: el almacén bitemporal escrito en un solo tiempo
Un almacén con dos columnas de tiempo, escrito durante dos meses en una sola. Sesenta y tres días de deriva, cero reconciliaciones, y una validación que respondió «0 hallazgos» todo el tiempo — diciendo la verdad
🩺 Mostrar historia clínica → 📋 Protocolo de sesión →
Historia clínica · Servicio de Patología del Software
Un almacén construido para guardar dos tiempos por cada hecho —cuándo se registró y desde cuándo es cierto— escrito durante dos meses usando uno solo. El resultado no es un almacén a medias: es un fichero que crece. Sesenta y tres días entre el estado que guardaba y el grafo real. Cinco commits cambiaron el grafo en esa ventana y ninguno lo alcanzó. Cero retracciones emitidas en toda la historia previa, porque el código no tenía esa palabra. Y el reloj que ordena las operaciones arrancaba en 1 en cada pasada, así que lo de julio quedaba archivado debajo de lo de mayo. Nada de esto disparó nada. validate ontology respondía 61 nodos, 175 aristas, 0 hallazgos — y respondía la verdad, porque la pregunta que hacía era otra.
Mostrar cuadro
- bitemporal
- Un almacén que guarda dos tiempos por cada hecho: cuándo se registró (tiempo de transacción) y desde cuándo es cierto en el mundo (tiempo de validez). Sirve para una sola cosa, y es enorme: poder preguntar «¿qué creía el sistema el martes?» sin que la respuesta de hoy la haya borrado.
- supersesión
- Sustituir un hecho sin destruir el anterior: el nuevo se apila encima y el viejo sigue alcanzable por su fecha. Es lo contrario de sobrescribir. Un almacén que supersede acumula historia; uno que sobrescribe produce un presente cada vez más solo.
- retracción
- La operación que declara que un hecho dejó de valer. No lo borra: lo cierra. Sin ella un almacén sólo sabe añadir, así que lo que desaparece del origen no desaparece de la proyección — simplemente deja de mencionarse, y nadie distingue «se retiró» de «no se ha vuelto a decir».
- reloj lógico
- El sello de orden que lleva cada operación. Su única obligación es no retroceder nunca. Un reloj que arranca en 1 en cada pasada cumple el tipo, satisface al compilador, ordena consistentemente dentro de su pasada — y coloca lo de hoy debajo de lo de mayo, sin quejarse.
Protocolo para declarar, versionar y verificar esto → ontoref.dev
El doble balance — lo que costó, y lo que dejó
Las cifras de la izquierda son el cuadro; las de la derecha, el alta. Pero la que decide el caso no está en ninguna de las dos columnas, sino entre ellas: durante los sesenta y tres días, la validación respondía cero hallazgos. Es la medida exacta de la distancia entre medir la estructura y medir la frescura, y esa distancia había sido, hasta hoy, invisible por construcción — porque ninguna de las dos preguntas sabía de la existencia de la otra.
Lo que costó el crimen
- Días entre el estado que la proyección guardaba y el grafo real 63
- Reconciliaciones en esa ventana 0
- Commits que cambiaron el grafo en esa ventana, sin que la proyección se enterara 5
- Edad del fichero el día que se incorporó a git 45 días de caducidad
- El único commit al almacén en toda la ventana fef21f4 · «init repo» · 2026-07-10
- Operaciones de retracción emitidas en toda la historia previa 0
- Valor del reloj al empezar CADA pasada de extracción 1
- Lo que respondía la validación mientras todo esto ocurría 61 nodos · 175 aristas · 0 hallazgos
- Celdas asertadas en la primera reconciliación real 1097
- Celdas retractadas en esa misma pasada 426
- Entidades retiradas 91
- El almacén, antes y después 3.686.400 → 4.747.264 bytes
- Identidades distintas del grafo en una tarde 79cf7a96… → 60702123… → aa7967c8…
Lo que el caso dejó
- La regla un almacén bitemporal escrito en un solo tiempo no es un almacén a medias: es un fichero grande
- La extracción reconcilia — aserta, retracta y retira; una pasada sin cambios no apenda nada
- El reloj anclado al reloj de pared · una pasada es UN instante
- La puerta un commit que toca el grafo sin reconciliar se rechaza — y sólo lee
- El corpus 13 pruebas en
extract.rs· 3 enrojecen si se devuelve el reloj a 1 - El arreglo, medido 740+ / 78− en 7 ficheros · 142+ / 21− sólo en el reloj
- La práctica, declarada
substrate-reconciliation, consultable en la ontología - La deuda, escrita nada ata todavía el extractor al contrato de ADR-041
El punto de abandono — lo que no sale en la tabla
El punto de abandono estuvo al principio, y fue el diagnóstico inicial: «ontoref no tiene supersesión de aristas». Era una respuesta razonable, y era falsa. La salida cómoda estaba servida —construir la supersesión que ya existía, encima de la que ya existía— y habría producido un segundo mecanismo funcionando en paralelo al primero, cada uno con su forma, sin que ninguno de los dos supiera del otro. Es decir: el defecto de este mismo expediente, cometido a mano mientras se investigaba. Lo que lo evitó no fue prudencia: fue abrir el fichero antes de escribir nada, y encontrarse tx_time, valid_from y as_of en las primeras seis líneas.
Diagnóstico diferencial — lo que se descartó
| «ontoref no tiene supersesión de aristas» | “«ontoref no tiene supersesión de aristas.» Falso, y se descarta abriendo el fichero: `tx_time`, `valid_from` y `as_of` llevaban meses ahí. La hipótesis era razonable por una razón que ya es el segundo hallazgo del caso — la capacidad estaba construida y no documentada, así que la única forma de saber que existía era leer el código de quien la escribió. Un diagnóstico inicial equivocado sobre las propias capacidades no es descuido: es la consecuencia previsible de no publicarlas.” | descartado — pero delata una capacidad no publicada |
| «re-extraer fallaría: el reloj no sería monótono» | “«Re-extraer fallaría: los relojes no serían monótonos.» Suena a bloqueo serio, y se descarta leyendo `OpLog::append`: la comprobación de monotonía mira los padres de la operación, y la extracción los emitía vacíos. La comprobación existía, era correcta, y no disparaba nunca. Un guardia mirando una puerta por la que nadie pasa.” | descartado — un guardia sin puerta |
| «la puerta tiene que re-extraer para saberlo» | “«La puerta tendrá que re-extraer para saber si hay deriva.» Descartada al mirar el coste: el almacén está en git, pesa alrededor de un megabyte por reconciliación, y reescribir un fichero versionado a mitad de un commit obliga a rehacer el índice. La puerta que hoy corre no escribe: sólo lee. Y esa restricción no la impuso el gusto, la impuso la aritmética.” | descartado — por aritmética, no por gusto |
| «en dry-run no se apenda, luego no toca el fichero» | “«En dry-run no se apenda, luego el fichero no se toca.» **Descartada midiendo bytes**, y es la mejor del grupo: el fichero cambiaba igual. `OpLog::open` usa `Database::create`, que confirma una transacción de escritura sólo con abrir. Lo importante no es el defecto, es lo que casi ocurre — una prueba que contara operaciones habría pasado en verde sobre una puerta que ensuciaba el árbol en cada ejecución. Se cazó porque se midió el artefacto, no la intención.” | descartado midiendo — y ahí asomó la trampa |
| «el contrato está en un ADR aceptado, luego el código lo cumple» | “«El contrato está en un ADR aceptado, luego el código lo cumple.» CULPABLE, y no es de nadie. ADR-041 declara el modelo bitemporal con todas sus letras: retracción, los dos tiempos, hechos superseded y no sobrescritos. Está aceptado. `extract.rs` no emitía una sola retracción, pasaba el segundo tiempo vacío siempre y reiniciaba el reloj en cada pasada. Las dos afirmaciones convivieron meses en el mismo repositorio, contradiciéndose, y nada las puso jamás en la misma frase. Un ADR aceptado se lee como una descripción del sistema. Es una descripción de la intención.” | CULPABLE — y no es de nadie: es del hueco entre el ADR y el código |
Etiología — la causa — extract.rs — el segundo tiempo, siempre vacío; el reloj, siempre en 1
// extract.rs — antes
let mut next_ts = 1u64; // ← en CADA pasada. Mayo y julio, ambos en 1.
for node in nodes {
let op = build_signed_op(…, Hlc::new(next_ts, 0));
log.append(&op)?; // ← append. Sólo append. Siempre append.
next_ts += 1;
}
fn build_signed_op(…) -> Operation {
OpBody {
parents: vec![], // ← sin padres: la comprobación de
payload: OpPayload::EntityAssert { // monotonía nunca dispara
attrs,
entity: EntityId::new(entity_id),
valid_from: None, // ← el segundo tiempo, siempre vacío
},
timestamp,
}
}
// Y en todo el fichero, ni una sola vez: OpPayload::Retract
Y conviene ver la forma completa, porque es la que se repite: el contrato que describía todo esto correctamente —ADR-041, con retracción, los dos tiempos y la supersesión— estaba escrito, aceptado y vigente. No se ignoró por prisa ni se discutió. Sencillamente nada en el sistema tenía autoridad para comparar el texto del ADR con el cuerpo de extract.rs, así que la contradicción no era un fallo que alguien pudiera ver: era un hecho sobre dos ficheros que nunca se leían juntos.
Tratamiento — Reconciliar en vez de sobrescribir, anclar el reloj al mundo, y poner una puerta que sólo lee
# 1. Reconciliar: leer lo que ya hay, y emitir SÓLO la diferencia.
1097 celdas asertadas · 426 retractadas · 91 entidades retiradas
# una re-extracción sin cambios apenda 0 operaciones. Es una prueba.
# 2. Anclar el reloj al mundo. Una pasada es UN instante, no una cuenta.
Hlc::new(wall_clock_millis, seq) # y una pasada posterior ordena
# por encima de una anterior POR FECHA
# 3. La puerta. Toca el grafo sin reconciliar y el commit no existe.
substrate-freshness · `sync substrate --check` · sólo LEE
# 4. Lo que la puerta NO comprueba, escrito en el propio expediente:
# frescura ≠ conformidad bitemporal. Un extractor monotemporal
# reescrito mañana pasaría en verde.
La confesión va en el expediente porque es la prueba, no el adorno: la puerta nueva comprueba frescura, no conformidad bitemporal. Pregunta «¿se reconcilió?» y no «¿se reconcilió emitiendo retracciones y conservando lo superseded?». Un extractor reescrito mañana en monotemporal pasaría check-substrate en verde — es decir, este mismo caso, otra vez, sobre su propia cura. Y hay una frontera que ya no se puede reparar: las operaciones anteriores al anclaje conservan sus números de secuencia. Son alcanzables por posición y no por fecha, para siempre, y eso está visible en el dato en vez de disimulado.
Pronóstico
La serie pregunta: ¿con ontoref no hubiera sido así? Y este caso obliga a la respuesta incómoda, porque el mecanismo no faltaba: estaba construido, probado y aceptado. El modelo bitemporal existía con todas sus piezas —los dos tiempos, la retracción, la supersesión— y estaba declarado en un ADR aceptado que cualquiera podía leer. El código que lo alimentaba no usaba ninguna de esas piezas. Y las dos cosas convivieron meses en el mismo repositorio sin rozarse, porque nada en el sistema tenía la autoridad de leer un ADR y un fichero fuente en la misma frase. Lo que hace que el caso valga —lo que lo separa de un fallo de tubería— es que no hubo ningún momento en que algo pareciera roto. La validación del grafo respondía cero hallazgos, y era verdad: mide integridad estructural, no frescura de la proyección. El almacén crecía en disco, lo cual se lee exactamente como salud. El fichero estaba versionado, lo cual se lee como custodia. El día que entró al repositorio llevaba cuarenta y cinco días caducado y el mensaje del commit decía «init repo», que es lo que se escribe cuando algo empieza, no cuando algo lleva mes y medio parado. Cada señal disponible apuntaba a un sistema sano. La enfermedad consistía precisamente en no dejar rastro: un almacén que sobrescribe no acumula pruebas de haber sobrescrito. Y por eso el hallazgo no es el defecto sino la creencia que lo sostuvo — que un contrato declarado y aceptado describe el sistema. No lo describe. Describe la intención, y la distancia entre las dos sólo es visible cuando alguien construye el mecanismo que las obliga a coincidir. Aquí ese mecanismo todavía no existe: la puerta que se levantó mide frescura, no conformidad. Está escrito arriba, en la deuda, con la dirección de lo que la saldaría.
| El código contradice un contrato declarado y aceptado | review-against-invariant. ADR-041 declara el modelo: Retract, tx_time frente a valid_from, hechos superseded y no sobrescritos. Está aceptado. Sus restricciones no alcanzan extract.rs, así que el contrato y el código convivieron meses diciendo cosas contrarias y nada los puso en la misma frase. |
| Una proyección 63 días por detrás de su origen | declared state. No existía ningún estado declarado que dijera «la proyección está fresca», así que tampoco existía la pregunta. Lo que un sistema no declara no lo puede consultar, y lo que no puede consultar lo cuenta como bueno. |
| Un commit incorpora un fichero caducado sin decirlo | gate. Hoy substrate-freshness se niega en el acto. El 10 de julio nada miraba, así que un fichero con 45 días de caducidad entró al repositorio con el mensaje «init repo» y se leyó como una inicialización. |
| Una capacidad construida y no documentada | qa/howto. as_of llevaba meses funcionando en el almacén y no había forma de saberlo sin leer el código, así que el primer diagnóstico de la sesión fue «esto no existe». Una capacidad que sólo conoce quien la escribió no es una capacidad del proyecto: es una del autor. |
| Una puerta que comprueba lo adyacente a lo que promete | Declararlo. La puerta nueva mide frescura y no conformidad, y eso va escrito en lesson_debt de este mismo expediente. Una puerta que no declara su alcance se lee por su nombre, y su nombre siempre promete más de lo que mira. |
La profilaxis — qué exige hoy la lección
- ✓La extracción reconcilia en vez de sobrescribir: emite Retract, conserva lo superseded y una re-extracción sin cambios no apenda nada
test:code/crates/ontoref-ontology-content/src/extract.rs - ✓Un commit que toca el grafo sin reconciliar el substrato se rechaza antes de existir
gate:just check-substrate - ✓…y en la cadena, no como costumbre: la puerta corre en cada commit y sólo lee
gate:.pre-commit-config.yaml#substrate-freshness - ✓Una pasada de extracción es UN instante del reloj de pared, y una pasada posterior ordena por encima de una anterior por fecha
test:code/crates/ontoref-ontology-content/src/extract.rs#a_run_is_stamped_with_the_wall_clock_and_is_one_instant - ✓La reconciliación del substrato es una práctica declarada del proyecto, consultable, no un hábito de quien se acuerde
ontology:.ontoref/ontology/core.ncl#substrate-reconciliation
⊘Deuda declarada: Ninguna restricción tipada ata extract.rs al contrato que ADR-041 declara. La puerta nueva comprueba FRESCURA —¿se reconcilió?— y no CONFORMIDAD BITEMPORAL —¿se reconcilió emitiendo Retract y conservando lo superseded?—. Un extractor reescrito mañana en monotemporal pasaría check-substrate en verde, que es la forma exacta de este caso repetida sobre su propia cura. Lo saldaría una restricción en ADR-041 cuyo oráculo fuese el corpus de pruebas que esta sesión dejó, y la decisión de identidad de entidad (edge:{from}:{to}) que quedó sin tomar. Y hay una segunda deuda, mayor y más incómoda: la historia bitemporal existe en disco y ningún binario ni endpoint expone as_of. Nadie puede mirarla. Queda a la espera de decidir si una superficie puede responder por encima de su tier.
Alta y profilaxis
El alta se firma con un hecho y una reserva. El hecho: la primera reconciliación real emitió cuatrocientas veintiséis retracciones, y cada una de ellas es un cambio que llevaba meses ocurrido en el grafo y nunca registrado en su proyección — no las encontró nadie leyendo, las encontró el mecanismo el primer día que existió. La reserva es del mismo tamaño: la puerta que hoy protege esto mide si se reconcilió, no si se reconcilió bien. Es una puerta honesta con un alcance más estrecho que su nombre, y la única forma de que eso no se convierta en el próximo expediente es que esté escrito aquí, con la dirección de lo que lo saldaría, en vez de descansar en que alguien se acuerde.
Glosario
Sin coincidencias.