Carmen LabsCarmen Labs
MView Experimental · AI first

Entrada operativa para agentes que crean MViews.

Esta especificación es experimental y puede cambiar mientras se valida con implementaciones reales.

MView Experimental

Especificación autocontenida para crear, ejecutar y refrescar vistas persistentes sobre fuentes de solo lectura.

MView Experimental

Estado

Especificación activa de MView. El schema (/experimental/schema.json), esta página y el catálogo de ejemplos (/experimental/examples) deben coincidir exactamente entre sí; cualquier discrepancia es un defecto de la especificación, no una libertad de implementación. El registro de decisiones de diseño está en el roadmap.

Instrucciones para el modelo

Trata esta página como la especificación completa para crear una MView ahora. Usa exclusivamente los campos y convenciones definidos aquí.

Cuando el usuario solicite una MView:

  1. Inspecciona la fuente real. Nunca inventes columnas, claves, archivos o contenido.
  2. Conserva la fuente sin cambios.
  3. Elige un Source Adapter de esta especificación.
  4. Crea un directorio terminado en .mview junto a la fuente o dentro de .mviews/.
  5. Crea mview.json, un transform y un render para una vista inicial.
  6. Usa rutas relativas y valida el manifiesto con https://carmenlabs.com/experimental/schema.json.
  7. Ejecuta adapter y transform sobre la fuente real.
  8. Prueba el refresh modificando solamente una copia temporal de la fuente.
  9. Informa los archivos creados y las validaciones ejecutadas.

No escribas un parser de CSV, JSON, Markdown o texto si existe un adapter de referencia para ese formato. No entregues solamente HTML: una MView necesita manifiesto, transform y render.

Cada patrón del catálogo de ejemplos está además publicado como paquete completo y descargable bajo /experimental/examples/<nombre>/ — manifiesto, transform y render reales, no solo fragmentos. Bajalo y usalo como punto de partida en vez de escribir el manifiesto y la estructura de carpetas desde cero; solo cambian identidad, campos reales de la fuente y validaciones. Esto no aplica al render: adaptá la presentación al pedido del usuario, no heredes el HTML/CSS del ejemplo tal cual.

Definición

Una MView es un paquete persistente que conecta una fuente declarada y de solo lectura con una o más vistas reejecutables.

fuente original de solo lectura
            |
            v
      Source Adapter
            |
            v
    entrada normalizada
       /          \
      v            v
transform A    transform B
      |            |
      v            v
  render A      render B
      |            |
      v            v
   vista A       vista B

En cada refresh, el runtime vuelve a leer los bytes actuales, normaliza la fuente una vez y ejecuta cada vista de forma independiente. Una representación normalizada puede cachearse temporalmente, pero nunca sustituye a la fuente original.

Archivos mínimos

sales-dashboard.mview/
├── mview.json
└── views/
    └── summary/
        ├── transform.mjs
        └── view.html

El paquete no incluye adapters comunes. El runtime los registra y los reutiliza para todas las MViews.

Manifiesto

Todo mview.json debe validar contra:

https://carmenlabs.com/experimental/schema.json

Forma mínima:

{
  "$schema": "https://carmenlabs.com/experimental/schema.json",
  "id": "sales-dashboard",
  "name": "Sales Dashboard",
  "version": "1.0.0",
  "source": {
    "path": "../sales.csv",
    "adapter": "csv-table",
    "input": "table"
  },
  "views": [
    {
      "id": "summary",
      "name": "Summary",
      "prompt": "Show monthly sales and top clients.",
      "renderer": "html",
      "entry": {
        "transform": "./views/summary/transform.mjs",
        "render": "./views/summary/view.html"
      }
    }
  ]
}

Campos del paquete

CampoRequeridoContrato
$schemaExactamente https://carmenlabs.com/experimental/schema.json.
idIdentificador estable: minúsculas, números, ., _ y -.
nameNombre visible del paquete.
versionVersión semántica del paquete. Cambia al modificar artefactos persistidos.
sourceFuente de archivo o selección explícita de directorio.
viewsArray no vacío de vistas; sus id deben ser únicos.
descriptionnoDescripción humana.
settingsnoDefaults compartidos por todas las vistas.
metadatanoProvenance informativa; no controla ejecución ni renderizado.

Fuente de archivo

"source": {
  "path": "../sales.csv",
  "adapter": "csv-table",
  "input": "table",
  "options": {}
}
  • path es relativo al directorio .mview y apunta a la fuente autorizada.
  • adapter identifica un adapter registrado.
  • input declara la forma normalizada que ese adapter debe producir.
  • options configura explícitamente al adapter y participa en la clave de cache.
  • contentHash (opcional) es sha256: seguido del hash SHA-256 de los bytes originales del archivo. Permite detectar drift entre la fuente y el momento en que la MView fue creada o validada por última vez, y ofrecer regeneración cuando no coincide. Mismo formato que en la fuente de directorio.

