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.

Catálogo de ejemplos para MView Experimental

Referencias técnicas para crear MViews sin imponer una presentación visual.

Catálogo de ejemplos para MView Experimental

Estado

Este catálogo forma parte de la especificación Experimental. Sus manifests usan exclusivamente el schema Experimental.

El objetivo no es mostrar todas las posibilidades de MView. Es entregar un conjunto pequeño de patrones completos para que un modelo seleccione el más cercano y escriba la menor cantidad posible de código nuevo.

Los 7 ejemplos mínimos que este catálogo describe en prosa están además implementados como paquetes reales, probados end-to-end (validación, ejecución contra la fuente real, refresh sobre una copia temporal, y confirmación de que la fuente y el paquete no se modifican), bajo mview/viewer/examples/. Cada carpeta tiene su propio README.md con "qué copiar" / "qué reemplazar". Correr pnpm test en mview/viewer ejecuta los siete junto con el resto de la suite.

Regla para agentes

Antes de crear archivos:

  1. Inspeccionar la fuente real.
  2. Elegir la forma normalizada.
  3. Elegir un adapter registrado.
  4. Copiar el ejemplo más cercano.
  5. Reemplazar únicamente identidad, prompt, campos reales, transform y presentación.

Si existe un adapter adecuado, el modelo no debe copiarlo, modificarlo ni reimplementarlo dentro del paquete.

Matriz de selección

Fuente inspeccionadaAdapterEntrada normalizadaEjemplo basePaquete descargable
CSV UTF-8csv-tabletablePanel tabular/experimental/examples/csv/
JSON estrictojson-valuevalueResumen estructurado/experimental/examples/json/
Markdown UTF-8markdown-textmarkdownNavegador de documento/experimental/examples/markdown/
TXT UTF-8plain-texttextMétricas de texto/experimental/examples/text/
Selección de archivosuno por archivofilesystemReporte de carpeta/experimental/examples/directory/

Los archivos exactos de cada paquete (fuente, manifiesto, transform y render por vista) están listados en Paquetes descargables, más abajo.

Paquetes descargables

Los 7 ejemplos mínimos no son solo prosa: cada archivo real está publicado como recurso estático bajo /experimental/examples/<nombre>/. Bajalo, modificalo, y usalo como punto de partida — no hace falta escribir un paquete desde cero para tener algo ejecutable.

CSV con dos vistas

JSON estructurado

Markdown

Texto plano

Directorio explícito

Vista que falla junto a otra válida

Convención de paquetes

<nombre>.mview/
├── mview.json
└── views/
    └── <view-id>/
        ├── transform.mjs
        └── view.html

Una segunda vista añade otro directorio bajo views/; no duplica la fuente ni el adapter.

Ejemplo 1: CSV con una vista

Fuente

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

Manifiesto

{
  "$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"
      }
    }
  ]
}

Transform

export default async function transform(input) {
  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
  };
}

Render HTML mínimo

<!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>

El uso de textContent es intencional: los datos de la fuente no se tratan como HTML confiable.

Ejemplo 2: añadir una segunda vista al mismo CSV

Solo se agrega otra entrada al array y otro directorio. La fuente se sigue leyendo y adaptando una vez por refresh.

{
  "id": "top-clients",
  "name": "Top Clients",
  "prompt": "Rank clients by accumulated sales.",
  "renderer": "html",
  "entry": {
    "transform": "./views/top-clients/transform.mjs",
    "render": "./views/top-clients/view.html"
  }
}
export default async function transform(input) {
  const totals = new Map();
  for (const row of input.rows) {
    totals.set(row.Client, (totals.get(row.Client) || 0) + Number(row.Total || 0));
  }
 
  return {
    clients: [...totals.entries()]
      .map(([client, total]) => ({ client, total }))
      .sort((a, b) => b.total - a.total)
  };
}

Este ejemplo debe probar que:

  • el adapter se invoca una vez;
  • ambos transforms reciben la misma entrada lógica;
  • mutar la entrada desde un transform no afecta al otro;
  • un fallo de render en una vista no oculta la otra.

Ejemplo 3: JSON estructurado

Fuente

{
  "project": "Atlas",
  "status": "active",
  "milestones": [
    { "name": "Design", "done": true },
    { "name": "Launch", "done": false }
  ]
}

Fuente en el manifiesto

"source": {
  "path": "../project.json",
  "adapter": "json-value",
  "input": "value"
}

Transform

export default async function transform(input) {
  const project = input.value;
  if (!project || typeof project !== "object" || Array.isArray(project)) {
    throw new Error("Expected a project object in project.json");
  }
 
  const milestones = Array.isArray(project.milestones) ? project.milestones : [];
  return {
    name: String(project.project || "Unnamed project"),
    status: String(project.status || "unknown"),
    completed: milestones.filter((item) => item?.done === true).length,
    total: milestones.length
  };
}

El adapter resuelve JSON físico. El transform valida significado y construye el modelo específico de la vista.

Ejemplo 4: Markdown

Fuente en el manifiesto

"source": {
  "path": "../notes.md",
  "adapter": "markdown-text",
  "input": "markdown"
}

Transform

