WQuestions

Anexos

32

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.

python
# ── 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.

python
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.

python
@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.

python
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.

python
@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.

Q quién agente — quien actúa vendedor_17 O qué objeto, evento, situación venta_001 L dónde lugar tienda_centro T cuándo tiempo 16:32 N cuánto número, magnitud 49.90 K cuál clase, categoría venta M cómo predicado, conexión — el cable que cose las seis lecturas
Figura 32.1. Los siete ejes. Seis fijan el valor de un hecho (una lectura por eje, a la derecha el ejemplo de venta_001); el séptimo, M, no aloja nodos: es el de los predicados que cosen las seis lecturas en un hecho consultable.
Q quién · agente O qué · objeto / evento L dónde · lugar T cuándo · tiempo N cuánto · número K cuál · clase M cómo · predicado

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:

gol_001O agenteM(O→Q) messiQ

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).

Predicados de rol más usados. La firma indica eje de origen → eje de destino.
PredicadoFirmaConectaEjemplo
agenteM(O→Q)situación → quien actúagol_001 · agente · messi
pacienteM(O→Q)situación → quien la recibediagnostico_771 · paciente · vega
beneficiarioM(O→Q)situación → a favor de quiénasig_001 · beneficiario · luis
temaM(O→O)situación → su objetocanc_001 · tema · viaje_088
instrumentoM(O→O)situación → con quéasig_001 · instrumento · vehiculo_12
lugar_deM(O→L)situación → dóndeventa_001 · lugar_de · tienda_centro
momentoM(O→T)situación → cuándo (puntual)venta_001 · momento · t_16_32
inicio / finM(O→T)situación → ventana temporalsesion_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.

Predicados estructurales: categorización y composición.
PredicadoFirmaSignificaEjemplo
instancia_deM(O→K)esta entidad es un caso de esta claseventa_001 · instancia_de · venta
subclase_deM(K→K)esta clase especializa a otraventa · subclase_de · transaccion
parte_deM(O→O)esta entidad compone a otra mayorescena_42 · parte_de · pelicula_marea
estatus_factualM(O→K)estado de la situaciónviaje_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.

Los cuatro predicados en que se descompone el «por qué» (D7).
PredicadoRespondeApunta aEjemplo
causado_por¿qué lo produjo?causa eficientetarifa elevada → estado de alta demanda
motivado_por¿por qué razón?motivo del agenteprescripción → diagnóstico
con_finalidad¿para qué?fin perseguidoprescripción → objetivo terapéutico
justificado_por¿con qué fundamento?norma o autoridadordenanza → 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.

Predicados que registran la enmienda sin destruir el dato previo.
PredicadoSignificaEjemplo
rectificacorrige un hecho anterior conservándolorediagnóstico → diagnóstico previo
cancelaanula una situación previacancelación → viaje original
verificado_contrase validó frente a una reglaprescripció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.

python
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.

python
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).

python
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.

python
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.

python
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.

python
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)—.

python
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.

python
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:

python
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.

python
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.

python
# 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.

python
# 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.

python
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.

python
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.

python
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.

python
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.

python
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.

tripletas
(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.

json
{
  "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.

json
{
  "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.