Fuente de directorio

"source": {
  "kind": "directory",
  "path": "../reports",
  "input": "filesystem",
  "files": [
    {
      "path": "january.csv",
      "adapter": "csv-table",
      "input": "table",
      "contentHash": "sha256:111..."
    },
    {
      "path": "notes.md",
      "adapter": "markdown-text",
      "input": "markdown",
      "contentHash": "sha256:222..."
    }
  ],
  "contentHash": "sha256:333..."
}

Un directorio no concede acceso general al filesystem. El runtime lee únicamente source.files; no recorre el directorio ni incorpora archivos nuevos, ni siquiera por extensión. source.files es obligatorio y no vacío para toda fuente de directorio — no hay forma implícita o basada en reglas (por ejemplo, por extensión) de seleccionar archivos. Cada path es relativo a source.path, no contiene . o .., no es absoluto, debe ser único dentro de files, y debe permanecer dentro del directorio después de resolver symlinks. adapter y options siguen el mismo contrato que en la fuente de archivo, por archivo.

files[].contentHash es opcional y, si está presente, es sha256: seguido del hash SHA-256 de los bytes originales de ese archivo. source.contentHash, si está presente en una fuente de directorio, es el hash agregado de toda la selección:

  1. Para cada archivo, construir { path, contentHash } con su hash real calculado en tiempo de ejecución (no el declarado en el manifiesto).
  2. Ordenar la lista lexicográficamente por path.
  3. Serializar la lista ordenada con JSON.stringify.
  4. Calcular SHA-256 sobre esa serialización y anteponer sha256:.

Este es el mismo cálculo que ya implementa el runtime de referencia (engine.js). El agregado cambia si cambia el contenido, el path o la membresía de la selección, pero no si solo cambia el orden de source.files en el manifiesto.

Vista

CampoRequeridoContrato
idÚnico dentro del paquete.
nameNombre visible.
promptIntención vigente de la vista y punto de partida para regenerarla.
rendererÚnico valor soportado en esta versión: "html" (const en el schema). No es un enum abierto todavía; agregar otro renderer es un cambio de especificación, no de implementación.
entry.transformMódulo relativo dentro del paquete.
entry.renderArtefacto relativo dentro del paquete.
descriptionnoDescripción humana de la interpretación.
settingsnoConfiguración específica de la vista.
resourcesnoDirectorio de recursos relativo y contenido dentro del paquete.

Los settings efectivos se calculan mediante merge superficial en este orden:

  1. settings del paquete.
  2. settings de la vista.
  3. Overrides explícitos del runtime.

Los valores posteriores reemplazan a los anteriores. No se hace merge recursivo.

Validaciones semánticas adicionales al schema

mview.schema.json valida forma y tipos, pero JSON Schema no puede expresar unicidad por clave dentro de un array de objetos ni resolución real de symlinks. Todo runtime conforme debe validar además, antes de ejecutar cualquier vista:

  • views[].id es único dentro del paquete.
  • source.files[].path es único dentro de la selección (solo aplica a fuente de directorio).
  • Cada entry.transform, entry.render, view.resources y source.files[].path resuelve, después de seguir symlinks, dentro de la raíz que lo contiene (el paquete .mview para entries y recursos; source.path para archivos de directorio). Cualquier resolución que escape se rechaza.
  • El adapter declarado (a nivel de source o de cada source.files[]) está registrado y su input coincide con la capacidad que ese adapter produce.

Un manifiesto que pasa el schema pero falla alguna de estas validaciones no es una MView ejecutable.

Entradas normalizadas

text

{ text: string }

table

{
  columns: string[];
  rows: Record<string, string>[];
}

markdown

{
  raw: string;
  headings: Array<{ level: number; text: string }>;
}

value

type JsonValue =
  | null
  | boolean
  | number
  | string
  | JsonValue[]
  | { [key: string]: JsonValue };
 
{ value: JsonValue }

filesystem

{
  path: string;
  files: Array<{
    path: string;
    inputType: "text" | "table" | "markdown" | "value";
    input: TextInput | TableInput | MarkdownInput | ValueInput;
    contentHash: string;
  }>;
}

files se ordena lexicográficamente por path. contentHash es SHA-256 de los bytes originales con prefijo sha256:.

Source Adapters

Un Source Adapter interpreta bytes físicos y produce una forma normalizada general. No conoce vistas, prompts ni modelos visuales.

export const id = "plain-text";
export const input = "text";
 
export default async function adapt(bytes, context) {
  return { text: new TextDecoder("utf-8", { fatal: true }).decode(bytes) };
}

Contexto:

{
  source: Readonly<Record<string, JsonValue>>;
  options: Readonly<Record<string, JsonValue>>;
}

Reglas:

  • Recibe solamente los bytes declarados y contexto acotado.
  • No descubre ni abre archivos.
  • No escribe la fuente.
  • Su id y forma input son estables. Un cambio incompatible requiere otro identificador.
  • Debe fallar con un error claro ante encoding o sintaxis inválida.

Adapters disponibles:

IDFormatoSalidaCódigo de referencia
csv-tableCSV UTF-8table/experimental/adapters/csv-table.mjs
json-valueJSON UTF-8value/experimental/adapters/json-value.mjs
markdown-textMarkdown UTF-8markdown/experimental/adapters/markdown-text.mjs
plain-texttexto UTF-8text/experimental/adapters/plain-text.mjs

El runtime registra estos módulos. El agente selecciona el ID; no copia el adapter dentro del paquete.

Transform

export default async function transform(input, context) {
  return { rows: input.rows };
}

Contexto:

{
  source: Readonly<Record<string, JsonValue>>;
  settings: Readonly<Record<string, JsonValue>>;
  view: Readonly<Record<string, JsonValue>>;
  manifest: Readonly<{
    id: string;
    name: string;
    description?: string;
    version: string;
    source: Record<string, JsonValue>;
    settings: Record<string, JsonValue>;
    metadata: Record<string, JsonValue>;
  }>;
}

manifest expone identidad y provenance del paquete (no de la vista) para casos donde el transform necesita, por ejemplo, incluir la versión del paquete en el modelo. No sustituye a settings ni a view como fuente de configuración que afecta el render.

El transform:

  • recibe entrada normalizada, nunca bytes ni handles de archivo;
  • no accede al filesystem, red, reloj, aleatoriedad o estado oculto;
  • es determinista respecto de input y context;
  • no muta input ni context;
  • devuelve solamente JsonValue;
  • valida el significado que necesita y falla con mensajes claros.

No son serializables: undefined, BigInt, funciones, símbolos, ciclos, NaN e infinitos.

Render

El runtime entrega:

{
  data: JsonValue;
  settings: Record<string, JsonValue>;
  source: Record<string, JsonValue>;
  view: Record<string, JsonValue>;
  resources: Record<string, string>;
}

Renderer html

entry.render apunta a un documento HTML estático. Antes de ejecutar sus scripts inline, el runtime inyecta el objeto anterior como window.mview.

<p id="total"></p>
<script>
  document.querySelector("#total").textContent = String(window.mview.data.total);
</script>

Reglas (MUST / MUST NOT):

  • El render presenta data; no lee ni interpreta la fuente.
  • No realiza fetch, XHR, WebSocket ni navegación programática.
  • Inserta datos mediante textContent, atributos seguros o APIs equivalentes; nunca los considera HTML confiable.
  • El runtime debe ejecutar el documento renderizado en un contexto de navegador aislado —por ejemplo un iframe con sandbox="allow-scripts" sin allow-same-origin ni allow-forms— sin acceso al host, filesystem o red del proceso que lo aloja.
  • El runtime debe servir el documento renderizado con una Content-Security-Policy que al menos bloquee red saliente (connect-src 'none') y restrinja frame-ancestors al propio host.
  • Recursos locales deben estar declarados y permanecer dentro de resources.

Esta política es normativa para cualquier renderer html conforme, independientemente del entorno (navegador embebido, Electron, servidor local, etc.).

Seguridad y rutas

  • El runtime es dueño de toda E/S.
  • Adapter, transform y render se consideran código no confiable.
  • Todo entry point y recurso debe permanecer dentro del paquete después de resolver symlinks.
  • source.path puede salir del paquete porque declara la fuente, pero debe permanecer dentro del workspace o ámbito autorizado por el usuario.
  • Una fuente de directorio expone exclusivamente source.files; nunca se extiende automáticamente con otros archivos del directorio, ni por regla ni por extensión.
  • Transform y render no reciben filesystem ni red.
  • El manifiesto se valida antes de guardar, cargar o ejecutar.
  • El runtime limita tiempo, memoria y tamaño de salida según su entorno.

Estrategias de ejecución (no normativo)

La especificación no exige un mecanismo concreto de aislamiento de proceso para el transform, solo las garantías anteriores. El runtime de referencia (mview/viewer) resuelve esto así, como un ejemplo válido entre varios posibles:

  • El transform corre en un subproceso de Node separado, con permisos de filesystem restringidos (--permission --allow-fs-read acotado al propio runner), límite de memoria y timeout.
  • El servidor HTTP local se enlaza solo a 127.0.0.1.
  • El endpoint de refresh exige un token efímero generado por sesión; no hay refresh no autenticado.

