Anexos
Anexo: el código
Una hoja de ruta técnica para quien va a implementar. Aquí están, en un solo lugar, las estructuras de datos, el catálogo de ejes y predicados, las cuatro APIs del prototipo y los formatos en que viaja un hecho. Menos relato, más referencia.
A lo largo del libro el código apareció a cuentagotas: una llamada aquí para ilustrar la reificación de una situación, una consulta allá para mostrar la bitemporalidad. Ese goteo es deliberado en el cuerpo del texto (cada fragmento llega cuando la idea que lo justifica está fresca), pero estorba a quien se sienta a programar. Este anexo invierte la prioridad: consolida en un único sitio las piezas técnicas, ordenadas no por el hilo narrativo sino por su función. Es un manual de referencia, no un capítulo para leer de corrido.
La buena noticia, que este anexo deja a la vista mejor que ninguna otra página, es que hay poco que memorizar. El núcleo del modelo cabe en un puñado de estructuras de datos y cuatro funciones. Todo lo demás (los ocho dominios industriales, los cuatro escenarios de estrés, la aplicación que se describe a sí misma) se construye combinando esas pocas piezas. Si solo vas a leer una sección, que sea el catálogo de predicados: ahí está el vocabulario con el que se escribe cualquier hecho.
Sobre los identificadores de este anexo
Los ejemplos usan el repertorio del libro (la venta de Marco,
el gol de Messi, la película de Serra, la ordenanza de micromovilidad, la sesión de IA,
el episodio del paciente Vega) para que reconozcas los casos. Las estructuras
(los campos de un hecho, la forma de un patrón, las firmas de las APIs) son las del
prototipo real, que vive en prototipo/wq/. Cambia los nombres a tu dominio;
la maquinaria no cambia.
Las cuatro APIs del núcleo
Todo el modelado de dominios pasa por cuatro funciones. Dos escriben en el grafo, dos lo interrogan. No hay una quinta función «importante» escondida: con estas se cubren los ocho dominios de la Parte V de punta a punta.
# ── ESCRITURA ──────────────────────────────────────────────────────────────
ingest_situation(u, lex, verbo, *, roles={}, complements=[], extra={}, sit_id=None)
# Reifica una situación en el universo `u`. Resuelve `verbo` contra el
# lexicon `lex`, valida los roles obligatorios de su signatura y asienta
# los hechos atómicos. `complements` desambigua la polisemia; `extra`
# admite metadatos como `estatus_factual`. Devuelve el Individual creado.
u.assert_fact(sujeto, rol, valor, *, valid_from=None, valid_to=None)
# Asienta un hecho atómico directo (una tripleta). `valid_from`/`valid_to`
# activan la bitemporalidad (D6). Nunca sobreescribe: cada llamada agrega.
# ── LECTURA ────────────────────────────────────────────────────────────────
query(u, Pattern(fixed={}, ask={}, type_constraint=None), *, at=None)
# Resuelve un patrón contra el grafo. Devuelve una lista de bindings (un
# dict por coincidencia). `at` proyecta la consulta a un instante de
# tiempo de validez. Las variables a resolver se declaran con `Var()`.
count(u, Pattern(...), *, at=None)
# Igual que `query`, pero devuelve solo la cardinalidad (un entero).
Junto a count conviven agregadores del mismo molde (suma,
promedio) que aplican una función sobre el rol numérico de las coincidencias.
Comparten la firma exacta de count, cambiando solo qué devuelven; por eso no
merecen entrada propia.
Estructuras de datos
El grafo se apoya en cuatro tipos. Conviene leerlos antes que cualquier código de dominio: todo lo demás los manipula.
El eje (Axis) y el individuo (Individual)
Cada nodo del grafo es un Individual anclado a uno de los seis ejes de valor.
El eje no es decorativo: determina qué pregunta responde el nodo y, por tanto, qué
predicados puede recibir. El séptimo eje, M (cómo), no etiqueta nodos: es
el de los cables que los unen.
class Axis(Enum):
Q = "quien" # agente
O = "que" # objeto, evento, situación
L = "donde" # lugar
T = "cuando" # tiempo
N = "cuanto" # número, magnitud
K = "cual" # clase, categoría
# M (cómo) no es eje de nodos: es el eje de los predicados (cables).
@dataclass
class Individual:
id: str # identificador estable y único
axis: Axis # a qué eje pertenece
label: str = "" # rótulo legible (opcional)
El hecho (Fact)
La unidad atómica del modelo es la tripleta tipada (sujeto, rol, valor) (D3).
En el prototipo es un Fact. Los campos valid_from y
valid_to implementan la vigencia (D6): un hecho no se borra ni se edita: se
cierra con un valid_to y se asienta otro que lo reemplaza.
@dataclass
class Fact:
subject: Individual # el nodo del que se predica
role: str # el cable (predicado del eje M)
value: Union[Individual, str, int, float] # nodo o literal
valid_from: datetime = None # inicio de vigencia (D6)
valid_to: datetime = None # fin de vigencia; None = abierto al futuro
Un literal (un número, una cadena) puede ocupar la casilla del valor cuando el destino es
una magnitud o un texto; cuando el valor es otra entidad del grafo, es un
Individual. Esa es la única bifurcación de tipo en toda la estructura.
El patrón de consulta (Pattern y Var)
Una consulta es un patrón con tres ranuras. fixed ata roles a valores
concretos (lo que ya sabes); ask declara con Var() las variables
a resolver (lo que preguntas); type_constraint restringe el tipo del sujeto vía
instancia_de.
class Var:
"""Marca una ranura a resolver dentro de un patrón."""
@dataclass
class Pattern:
fixed: dict = field(default_factory=dict) # roles atados a valores
ask: dict = field(default_factory=dict) # roles con Var() a resolver
type_constraint: Individual = None # filtra por instancia_de
La entrada de lexicon (LexiconEntry)
El lexicon es el compilador que traduce un verbo del lenguaje a la signatura de una
situación (D8). Cada LexiconEntry declara qué tipo de situación produce el
verbo, qué roles son obligatorios y (si el verbo es polisémico) qué complemento la dispara.
@dataclass
class LexiconEntry:
verb: str # el verbo de superficie ("servir", "anotar"…)
situation_type: str # la clase de situación que produce (→ K)
obligatory: list # roles que la signatura exige
pattern: tuple = () # complemento que dispara esta acepción
notes: str = "" # acepción genérica / aclaración
Catálogo de los ejes
Las seis preguntas de valor más el eje de los predicados. Cada hecho fija una posición en este espacio: una lectura por eje, cosida por los cables de M.
venta_001); el
séptimo, M, no aloja nodos: es el de los predicados que cosen las seis
lecturas en un hecho consultable.Catálogo de predicados canónicos
Los predicados son los cables del eje M. Propiedades y relaciones se
unifican: ambas son cables; solo difieren en cardinalidad —funcional (un solo valor) o
múltiple (D2)—. La firma de cada predicado declara de qué eje sale y a cuál llega; la
escribimos M(O→Q): «desde un nodo O hacia un nodo Q». El motor solo valida este
repertorio canónico; cualquier rol que un dominio invente (por la política liberal del
catálogo) se acepta sin declararlo.
Conviene mostrar primero la forma del cable con el componente insignia. El hecho «Messi es
el autor de gol_001» es una tripleta cuyo cable es agente:
Predicados de rol (signatura de las situaciones)
Estos cables conectan una situación (O) con sus participantes. Son los roles que un
LexiconEntry declara obligatorios. El rol agente es contextual: lo
puede ocupar un humano, una organización, un software o un sensor, según el verbo (D5).
| Predicado | Firma | Conecta | Ejemplo |
|---|---|---|---|
agente | M(O→Q) | situación → quien actúa | gol_001 · agente · messi |
paciente | M(O→Q) | situación → quien la recibe | diagnostico_771 · paciente · vega |
beneficiario | M(O→Q) | situación → a favor de quién | asig_001 · beneficiario · luis |
tema | M(O→O) | situación → su objeto | canc_001 · tema · viaje_088 |
instrumento | M(O→O) | situación → con qué | asig_001 · instrumento · vehiculo_12 |
lugar_de | M(O→L) | situación → dónde | venta_001 · lugar_de · tienda_centro |
momento | M(O→T) | situación → cuándo (puntual) | venta_001 · momento · t_16_32 |
inicio / fin | M(O→T) | situación → ventana temporal | sesion_ia_5521 · inicio · t0 |
Predicados estructurales
Cables que arman la jerarquía y la categorización del grafo. instancia_de es el
más importante de todo el modelo: ancla una entidad situada (O) a su plantilla atemporal (K),
y es el que lee type_constraint en una consulta.
| Predicado | Firma | Significa | Ejemplo |
|---|---|---|---|
instancia_de | M(O→K) | esta entidad es un caso de esta clase | venta_001 · instancia_de · venta |
subclase_de | M(K→K) | esta clase especializa a otra | venta · subclase_de · transaccion |
parte_de | M(O→O) | esta entidad compone a otra mayor | escena_42 · parte_de · pelicula_marea |
estatus_factual | M(O→K) | estado de la situación | viaje_088 · estatus_factual · cancelado |
Predicados del «por qué» (D7)
No hay un eje «por qué». El porqué se reparte en cuatro cables distintos, y la distinción no es cosmética: confundir la causa con la finalidad es un error de modelado. Las cuatro pueden coexistir sobre la misma situación sin pisarse.
| Predicado | Responde | Apunta a | Ejemplo |
|---|---|---|---|
causado_por | ¿qué lo produjo? | causa eficiente | tarifa elevada → estado de alta demanda |
motivado_por | ¿por qué razón? | motivo del agente | prescripción → diagnóstico |
con_finalidad | ¿para qué? | fin perseguido | prescripción → objetivo terapéutico |
justificado_por | ¿con qué fundamento? | norma o autoridad | ordenanza → competencia municipal |
Predicados de trazabilidad
Como el modelo nunca borra, necesita cables que registren la enmienda: cuál hecho reemplaza a cuál. Son la firma de la inmutabilidad (D6) en el grafo.
| Predicado | Significa | Ejemplo |
|---|---|---|
rectifica | corrige un hecho anterior conservándolo | rediagnóstico → diagnóstico previo |
cancela | anula una situación previa | cancelación → viaje original |
verificado_contra | se validó frente a una regla | prescripción → regla de contraindicación |
La política liberal del catálogo
Este catálogo es canónico, no cerrado. El motor
valida estrictamente los predicados de arriba, pero deja pasar cualquier rol de dominio que
un negocio necesite: medicamento_prescrito, medida_de,
diagnosticado_como, equipo, frecuencia,
duracion, monto… Ninguno está declarado en el núcleo y todos
funcionan. Es la decisión que mantiene el motor pequeño mientras el lexicon crece.
Recetario por dominio
Lo que sigue es el código publicado en la Parte V, consolidado y agrupado por la maniobra que ilustra. Sirve como banco de patrones: cuando te topes con un caso parecido en tu dominio, copia el molde y cambia los identificadores.
Reificar una situación con polisemia
Vender una camiseta es una situación con vendedor, cliente, lugar y momento. El
parámetro complements resuelve la ambigüedad del verbo: «vender» puede ser
vender un producto o «venderse» (delatarse); el complemento lo decide.
vta = ingest_situation(u, lex, "vender",
roles={
"agente": vendedor_17,
"lugar_de": tienda_centro,
"momento": t_16_32,
},
complements=["camiseta"], # dispara la acepción correcta
extra={"estatus_factual": cerrada},
sit_id="venta_001",
)
La desambiguación se sostiene en el lexicon: tres acepciones del mismo verbo, cada una con
su pattern disparador y su situation_type. El parser elige de la
más específica a la más general.
lex.register(LexiconEntry(
verb="vender",
situation_type="transaccion_venta",
obligatory=["agente", "objeto"],
pattern=("camiseta",), # dispara con el complemento "camiseta"
))
lex.register(LexiconEntry(
verb="vender",
situation_type="accion_delatar",
obligatory=["agente", "paciente"],
pattern=("se",), # locución idiomática «venderse»
))
lex.register(LexiconEntry(
verb="vender",
situation_type="accion_vender",
obligatory=["agente", "tema"],
notes="genérico — ceder a cambio de un precio",
))
El dialecto de dominio: hablar el idioma del negocio
Un dialecto mapea el vocabulario del comercio a los roles canónicos. El usuario dice
«vendedor», «venta», «artículo»; el sistema traduce a agente,
transaccion_venta, objeto sin que se entere (D9).
lex.register_domain_dialect("tienda_aurora", {
"vendedor": "agente",
"venta": "transaccion_venta",
"articulo": "objeto",
"fidelidad": "beneficio_fidelidad",
})
Consultar: patrón fijo + restricción de tipo
«¿Cuántas camisetas vendió Marco hoy?» es un conteo con sujeto fijo y restricción de tipo.
El patrón ata agente y estatus_factual, y restringe a la clase
transaccion_venta.
n = count(u, Pattern(
fixed={"agente": vendedor_17, "estatus_factual": cerrada},
type_constraint=u.ind("transaccion_venta"),
))
# n == 8
Y una decisión de negocio («a la séptima, una gratis») no es código del motor: es Python que consume el resultado de la consulta. La regla vive en la aplicación, no en el grafo.
califica = n >= 7
# True
Consulta bitemporal: la misma pregunta, distinto instante (D6)
La misma consulta sobre el mismo individuo devuelve respuestas distintas según el
at= que recibe. La directora Serra vivió en ciudad_a hasta 2025 y
luego se mudó a ciudad_b; el grafo recuerda ambas residencias.
res_2022 = query(u, Pattern(
fixed={"agente": serra},
ask={"lugar_de": Var()},
type_constraint=u.ind("residencia"),
), at=datetime(2022, 6, 1, tzinfo=timezone.utc))
# res_2022[0]["lugar_de"].id == "ciudad_a"
res_2027 = query(u, ..., at=datetime(2027, 6, 1, tzinfo=timezone.utc))
# res_2027[0]["lugar_de"].id == "ciudad_b"
Software como agente (D5)
El rol agente no exige un humano. Al asignar un viaje, el agente es la
app: cuatro participantes con roles distintos —app (agente, Q), solicitud previa
(tema, O), conductor (beneficiario, Q), vehículo (instrumento, O)—.
asig = ingest_situation(u, lex, "asignar", roles={
"agente": app, # ¡la APP ocupa el rol Q!
"tema": sol, # asigna la solicitud previa
"beneficiario": luis, # al conductor disponible
"instrumento": vehiculo_12, # con su vehículo
"momento": at(1),
}, sit_id="asig_001")
Causalidad emergente: explicar un valor
Una tarifa elevada no se afirma porque sí: queda explicada por causado_por
apuntando a un estado de alta demanda, que a su vez se reifica con su lugar y su momento.
Recuperar la explicación es un salto del grafo.
estado_demanda = u.add_individual(Individual(
id="alta_demanda_2026_05_16_14_30", axis=Axis.O,
label="alta demanda 16/5 14:30"))
u.assert_fact(estado_demanda, "instancia_de", alta_demanda)
u.assert_fact(estado_demanda, "lugar_de", plaza_mayor)
u.assert_fact(estado_demanda, "momento", at(0))
tarifa = u.add_individual(Individual(id="tarifa_viaje_088", axis=Axis.O))
u.assert_fact(tarifa, "instancia_de", category("tarifa"))
u.assert_fact(tarifa, "monto", n_25_usd)
u.assert_fact(tarifa, "causado_por", estado_demanda)
Y la consulta inversa («¿por qué costó tanto?») recorre los hechos de la tarifa buscando el
cable causado_por:
explicaciones = u.facts_about(tarifa)
causa = [f for f in explicaciones if f.role == "causado_por"]
# causa[0].value.id == "alta_demanda_2026_05_16_14_30"
Nunca borramos: la cancelación como hecho nuevo
Cancelar no destruye nada. Es una situación nueva que cancela la previa y le
pone estatus_factual: cancelado. El historial sobrevive intacto.
canc = ingest_situation(u, lex, "cancelar", roles={
"agente": valeria,
"tema": viaje_088,
"momento": at(62),
}, sit_id="canc_001")
u.assert_fact(canc, "cancela", viaje_088)
u.assert_fact(viaje_088, "estatus_factual", cancelado)
Rediagnóstico con vigencia y trazabilidad
Un diagnóstico se reemplaza por otro vía bitemporalidad. El primero es válido entre la
consulta y el rediagnóstico; el segundo, desde ahí en adelante (sin valid_to).
La relación rectifica deja el rastro del cambio.
# Diagnóstico original (vigente hasta el rediagnóstico)
u.assert_fact(diag, "diagnosticado_como", hta_g1,
valid_from=t_consulta, valid_to=t_redx)
# Rediagnóstico (vigente desde t_redx, abierto al futuro)
u.assert_fact(diag2, "diagnosticado_como", hta_g2,
valid_from=t_redx)
u.assert_fact(diag2, "rectifica", diag)
Roles de dominio que apuntan a K
«El medicamento prescrito» no es un objeto físico individual (O), sino una categoría
farmacológica (K). El catálogo declara tema: O→O, así que el dominio agrega su
propio rol medicamento_prescrito: O→K gracias a la política liberal. Lo mismo
con medida_de.
# El medicamento prescrito es una categoría (K), no un objeto.
u.assert_fact(pres, "medicamento_prescrito", enalapril)
# La variable medida también es una categoría.
u.assert_fact(medicion, "medida_de", presion_arterial)
El «por qué» en triple: motivo, finalidad y verificación
Tres cables del porqué (D7) sobre una misma prescripción, sin pisarse:
motivado_por apunta al diagnóstico, con_finalidad al objetivo
terapéutico, verificado_contra a la regla que confirma que no contradice una
contraindicación.
pres = ingest_situation(u, lex, "prescribir", roles={
"agente": dra_torres,
"paciente": vega,
"medicamento_prescrito": enalapril,
"frecuencia": cada_manana,
"duracion": indefinida,
"momento": at(0),
})
u.assert_fact(pres, "motivado_por", diag) # por qué se prescribió
u.assert_fact(pres, "con_finalidad", objetivo) # para qué se prescribió
u.assert_fact(pres, "verificado_contra", regla_contraindicacion)
Recuperar una cadena por composición
Un partido completo es una entidad superior que contiene sus jugadas vía
parte_de. Filtrar por ese cable devuelve toda la cadena de una sola pasada.
partes = [f.subject for f in u.facts_with_role("parte_de")
if f.value.id == "partido_arg_per_2026"]
# devuelve todas las jugadas registradas del partido
Estado derivado: no se asienta, se calcula
El marcador de un partido no es un hecho que alguien escriba: emerge de contar los goles de cada equipo. La consulta es un doble conteo con una composición final.
goles_argentina = count(u, Pattern(
fixed={"equipo": argentina},
type_constraint=u.ind("gol_jugada_abierta"),
))
goles_peru = count(u, Pattern(
fixed={"equipo": peru},
type_constraint=u.ind("gol_jugada_abierta"),
))
marcador = f"{goles_argentina} - {goles_peru}"
Un LLM ingiere una nota en lenguaje natural
Como el lexicon expone las signaturas de los verbos como function schemas, un
modelo de lenguaje puede leer una nota clínica y producir una invocación
ingest_situation por verbo identificado. La extracción es directa: el modelo no
inventa el esquema, lo lee del lexicon.
ingest_situation("consultar", roles={
"agente": dra_torres, "paciente": vega,
"momento": fecha_nota, "motivo": cefalea,
})
ingest_situation("medir", roles={
"agente": dra_torres, "paciente": vega,
"medida_de": presion_arterial,
"monto": "145/92", "unidad": mmHg,
})
ingest_situation("diagnosticar", roles={
"agente": dra_torres, "paciente": vega,
"diagnosticado_como": hta_g1,
})
ingest_situation("prescribir", roles={
"agente": dra_torres, "paciente": vega,
"medicamento_prescrito": enalapril,
"frecuencia": cada_manana,
})
ingest_situation("controlar", roles={
"paciente": vega, "agente": dra_torres,
"momento": fecha_nota_mas_30,
"estatus_factual": previsto,
})
Una consulta que cruza dominios
El mismo grafo permite cruzar territorios que en sistemas tradicionales jamás se hablarían. Aquí, dos consultas paralelas —una a un dominio público (una ordenanza), otra a uno de telemetría (sesiones de IA)— que la aplicación combina en una respuesta única.
normas = query(u, Pattern(
fixed={"tema_categorico": micromovilidad},
type_constraint=u.ind("ordenanza_municipal"),
ask={"agente": Var(), "momento": Var(), "entra_en_vigor": Var()},
))
uso = query(u, Pattern(
fixed={"modelo": modelo_lumen_2026},
type_constraint=u.ind("sesion_ia"),
ask={"momento": Var(), "tokens_salida": Var()},
))
Formatos: cómo viaja un hecho
El mismo hecho se puede mirar en tres representaciones. Todas dicen lo mismo; cambian la audiencia. La tripleta es para leer; el JSON es para transmitir; el function schema es para que un LLM lo invoque.
Forma tripleta (para leer)
La notación canónica del libro. Cada línea es un hecho atómico; el comentario tras
∈ recuerda la firma del cable. Esta forma es la que aparece en las figuras y la
que conviene tener en la cabeza al modelar.
(venta_001) ∈ O # la situación
(venta_001, instancia_de, venta) ∈ M(O, K)
(venta_001, agente, vendedor_17) ∈ M(O, Q)
(venta_001, objeto, camiseta_88) ∈ M(O, O)
(venta_001, lugar_de, tienda_centro) ∈ M(O, L)
(venta_001, momento, 16:32) ∈ M(O, T)
(venta_001, monto_usd, 49.90) ∈ M(O, N) # USD
(venta_001, estatus_factual, cerrada) ∈ M(O, K)
Forma JSON (para transmitir)
La serialización por la que viaja un hecho entre sistemas. Cada hecho lleva su sujeto, su
rol y su valor; el valor distingue entre referencia a otro nodo (ref) y literal
(lit) con su eje. La bitemporalidad va en valido.
{
"sujeto": "venta_001",
"eje_sujeto": "O",
"hechos": [
{"rol": "instancia_de", "valor": {"ref": "venta", "eje": "K"}},
{"rol": "agente", "valor": {"ref": "vendedor_17", "eje": "Q"}},
{"rol": "objeto", "valor": {"ref": "camiseta_88", "eje": "O"}},
{"rol": "lugar_de", "valor": {"ref": "tienda_centro", "eje": "L"}},
{"rol": "momento", "valor": {"lit": "2026-05-14T16:32:00-05:00", "eje": "T"}},
{"rol": "monto_usd", "valor": {"lit": 49.90, "eje": "N", "unidad": "Currency:USD"}}
],
"valido": {"desde": "2026-05-14T16:32:00-05:00", "hasta": null}
}
Forma function schema (para que un LLM lo invoque)
El lexicon proyecta cada verbo como una herramienta que un modelo de lenguaje puede llamar. Los roles obligatorios de la signatura se vuelven parámetros requeridos; los opcionales, opcionales. Así el LLM no improvisa el esquema: lo recibe.
{
"name": "ingest_vender_camiseta",
"description": "Registra que un vendedor vendió una camiseta a un cliente.",
"parameters": {
"type": "object",
"properties": {
"agente": {"type": "string", "description": "quién la vendió (Q)"},
"objeto": {"type": "string", "description": "qué se vendió (O)"},
"lugar_de": {"type": "string", "description": "dónde se vendió (L)"},
"momento": {"type": "string", "format": "date-time"},
"monto_usd": {"type": "number", "description": "importe en dólares (N)"}
},
"required": ["agente", "objeto"]
}
}
Lo que se ve al mirarlo todo junto
Reunir el código disperso revela patrones que el flujo capítulo a capítulo diluye. Cinco regularidades valen la pena nombrar, porque explican por qué el modelo escala sin engordar.
1 La API es diminuta
Cuatro funciones (ingest_situation,
assert_fact, query, count) modelan ocho dominios
completos. No hay doscientas funciones que aprender; hay cuatro.
2 Las llamadas son uniformes
El patrón visual es siempre el mismo: verbo, diccionario de roles, opcionales al final. Una vez que reconoces la silueta, el código se lee como prosa.
3 El verbo va primero
Se elige el verbo y después los roles, no al revés. Refleja la signatura tipada del verbo: primero el contrato, después los argumentos.
4 La política liberal trabaja sola
Roles que ningún capítulo declara
(medicamento_prescrito, equipo, frecuencia) el
motor los acepta. Valida lo canónico; deja pasar lo de dominio.
5 Inmutabilidad y bitemporalidad por defecto
Casi todo fragmento importante asienta con
valid_from/valid_to, marca estatus_factual o
rectifica sin borrar. El modelo no tiene una operación «update»: tiene
«asentar un hecho nuevo que reemplaza al previo conservando el historial».
El motor no crece con cada dominio nuevo; el lexicon y los datos sí. Esa es la propiedad que mantiene corto el núcleo a pesar de cubrir un spa, un banco, una historia clínica y un partido con la misma maquinaria.La economía del modelo
Esas cinco regularidades, juntas, explican por qué el núcleo de modelado cabe en poco más de dos mil líneas a pesar de la diversidad de territorios que cubre. Y anticipan la prueba más exigente, la del capítulo 28: una aplicación con menús, formularios y entidades enteras que se levantó sobre este mismo núcleo casi sin agregarle código, porque también su estructura y su conducta son, al final, datos. El siguiente anexo abre esa caja y muestra el prototipo por dentro.