export default async function transform(input) {
  return {
    title: input.headings.find((heading) => heading.level === 1)?.text || "Document",
    sections: input.headings.filter((heading) => heading.level === 2),
    characterCount: input.raw.length
  };
}

El ejemplo base no debería implementar un parser Markdown en el transform ni insertar input.raw mediante innerHTML.

Ejemplo 5: texto plano

Fuente en el manifiesto

"source": {
  "path": "../server.log",
  "adapter": "plain-text",
  "input": "text"
}

Transform

export default async function transform(input) {
  const lines = input.text.split(/\r?\n/).filter(Boolean);
  return {
    lineCount: lines.length,
    errors: lines.filter((line) => line.includes("ERROR")).length,
    warnings: lines.filter((line) => line.includes("WARN")).length
  };
}

La semántica de ERROR y WARN pertenece al transform de esta vista, no al adapter de texto general.

Ejemplo 6: directorio explícito

Estructura

reports/
├── january.csv
├── february.csv
├── notes.md
└── private.txt

Fuente en el manifiesto

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

private.txt no forma parte de la MView. El runtime no debe leerlo, hashearlo, descubrirlo ni entregarlo al transform.

Transform orientativo

export default async function transform(input) {
  const tables = input.files.filter((file) => file.inputType === "table");
  const rows = tables.flatMap((file) => file.input.rows);
 
  return {
    selectedFiles: input.files.map((file) => file.path),
    rowCount: rows.length,
    total: rows.reduce((sum, row) => sum + Number(row.Total || 0), 0)
  };
}

inputType identifica la forma normalizada de cada archivo y coincide con el input declarado para ese archivo en el manifiesto.

Templates copiables

Transform de tabla

export default async function transform(input, context) {
  const required = [/* columnas reales inspeccionadas */];
  const missing = required.filter((column) => !input.columns.includes(column));
  if (missing.length > 0) {
    throw new Error(`Missing required columns: ${missing.join(", ")}`);
  }
 
  return {
    title: context.view.name || context.view.id,
    rows: input.rows.map((row) => ({
      // Mapear solamente campos reales necesarios por el render.
    }))
  };
}

Render de lista seguro

<ul id="items"></ul>
<script>
  const list = document.querySelector("#items");
  for (const item of window.mview.data.items) {
    const element = document.createElement("li");
    element.textContent = item.label;
    list.append(element);
  }
</script>

Error semántico claro

if (!Array.isArray(input.value?.items)) {
  throw new Error("Expected project.json to contain an items array");
}

Qué debe contener cada ejemplo implementado

<example>/
├── README.md
├── source files
├── expected/
│   └── data.json
├── <name>.mview/
│   ├── mview.json
│   └── views/
│       └── ...
└── test.mjs

El test debe:

  1. Validar el manifiesto.
  2. Copiar la fuente a un directorio temporal.
  3. Ejecutar adapter y transform.
  4. Comparar el modelo con expected/data.json.
  5. Cambiar la copia temporal de la fuente.
  6. Ejecutar refresh sin modificar el paquete.
  7. Confirmar que el modelo cambió.
  8. Confirmar que la fuente original permaneció intacta.

Anti-ejemplos necesarios

Además de caminos felices, el catálogo debe incluir fixtures que demuestren rechazos:

  • entry point con ../ que escapa del paquete;
  • symlink de recurso que escapa del paquete;
  • archivo de directorio no seleccionado;
  • dos vistas con el mismo id;
  • adapter que produce una forma diferente de la declarada;
  • transform que devuelve undefined, BigInt o un ciclo;
  • transform que intenta leer el filesystem;
  • render que intenta obtener la fuente mediante fetch;
  • fallo de una vista junto a otra vista válida.

Estado: cumplido

Todos, salvo uno, están probados en mview/viewer/test/security-anti-examples.test.js (10 casos). "Archivo de directorio no seleccionado" se prueba dentro de examples/directory/test.mjs (march.csv se agrega a la copia temporal sin declararlo y se confirma que no afecta el modelo). "Fallo de una vista junto a otra válida" tiene su propio ejemplo completo en examples/failing-view/. El único caso no cubierto por un test automatizado es "render que intenta fetch": se aplica vía la Content-Security-Policy del documento renderizado (connect-src 'none' en viewer.js), una garantía que exige un navegador real y no el test runner de Node; queda documentado como aplicado pero no verificado por test automatizado hasta que exista un runner de integración con navegador headless.

Construir el caso "adapter que produce una forma diferente" no encontró un bug — la validación de forma normalizada en adapters.js ya lo rechazaba correctamente. Construir los casos de aislamiento de process/filesystem sí sirvió para reconfirmar, después del fix de transform-runner.js (ver roadmap, Fase 5), que los mensajes de error siguen siendo legibles y no duplican el prefijo "Error: ".

Criterio de calidad para modelos

Los ejemplos cumplen su propósito si un agente puede resolver una tarea común sin generar:

  • un Source Adapter;
  • un lector de archivos;
  • un validador JSON Schema;
  • un servidor web;
  • un mecanismo propio de refresh;
  • un global de datos distinto de la entrega definida por el renderer;
  • datos de muestra que sustituyan a la fuente real.

El código nuevo debería concentrarse en dos preguntas: qué interpretación necesita el usuario y cómo debe presentarse.