Los datos sin contexto son solo numeros. Saber que un producto cuesta quince dolares es util. Saber que cuesta quince dolares, que esta en la version doce, que fue actualizado por ultima vez hace tres dias y que nunca ha sido eliminado -- eso es una pista de auditoria.
La sesion 081 implemento el acceso a metadatos de version para las entidades de FLIN: la capacidad de leer .id, .version_number, .created_at, .updated_at y .deleted_at directamente desde cualquier instancia de entidad, incluyendo versiones historicas devueltas por .history. La implementacion tomo treinta minutos y agrego sesenta y tres lineas de codigo. El impacto fue desproporcionado: transformo el modelo temporal de FLIN de "almacenamos versiones" a "proporcionamos transparencia completa del ciclo de vida."
Que metadatos estan disponibles
Cada instancia de entidad en FLIN porta cinco campos de metadatos, poblados automaticamente por el runtime:
flinuser = User.find(id)
user.id // Unique entity identifier (integer)
user.version_number // Sequential version counter (integer)
user.created_at // Timestamp of initial creation (integer, ms)
user.updated_at // Timestamp of last modification (integer, ms)
user.deleted_at // Timestamp of soft deletion (optional integer)Estos no son campos definidos por el usuario. Existen en cada entidad independientemente de su esquema. Una entidad con un solo campo name: text tiene seis propiedades accesibles: name, id, version_number, created_at, updated_at y deleted_at.
El campo deleted_at es especial: siempre es opcional. Para entidades activas, devuelve none. Para entidades eliminadas suavemente, devuelve la marca de tiempo de eliminacion. Esta distincion esta incorporada en el sistema de tipos -- deleted_at tiene tipo Optional<Int> incluso cuando se accede en una referencia de entidad no opcional.
Lo que habia antes: los metadatos invisibles
Antes de la sesion 081, cada entidad en FLIN ya portaba metadatos -- la estructura EntityInstance tenia campos id, version, created_at, updated_at y deleted_at desde las primeras sesiones. La VM usaba estos campos internamente para operaciones temporales: el operador @ verificaba numeros de version, la operacion de guardado incrementaba marcas de tiempo y la eliminacion suave establecian deleted_at.
Pero los desarrolladores no podian acceder a nada de eso. Los metadatos eran invisibles. Si queria mostrar un numero de version en una plantilla, no podia. Si queria mostrar cuando fue actualizada una entidad por ultima vez, no podia. Los datos estaban ahi, dentro del runtime, pero no habia sintaxis para alcanzarlos.
Este es un patron comun en el desarrollo de lenguajes: estado interno que el runtime necesita pero que el desarrollador no puede tocar. La sesion 081 derribo esa pared.
La implementacion: extendiendo GetField
FLIN tiene dos opcodes para acceso a campos: GetField (estatico, cuando el nombre de la propiedad se conoce en tiempo de compilacion) y GetFieldDyn (dinamico, para nombres de propiedades calculados). Ambos necesitaban extenderse para reconocer campos de metadatos.
El cambio fue quirurgico. Antes de verificar campos definidos por el usuario, la VM ahora verifica nombres de campos de metadatos:
rustObjectData::Entity(e) => {
match name.as_str() {
"id" => Value::Int(e.id as i64),
"version" | "version_number" => Value::Int(e.version as i64),
"created_at" => Value::Int(e.created_at),
"updated_at" => Value::Int(e.updated_at),
"deleted_at" => e.deleted_at.map(Value::Int).unwrap_or(Value::None),
_ => e.fields.get(&name).cloned().unwrap_or(Value::None),
}
}Este codigo aparece en ambos manejadores GetField y GetFieldDyn -- una duplicacion que es necesaria por como la VM despacha estos opcodes, pero la logica es identica.
La decision de precedencia
Los campos de metadatos tienen precedencia sobre los campos definidos por el usuario. Si un desarrollador crea una entidad con un campo llamado id, el metadato integrado id gana:
flinentity Problematic {
id: text // User-defined 'id' field
name: text
}
item = Problematic { id: "custom-id", name: "test" }
save item
print(item.id) // Returns the entity ID (integer), NOT "custom-id"Esta fue una decision de diseno deliberada. Los metadatos del sistema deben ser accesibles de manera confiable. Si los campos definidos por el usuario pudieran ocultar los metadatos, los desarrolladores no tendrian forma de acceder al ID real de la entidad, la version o las marcas de tiempo. El compromiso -- que los nombres de campo id, version, version_number, created_at, updated_at y deleted_at estan efectivamente reservados -- es aceptable porque estos nombres son precisamente los que los desarrolladores querrian para metadatos de todos modos.
Verificacion de tipos
El verificador de tipos fue extendido para reconocer campos de metadatos en tipos de entidad:
rustFlinType::Entity(entity_name) => {
match property {
"id" | "version" | "version_number"
| "created_at" | "updated_at" => {
return Ok(if optional {
FlinType::Optional(Box::new(FlinType::Int))
} else {
FlinType::Int
});
}
"deleted_at" => {
return Ok(FlinType::Optional(Box::new(FlinType::Int)));
}
_ => {} // Fall through to user-defined field checking
}
}Dos matices en esta logica de verificacion de tipos:
Todos los campos de metadatos devuelven enteros. Las marcas de tiempo se almacenan como milisegundos desde epoch (i64). Los numeros de version e IDs tambien son enteros. Esta simplicidad significa que la aritmetica temporal funciona naturalmente: user.updated_at + 7.days es simplemente adicion de enteros.
deleted_at siempre es opcional. Incluso cuando se accede en una referencia de entidad no opcional, deleted_at devuelve Optional<Int>. Esto es porque la mayoria de las entidades no estan eliminadas, asi que el campo es None por defecto. El sistema de tipos refleja esta realidad -- debe manejar el caso None:
flin{if user.deleted_at}
<p>Deleted at: {user.deleted_at}</p>
{else}
<p>Active</p>
{/if}Metadatos en versiones historicas
El verdadero poder del acceso a metadatos emerge cuando se combina con .history. Cada version en la lista de historial porta sus propios metadatos, reflejando el estado en el momento en que esa version fue creada:
flin{for ver in product.history}
<div class="audit-entry">
<p>Version #{ver.version_number}</p>
<p>Entity ID: {ver.id}</p>
<p>Created at: {ver.created_at}</p>
<p>Updated at: {ver.updated_at}</p>
<p>Price: ${ver.price}</p>
</div>
{/for}Este bucle renderiza una pista de auditoria completa sin codigo adicional. Cada ver en el bucle es una instancia de entidad completa, reconstruida desde la estructura EntityVersion almacenada en el historial de versiones. La reconstruccion puebla los metadatos desde los valores almacenados de la version:
rust// EntityInstance structure
pub struct EntityInstance {
pub entity_type: String,
pub id: u64, // Accessible via .id
pub fields: HashMap<String, Value>,
pub version: u64, // Accessible via .version_number
pub created_at: i64, // Accessible via .created_at
pub updated_at: i64, // Accessible via .updated_at
pub deleted_at: Option<i64>, // Accessible via .deleted_at
}Cuando la VM construye una entidad historica desde un EntityVersion, mapea el timestamp de la version a created_at y updated_at, y el numero de version de la version al campo version. El resultado es que las entidades historicas se comportan identicamente a las entidades actuales -- la misma sintaxis de acceso a campos funciona para ambas.
Construyendo pistas de auditoria
Antes de la sesion 081, podia acceder al historial de entidades:
flinhistory = product.history
count = history.countDespues de la sesion 081, podia construir pistas de auditoria completas:
flin<div class="audit-log">
<h2>Document History</h2>
<table>
<thead>
<tr>
<th>Version</th>
<th>Date</th>
<th>Title</th>
<th>Status</th>
</tr>
</thead>
<tbody>
{for ver in document.history}
<tr>
<td>v{ver.version_number}</td>
<td>{ver.updated_at}</td>
<td>{ver.title}</td>
<td>{ver.status}</td>
</tr>
{/for}
</tbody>
</table>
</div>En un framework web tradicional, esta pista de auditoria requeriria:
- Una tabla separada
document_historycon claves foraneas. - Triggers de base de datos o middleware de aplicacion para poblarla en cada cambio.
- Un endpoint de API dedicado para consultarla.
- Un componente de frontend para mostrarla.
- Scripts de migracion para configurarla.
- Pruebas para verificar el comportamiento del trigger/middleware.
En FLIN, es un bucle {for} sobre .history con acceso a metadatos. La infraestructura no existe como codigo separado -- es el runtime del lenguaje en si.
Patrones de aplicacion del mundo real
Actualizaciones de productos de e-commerce
flin<div class="product-info">
<h1>{product.name}</h1>
<p class="price">${product.price}</p>
<p class="meta">
Version {product.version_number}
-- Last updated {product.updated_at}
</p>
</div>Los clientes pueden ver "Este producto fue actualizado por ultima vez hace 3 dias (version 12)" sin ningun codigo de seguimiento adicional.
Cambios en el perfil de usuario
flin<div class="profile-history">
<h3>Profile Change History</h3>
{for ver in user.history.last(5)}
<div class="change-entry">
<span class="version">v{ver.version_number}</span>
<span class="date">{ver.updated_at}</span>
<span class="name">{ver.name}</span>
<span class="email">{ver.email}</span>
</div>
{/for}
</div>Informes de cumplimiento
flin// Generate compliance report data
all_changes = document.history
.where_field("created_at", ">", last_month)
.order_by("created_at", "desc")
<div class="compliance-report">
<h2>Changes in Last 30 Days</h2>
<p>Total versions: {all_changes.count}</p>
{for change in all_changes}
<div class="report-entry">
<p>Version {change.version_number} at {change.created_at}</p>
<p>Status: {change.status}</p>
</div>
{/for}
</div>La estructura de datos subyacente
Comprender por que el acceso a metadatos es eficiente requiere comprender como se almacenan las entidades. La estructura EntityInstance porta metadatos junto con los campos definidos por el usuario:
rustpub struct EntityInstance {
pub entity_type: String,
pub id: u64,
pub fields: HashMap<String, Value>,
pub version: u64,
pub created_at: i64,
pub updated_at: i64,
pub deleted_at: Option<i64>,
}El HashMap fields contiene datos definidos por el usuario (name, price, email). Los metadatos viven fuera del HashMap como campos directos de la estructura. Esto significa que el acceso a metadatos es una lectura directa de campo -- O(1) sin busqueda de hash -- mientras que el acceso a campos definidos por el usuario requiere una busqueda en el HashMap.
Las versiones historicas usan una estructura mas compacta:
rustpub struct EntityVersion {
pub version: u64,
pub timestamp: i64,
pub fields: HashMap<String, Value>,
}Cuando la VM reconstruye una entidad historica para la iteracion de .history, mapea los campos de EntityVersion a un EntityInstance, poblando los metadatos desde los valores almacenados de la version. Esta reconstruccion ocurre una vez por acceso a la version y produce una entidad completa que soporta la misma sintaxis de acceso a campos que una entidad actual.
El alias: .version vs .version_number
Un detalle pequeno pero amigable para el usuario: el numero de version es accesible a traves de dos nombres. Tanto entity.version como entity.version_number devuelven el mismo valor. La forma corta es conveniente para acceso rapido. La forma larga es mas explicita y se lee mejor en plantillas donde la claridad importa.
flin// Both are equivalent
product.version // 5
product.version_number // 5Esto se implemento como un simple pattern match en el manejador de acceso a campos de la VM -- la rama "version" | "version_number" maneja ambos nombres y devuelve el mismo campo e.version. El verificador de tipos acepta de manera similar ambos nombres con tipos de retorno identicos.
El enfoque de doble nombre sigue un principio que aplicamos a lo largo de FLIN: cuando dos nombres son igualmente intuitivos y no hay ambiguedad, soportar ambos. Los desarrolladores no deberian necesitar recordar si es .version o .version_number -- pueden usar el que se sienta natural en contexto.
Impacto en el progreso
La sesion 081 completo tres tareas: - TEMP4-13: Acceder a version.id - TEMP4-14: Acceder a version.created_at - TEMP4-15: Acceder a version.version_number
TEMP-4 paso de dieciseis de veintidos (setenta y tres por ciento) a diecinueve de veintidos (ochenta y seis por ciento). Progreso temporal general: ciento dos de ciento sesenta (sesenta y tres punto ocho por ciento).
La implementacion fue pequena -- sesenta y tres lineas netas de codigo en tres archivos. Pero la funcionalidad que habilito -- transparencia completa del ciclo de vida para cada entidad en cada aplicacion FLIN -- es fundamental. Pistas de auditoria, informes de cumplimiento, visualizaciones de version y registros de actividad se vuelven triviales una vez que los metadatos son una propiedad de primera clase.
Por que los metadatos no son solo "algo agradable de tener"
En cada aplicacion que hemos construido en ZeroSuite, el acceso a metadatos ha sido un requisito, no una funcionalidad. Considere los patrones:
Tickets de soporte. Cuando un usuario reporta "mis datos se ven mal," el equipo de soporte necesita saber: Que version es esta? Cuando fue actualizada por ultima vez? Ha sido eliminada y restaurada? Sin metadatos, responder estas preguntas requiere acceso a la base de datos y consultas SQL. Con los metadatos de FLIN, el equipo de soporte puede ver la respuesta directamente en la UI de la aplicacion.
Concurrencia. Cuando dos usuarios editan la misma entidad, el numero de version habilita el control de concurrencia optimista. Antes de guardar, la aplicacion puede verificar si el numero de version ha cambiado desde que la entidad fue cargada. Si ha cambiado, otro usuario hizo un cambio y el guardado puede ser rechazado o fusionado.
flin// Optimistic concurrency pattern
loaded_version = product.version_number
// ... user makes changes ...
current_version = Product.find(product.id).version_number
{if loaded_version != current_version}
<p>This record was modified by another user. Please refresh.</p>
{/if}Cache. La marca de tiempo updated_at habilita la invalidacion de cache. Un cliente puede almacenar los datos de un producto junto con su valor updated_at y solo volver a buscar cuando el updated_at del servidor es mas reciente.
Feeds de actividad. Combinar .history con metadatos crea feeds de actividad sin una tabla de eventos separada:
flin// Recent activity across all documents
{for doc in Document.all}
{if doc.updated_at > last_week}
<div class="activity-item">
<p>{doc.title} updated (v{doc.version_number})</p>
<span class="date">{doc.updated_at}</span>
</div>
{/if}
{/for}Ninguno de estos patrones requiere infraestructura adicional en FLIN. Los metadatos siempre estan ahi, siempre son precisos y siempre son accesibles a traves de la misma sintaxis de propiedades usada para campos definidos por el usuario.
Treinta minutos de implementacion. Cero opcodes nuevos. Cero conceptos de runtime nuevos. Solo exponer lo que ya estaba ahi, hacer lo implicito explicito y dejar que los desarrolladores lo usen.
Esta es la Parte 7 de la serie "Como construimos FLIN" sobre el modelo temporal, documentando el sistema de metadatos de version que habilita pistas de auditoria sin configuracion.
Navegacion de la serie: - [046] Every Entity Remembers Everything: The Temporal Model - [047] Version History and Time Travel Queries - [048] Temporal Integration: From Bugs to 100% Test Coverage - [049] Destroy and Restore: Soft Deletes Done Right - [050] Temporal Filtering and Ordering - [051] Temporal Comparison Helpers - [052] Version Metadata Access (usted esta aqui) - [053] Time Arithmetic: Adding Days, Comparing Dates - [054] Tracking Accuracy and Validation - [055] The Temporal Model Complete: What No Other Language Has