Parte V · En la práctica
Un servicio on-demand
El spa fue un estanque quieto. Aquí entramos a los rápidos: un viaje en taxi donde deciden cuatro protagonistas a la vez —uno de ellos un programa—, las situaciones se encadenan al segundo y el precio sube porque empieza a llover.
Camila Duarte sale de un edificio frente a la plaza a las 22:40 de un viernes, abre la aplicación en el teléfono y toca «Pedir viaje». Veintiocho minutos después baja del auto en la terminal de buses. Para ella ocurrió una sola cosa: pidió un taxi y llegó. Pero en los servidores de la plataforma, entre el toque del botón y el portazo final, pasaron muchas: el sistema rastreó conductores cerca, eligió a uno, el conductor aceptó, condujo hasta la plaza, recogió a Camila, la llevó a la terminal y cerró el viaje. Cada paso dejó su huella, movió un cobro, alteró un mapa de tráfico y (si algo hubiera salido mal) habría abierto un reclamo que alguien en soporte tendría que reconstruir meses más tarde.
Visto desde la ingeniería de datos, un viaje en taxi no es un evento: es una cadena de seis situaciones que ocurren una tras otra, donde entran y salen distintos protagonistas, con relojes cronometrados al segundo y con reglas de precio que dependen del clima y de la demanda en tiempo real. El capítulo anterior fue modelar un estanque quieto: el cliente llega, se relaja, paga y se va. Este nos lanza a la corriente, y nos obliga a poner en práctica tres cosas que hasta ahora vimos sobre todo en teoría: la agencia contextual, la vigencia de lo que cambia y los cables del porqué.
Lo que pone a prueba este capítulo
Tres tensiones que el spa no tenía. Agentes múltiples: no solo personas deciden, también un programa. Situaciones encadenadas: media docena de eventos enganchados por orden y por causa, agrupados bajo una unidad comercial. Causalidad de mercado: el precio sube por la lluvia, y hay que poder explicarle a un pasajero, meses después, exactamente por qué.
Rumbo en una página
Fijemos el negocio antes de modelarlo. Rumbo es una plataforma de transporte que conecta a quien necesita moverse con quien tiene un auto y tiempo. No posee vehículos ni emplea conductores en relación de dependencia: opera el algoritmo que los empareja y cobra una comisión por cada viaje. Esa frase aparentemente trivial («opera el algoritmo») es la que vuelve interesante el dominio, porque el algoritmo toma decisiones con consecuencias legales y económicas, y eso lo convierte en un agente de pleno derecho.
En el viaje que seguiremos intervienen cuatro protagonistas. Conviene presentarlos con sus identificadores, porque a partir de aquí los nombraremos por ellos:
🧍
La pasajera · pasajera_duarte
Camila Duarte. El agente humano que solicita el servicio, lo recibe y, al final, lo califica.
🤖
La plataforma · plataforma_rumbo
El software que decide a qué conductor asignarle el pedido. No es un programa pasivo: es quien asigna.
🧑✈️
El conductor · conductor_salas
Iván Salas. El agente humano que acepta el viaje, recoge a la pasajera y la traslada.
🚗
El vehículo · vehiculo_4821
La máquina física (placa 4821) que hace posible el traslado. El instrumento del trabajo.
Cuatro protagonistas, una sola transacción
Si le preguntas a cualquiera «¿quién participa en el viaje?», la respuesta obvia es: Camila y su conductor, Iván. Pero en el modelo hay dos participantes más que suelen pasar inadvertidos. Uno es la plataforma (un agente de software que toma decisiones autónomas); el otro, el vehículo, la máquina que ejecuta el traslado. Para que la base de datos sea auditable, los cuatro tienen que dejar rastro.
Aquí entra en acción una de las decisiones de diseño más liberadoras de la arquitectura: la
agencia contextual (D5). El rol de agente no está reservado a los
humanos; lo puede ocupar quien ejecute la acción del verbo, sea persona,
organización, sensor o software. Y el verbo central de este viaje (asignar) no lo
ejecutó Camila ni Iván: lo ejecutó el algoritmo. Fue él quien decidió, a las 22:41, darle
ese pedido a ese conductor. Negarle la condición de agente sería falsear
quién tomó la decisión.
Observa cómo se acomodan los cuatro participantes en la situación de asignación. Tres caen
en el eje Q —la pasajera (en cuyo nombre se asigna), la plataforma (que asigna)
y el conductor (a quien se asigna)— y uno, el vehículo, cae en O como
instrumento:
Q como agente,
igual que entraría una persona: para el modelo, quien decide es agente, sea de carne o de
código. El vehículo entra en O como instrumento. Los dos humanos
ocupan sus papeles esperables. Que el algoritmo y el conductor compartan el eje
Q es, precisamente, la decisión D5 en funcionamiento.Construir esa situación en el prototipo no exige redactar las tripletas a mano. Una llamada consulta el lexicon, desambigua el verbo y enchufa los cuatro cables, validando que cada uno aterrice en su eje correcto:
asignacion = ingest_situation(u, lex, "asignar",
roles={
"agente": plataforma_rumbo, # Q · el algoritmo decide ← D5
"tema": solicitud_7732, # O · qué se asigna: el pedido de Camila
"beneficiario": conductor_salas, # Q · a quién: al conductor
"instrumento": vehiculo_4821, # O · con qué: el auto
"momento": t_2241, # T · la hora exacta
},
sit_id="asignacion_7732",
)
¿Y si mañana hay que auditar al GPS?
El vehículo vive en O como instrumento porque, casi siempre,
actúa por orden de un humano. Pero si un día el coche tomara una decisión propia (un
piloto automático que frena solo, un sensor que reporta una falla), no hay que rediseñar
nada: se crea un agente gps_4821 en Q, vinculado al vehículo
físico de O, y ese agente pasa a ocupar el rol del verbo que ejecutó. El
modelo no obliga a elegir entre humano y máquina: la agencia depende del verbo, no de la
sustancia de quien actúa.
La cadena de las seis situaciones
Cronológicamente, el viaje de Camila genera seis situaciones reificadas (seis nodos independientes en el grafo), cada una con su hora y sus participantes:
solicitar → asignar → aceptar → recoger → trasladar → completar
22:40 22:41 22:42 22:51 22:53 23:08
Esas flechas no son adorno tipográfico: son cables reales del modelo, precede
y su inverso sigue_a. La consecuencia es enorme. El sistema conoce el orden
exacto de los eventos sin tener que restar marcas de reloj cada vez que se le pregunta. Si
alguien quiere saber «¿qué pasó justo después de que Iván aceptara?», el motor sigue
el cable al nodo siguiente y responde de inmediato, sin aritmética de fechas. El orden
temporal está cosido a la estructura, no calculado a posteriori.
Pero el tiempo no es lo único que liga estos eventos. Algunos ocurren a causa de
otro. La plataforma no asignó el viaje a Iván por azar a las 22:41: lo hizo
porque Camila tocó el botón un minuto antes. Esa relación no es temporal —es
causal—, y para guardarla el modelo no tiene un eje «por qué», sino cuatro cables distintos
(D7). Aquí el adecuado es motivado_por:
(asignacion_7732, sigue_a, solicitud_7732) # cable temporal: el orden
(asignacion_7732, motivado_por, solicitud_7732) # cable causal: el porqué
Y que Iván recoja a Camila no es solo «el siguiente paso en el tiempo»: es una obligación nacida del momento en que aceptó. Por eso la recogida lleva los dos cables a la vez, y no se estorban entre sí:
(recogida_7732, sigue_a, aceptacion_7732) # ← cable temporal
(recogida_7732, motivado_por, solicitud_7732) # ← cable de motivo
Los dos cables sirven a públicos distintos. El temporal alimenta los cálculos operativos (cuánto tardó la plataforma en asignar, cuántos minutos esperó la pasajera, qué velocidad media tuvo el traslado). El causal alimenta al auditor, que no quiere saber cuándo sino por qué se tomó cada decisión. Tenerlos separados, y a la vez en el mismo grafo, es lo que permite responder ambas preguntas sin reconstruir nada.
Falta una pieza. Si estos seis eventos quedaran sueltos en el servidor, serían huérfanos:
ninguno sabría a qué viaje pertenece. Por eso los agrupamos bajo una unidad
comercial: creamos un nodo viaje_7732 y colgamos de él las seis
situaciones con el cable parte_de. Ese nodo es la entidad de negocio real (a él
se le pega la factura, a él acude soporte si hay un reclamo); las seis situaciones son su
interior cronológico.
sigue_a: dan el
orden sin aritmética de relojes. La flecha punteada magenta es motivado_por,
el cable causal que ata la asignación al pedido que la provocó. Las líneas tenues hacia
arriba son parte_de: cada situación cuelga de viaje_7732, la
unidad comercial. Reconstruir el viaje entero es, literalmente, recorrer estos cables.Conviene detenerse en que tres preguntas distintas conviven sobre las mismas seis situaciones, y cada una se responde siguiendo un cable que vive en un eje diferente:
T ¿en qué orden? El
cable sigue_a es asunto del eje del tiempo. Da la secuencia sin restar relojes:
preguntar «¿qué vino después de aceptar?» es seguir una flecha, no calcular una diferencia
de horas. Es lo que alimenta toda métrica operativa: espera de la pasajera, duración del
traslado, latencia de la asignación.
M ¿por qué ocurrió? El
cable motivado_por es un predicado del eje cómo, el que enlaza hechos
con hechos. No dice cuándo, sino qué provocó qué: la asignación existe porque hubo una
solicitud. Es lo que un auditor persigue cuando reconstruye una decisión.
O ¿de qué viaje es parte?
Y el cable parte_de ata cada situación a la unidad comercial en el eje
de los objetos. Tres ejes, tres preguntas, un mismo grafo.
Tarifa dinámica · explicar el caos del mercado
Hay un rasgo de este negocio que tensa de verdad la estructura de datos: el precio no es fijo. Cuando llueve, o cuando hay más pasajeros que autos disponibles, la tarifa sube. Un mismo trayecto que ayer costó 18 unidades hoy cuesta 30. El reto no es cobrar ese recargo (eso lo hace cualquier sistema) sino explicarlo seis meses después, cuando la pasajera escribe enfadada preguntando por qué le cobraron casi el doble.
Un sistema mediocre pegaría a la factura un número suelto: multiplicador: 1.67.
Funciona para el cobro y arruina la auditoría. Ese número no sabe por qué era 1.67;
no recuerda que esa noche llovía sobre la plaza ni que había tres veces más pedidos que
autos. La explicación se evaporó en el instante mismo del cobro.
La solución del modelo es reificar el estado del mercado: convertir «esa
noche, en esa zona, había pico de demanda y lluvia» en un objeto con identidad propia en
O, y atar la tarifa a él con el cable causal causado_por (D7). El
estado deja de ser un adjetivo perdido y pasa a ser un hecho consultable, con su lugar, su
hora y su intensidad:
# 1 · El estado de la calle como objeto con identidad
estado_mercado = u.add_individual(Individual(id="pico_demanda_2240", axis=Axis.O))
u.assert_fact(estado_mercado, "instancia_de", "estado_alta_demanda")
u.assert_fact(estado_mercado, "lugar_de", zona_plaza) # L · dónde
u.assert_fact(estado_mercado, "momento", t_2240) # T · cuándo
u.assert_fact(estado_mercado, "factor_clima", lluvia) # K · por qué subió
u.assert_fact(estado_mercado, "multiplicador", n_1_67) # N · cuánto
# 2 · La tarifa, atada a su causa
tarifa = u.add_individual(Individual(id="tarifa_7732", axis=Axis.O))
u.assert_fact(tarifa, "paga_de", viaje_7732)
u.assert_fact(tarifa, "monto", n_30_00)
u.assert_fact(tarifa, "moneda", moneda_local)
u.assert_fact(tarifa, "causado_por", estado_mercado) # ← el porqué, como cable
La diferencia, a la hora de operar el negocio, es nítida. Visto como tripletas, el recargo ya no es un número anónimo, sino un hecho que apunta a su causa:
Cuando Camila escriba «¿por qué pagué 30 si la otra vez fueron 18?», el sistema no
necesita que un empleado bucee en registros de tráfico. Sigue el cable causado_por
y responde solo: «La tarifa se calculó por un estado de alta demanda registrado a las
22:40 en la zona de la plaza, con un factor de lluvia, que aplicó un multiplicador de
1.67». La justificación estaba guardada como un hecho, no reconstruida a mano.
Un porqué no es un campo: es un cable a un hecho
La tentación es modelar el recargo como una columna numérica. Pero un
número no se puede auditar: no recuerda su causa. Al reificar el estado del mercado y
unirlo con causado_por, el «por qué subió» deja de ser folclore de soporte y
se vuelve un dato de primera clase, con su lugar, su hora y su factor. La auditoría más
temida del rubro se convierte en seguir un cable.
Cuando algo se cancela · el arte de no borrar nada
El rubro del transporte vive de cancelaciones. Alguien pide un auto y se arrepiente; un conductor acepta y se le pincha una llanta. La pregunta perenne de quien programa es: ¿qué hago con el viaje cancelado? ¿Lo borro? ¿Lo marco en rojo? Ambas respuestas destruyen información. Borrar deja un hueco; sobrescribir miente sobre el pasado.
La convención del modelo es la inmutabilidad: nunca se borra, nunca se reescribe la historia. Una cancelación no es un cambio sobre el viaje viejo: es un evento nuevo que lo apunta. Y ese principio es exactamente la vigencia (D6) en su forma más concreta: el estado del viaje no se modifica en su lugar, sino que se registra como un hecho fechado que abre un intervalo de validez.
# La cancelación es un evento nuevo, con su autor y su hora
cancelacion = ingest_situation(u, lex, "cancelar",
roles={
"agente": pasajera_duarte, # Q · quién canceló (queda registrado)
"tema": viaje_7732, # O · qué se cancela: el viaje
"momento": t_2243, # T · cuándo
},
sit_id="cancelacion_7732",
)
u.assert_fact(cancelacion, "cancela", viaje_7732) # el cable hacia el original
El cambio de estado del viaje no machaca un campo: se asienta como un hecho con vigencia, igual que un libro contable que solo agrega asientos y jamás tacha. El estado anterior conserva su intervalo cerrado; el nuevo abre el suyo:
(viaje_7732, estado, solicitado, inicio=22:40, fin=22:43) # vigente y luego cerrado
(viaje_7732, estado, cancelado, inicio=22:43, fin=∞) # el estado vigente ahora
Nacieron varios hechos nuevos (la situación de cancelación con su autor y su hora, el cable
cancela y la línea de estado fechada) y ninguno destruyó nada. El viaje sigue
entero en el grafo, solo que ahora su estado vigente es cancelado. Cuando la
empresa calcule los viajes facturables del día, el código simplemente ignora los que tengan
estado vigente cancelado. La base de datos se vuelve un registro histórico
indestructible: perfecto para una disputa legal, donde la pregunta no es «¿cuál es el estado
hoy?» sino «¿en qué estado exacto estaba a las 22:42?».
Tres consultas operativas
El valor de un sistema se mide por la facilidad con que responde lo que de verdad importa al cierre del día. Veamos tres preguntas críticas del rubro, resueltas como recorridos del grafo en lugar de cadenas de tablas.
Consulta 1 · ¿Adónde llevó Iván a Camila?
El motor busca un patrón (las situaciones de clase trasladar donde el agente
sea Iván y la pasajera sea Camila) y proyecta el destino. No hay tablas que unir: hay un
patrón que se satisface.
r = query(u, Pattern(
fixed={"agente": conductor_salas, "pasajero": pasajera_duarte},
ask={"destino": Var()},
type_constraint=u.ind("traslado"),
))
# → terminal_buses
Consulta 2 · ¿Cuántos viajes completó Iván esta noche?
Un conteo sobre el patrón de viajes con agente Iván y estado vigente completado.
n = count(u, Pattern(
fixed={"agente": conductor_salas, "estatus_factual": u.ind("completado")},
type_constraint=u.ind("viaje"),
))
Repara en algo que conviene no dejar pasar: este conteo es estructuralmente idéntico
al que usamos en el spa para contar los circuitos pagados de Lucía. Cambian los nombres de
los roles y de las clases (conductor_salas en vez de lucia,
viaje en vez de circuito_termal) pero la maquinaria es la misma.
La uniformidad del modelo permite reciclar la misma consulta entre un spa de barrio y una
plataforma transnacional, dos negocios sin nada en común salvo su forma profunda.
Consulta 3 · ¿Por qué la tarifa fue de 30?
Se siguen los hechos de la tarifa y se filtra el cable causal. La respuesta es el estado del
mercado que la provocó: no un número opaco, sino un hecho con lugar y hora.
hechos = u.facts_about(u.ind("tarifa_7732"))
causa = [f for f in hechos if f.role == "causado_por"]
# → pico_demanda_2240 (alta demanda + lluvia, 22:40, zona de la plaza)
De un viaje a la flota entera
Hasta aquí seguimos un viaje: la solicitud de Camila, la asignación a Iván, el cobro de la tarifa. Pero quien opera Rumbo no vive en un viaje, vive en diez mil. Y la pregunta que paga el sueldo no es «¿adónde llevó Iván a Camila?», sino «¿qué zona se queda sin autos a las siete de la tarde?» o «¿qué conductor completa más carreras de las que rechaza?». Esas no son consultas nuevas: son las mismas situaciones de viaje, agrupadas por destino, por hora, por conductor. El conteo de los viajes de Iván que vimos recién es ya un corte de ese tablero; correrlo sobre cada conductor da el ranking de la flota.
# Viajes completados hacia una zona — un corte; el tablero cruza todas las zonas y conductores
count(u, Pattern(fixed={"destino": u.ind("terminal_buses"), "estatus_factual": u.ind("completado")},
type_constraint=u.ind("viaje")))
El antes y el después · del esquema fragmentado al grafo único
Antes, en SQL. El esquema clásico de un servicio de transporte reparte la realidad
en tablas separadas que no se hablan solas: viajes, conductores,
pasajeros, tarifas, eventos_viaje. Reconstruir la
cadena completa (qué ocurrió, en qué orden y por qué el precio subió) obliga a encadenar
JOIN en cascada y luego ordenar en código por marca de tiempo. Y la pregunta
que de verdad duele («¿cuál fue la causa concreta de que esa tarifa subiera a 30 ese
minuto?») sencillamente no tiene respuesta: no existe una tabla de causas; el motivo
(lluvia, alta demanda) se perdió o agoniza en un campo de texto libre que nadie consulta con
coherencia.
-- Antes: el viaje, sus eventos y su tarifa viven en tablas distintas;
-- el orden se reconstruye ordenando por timestamp y el "porqué" no existe.
CREATE TABLE viajes (id INTEGER PRIMARY KEY, pasajero_id INT, conductor_id INT, estado TEXT);
CREATE TABLE eventos_viaje (id INTEGER PRIMARY KEY, viaje_id INT, tipo TEXT, ts TIMESTAMP);
CREATE TABLE tarifas (id INTEGER PRIMARY KEY, viaje_id INT, monto NUMERIC, multiplicador NUMERIC);
SELECT e.tipo, e.ts
FROM eventos_viaje e
JOIN viajes v ON v.id = e.viaje_id
WHERE v.id = 7732
ORDER BY e.ts; -- el orden se calcula; no está en la estructura
-- ¿la causa del multiplicador 1.67? No hay columna que lo diga.
Después, en WQuestions. La misma realidad es un grafo único. viaje_7732
agrupa las seis situaciones reificadas (solicitar, asignar,
aceptar, recoger, trasladar, completar)
con cables parte_de y sigue_a. La tarifa es un objeto en
O unido al estado de alta demanda por causado_por. Reconstruir el
viaje es seguir los cables desde viaje_7732; obtener la causa del precio es leer
un solo hecho. No hay JOIN, no hay pegamento en código que ordene por fecha, no
hay pregunta que el esquema no admita —porque el motivo nunca fue un número: siempre fue un
evento con identidad.
# Después: la cadena se recorre por el cable; la causa se lee como un hecho.
cadena = u.follow(u.ind("viaje_7732"), "parte_de") # las seis situaciones
causa = u.facts_about(u.ind("tarifa_7732"), role="causado_por") # un solo paso
En un servicio on-demand, el porqué de un precio no es una columna: es un evento con lugar y hora. Reificar el estado del mercado, y no esconderlo en un número, es lo que convierte el peor dolor de cabeza de soporte en una sola consulta.La lección del taxi
Lo que el taxi enseñó que el spa ocultaba
El spa fue un cliente amable: una venta que ocurre en un punto del tiempo. El servicio on-demand exigió más, y el modelo lo sostuvo sin un solo parche. Cerremos con el balance de lo que demostró el prototipo al absorber este dominio de punta a punta.
El veredicto del segundo dominio industrial
- Agentes de software (D5). Le dimos poder de decisión
al algoritmo: la plataforma ocupa el rol de
agenteenQ, junto al conductor humano. El modelo no distingue de qué sustancia está hecho quien decide; solo le importa que ejecute el verbo. - Situaciones encadenadas. Seis eventos enganchados por
orden (
sigue_a) y por causa (motivado_por), todos agrupados bajo una unidad comercial (parte_de). El orden vive en la estructura, no en una resta de relojes. - Los porqués del mercado (D7). «La lluvia» y «el alza»
dejaron de ser folclore: se reificaron como un estado en
Oy se ataron al precio concausado_por. La tarifa más reclamada del rubro se explica sola. - Inmutabilidad y vigencia (D6). Una cancelación no borra ni reescribe: agrega un evento y un estado fechado. El grafo se vuelve un registro histórico que responde no solo «¿cómo está hoy?» sino «¿cómo estaba a las 22:42?».
La prueba de código superó a la simulación: no hubo que inventar un rol nuevo ni torcer el catálogo de la arquitectura para sostener una operación que cambia de estado una docena de veces en media hora y multiplica precios con la lluvia. En el próximo capítulo subimos otra vez la apuesta, pero en una dirección distinta. En una historia clínica no hay viajes rápidos ni tarifas que suban con el clima: hay diagnósticos, fármacos, contraindicaciones, protocolos y una densidad de información donde un error no cuesta dinero, sino salud. Veamos cómo el modelo, que ya domó un comercio y una plataforma, se enfrenta al peso de la medicina sin doblarse.