Otra implementación (por ejemplo, un runtime embebido en una app de escritorio) puede lograr el mismo aislamiento con Web Workers, procesos separados por sistema operativo, o un sandbox de VM distinto — el invariante que debe sostenerse es que un transform o render comprometido no puede leer ni escribir archivos que el usuario no expuso explícitamente.

Refresh y cache

Un refresh:

  1. Valida el manifiesto.
  2. Resuelve y valida rutas.
  3. Lee los bytes actuales de la fuente declarada.
  4. Ejecuta el adapter y valida la forma normalizada.
  5. Congela o clona la entrada para impedir mutación entre vistas.
  6. Ejecuta cada transform.
  7. Renderiza cada vista correcta.
validar manifiesto
        |
        v
resolver y validar rutas
        |
        v
leer bytes actuales de la fuente
        |
        v
adapter (una vez por refresh)
        |
        v
   entrada congelada
     /         \
    v           v
transform A   transform B   <- error aislado por vista
    |           |
    v           v
render A      render B      <- error aislado por vista

La cache de adaptación usa como mínimo: hash de bytes actuales, ID del adapter y source.options. La cache nunca reemplaza a la fuente de verdad.

Ámbitos de error:

  • Lectura, selección o adaptación: error de fuente; afecta a todas las vistas.
  • Transform: error de esa vista; las demás continúan.
  • Render: error de esa vista; las demás continúan.

Refresh y regeneración

  • Refresh: reejecuta artefactos existentes sin modificar el paquete.
  • Regenerar transform: modifica explícitamente la interpretación de una vista.
  • Regenerar render: modifica explícitamente su presentación.
  • Regenerar vista: reemplaza transform y render de una vista.
  • Regenerar paquete: permite reconsiderar fuente, selección, adapters y vistas.

Toda regeneración persistida incrementa version cuando cambia comportamiento observable.

Ejemplo completo

Fuente sales.csv:

Month,Client,Total
Jan,Acme,120
Feb,Globex,180
Mar,Acme,225

sales-dashboard.mview/mview.json:

{
  "$schema": "https://carmenlabs.com/experimental/schema.json",
  "id": "sales-dashboard",
  "name": "Sales Dashboard",
  "version": "1.0.0",
  "source": {
    "path": "../sales.csv",
    "adapter": "csv-table",
    "input": "table"
  },
  "views": [
    {
      "id": "summary",
      "name": "Summary",
      "prompt": "Show total sales and sales by month.",
      "renderer": "html",
      "entry": {
        "transform": "./views/summary/transform.mjs",
        "render": "./views/summary/view.html"
      }
    }
  ]
}

sales-dashboard.mview/views/summary/transform.mjs:

export default async function transform(input) {
  const required = ["Month", "Total"];
  const missing = required.filter((column) => !input.columns.includes(column));
  if (missing.length) throw new Error(`Missing required columns: ${missing.join(", ")}`);
 
  const months = input.rows.map((row) => ({
    month: row.Month,
    total: Number(row.Total || 0)
  }));
 
  return {
    rowCount: input.rows.length,
    totalSales: months.reduce((sum, row) => sum + row.total, 0),
    months
  };
}

sales-dashboard.mview/views/summary/view.html:

<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Sales Dashboard</title>
  </head>
  <body>
    <main>
      <h1>Sales Dashboard</h1>
      <p>Total: <strong id="total"></strong></p>
      <ul id="months"></ul>
    </main>
    <script>
      const { data } = window.mview;
      document.querySelector("#total").textContent = String(data.totalSales);
      const list = document.querySelector("#months");
      for (const item of data.months) {
        const row = document.createElement("li");
        row.textContent = `${item.month}: ${item.total}`;
        list.append(row);
      }
    </script>
  </body>
</html>

Más patrones para JSON, Markdown, texto, varias vistas y directorios aparecen en Ejemplos para agentes. La entrada /experimental contiene únicamente el procedimiento operativo y los índices necesarios para un agente.

Comprobación del paquete

Antes de terminar:

  • Existe un directorio *.mview/.
  • mview.json valida contra /experimental/schema.json.
  • Todos los views[].id son únicos.
  • Source Adapter e input son compatibles.
  • Todas las rutas de entry y recursos permanecen dentro del paquete.
  • Para fuente de directorio: source.files no está vacío, cada path es único y ninguno escapa de source.path (ni por .. ni por symlink).
  • El transform ejecuta con la entrada real y devuelve JSON válido.
  • El render consume exactamente el modelo retornado.
  • Un refresh sobre una copia modificada actualiza el resultado sin regenerar archivos.
  • La fuente original conserva exactamente sus bytes.
  • La respuesta final informa cada validación realmente ejecutada.

Recursos canónicos de Experimental

Todo lo necesario para construir una MView Experimental está contenido bajo /experimental.