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:
- Inspeccionar la fuente real.
- Elegir la forma normalizada.
- Elegir un adapter registrado.
- Copiar el ejemplo más cercano.
- 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 inspeccionada | Adapter | Entrada normalizada | Ejemplo base | Paquete descargable |
|---|---|---|---|---|
| CSV UTF-8 | csv-table | table | Panel tabular | /experimental/examples/csv/ |
| JSON estricto | json-value | value | Resumen estructurado | /experimental/examples/json/ |
| Markdown UTF-8 | markdown-text | markdown | Navegador de documento | /experimental/examples/markdown/ |
| TXT UTF-8 | plain-text | text | Métricas de texto | /experimental/examples/text/ |
| Selección de archivos | uno por archivo | filesystem | Reporte 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
- Fuente: pokemons-data.csv
- Manifiesto: mview.json
- Vista
stats: transform.mjs, view.html - Vista
pokedex: transform.mjs, view.html - README
JSON estructurado
- Fuente: products.json
- Manifiesto: mview.json
- Vista
overview: transform.mjs, view.html - README
Markdown
- Fuente: guide.md
- Manifiesto: mview.json
- Vista
toc: transform.mjs, view.html - README
Texto plano
- Fuente: access.log
- Manifiesto: mview.json
- Vista
metrics: transform.mjs, view.html - README
Directorio explícito
- Fuente: january.csv, february.csv, notes/summary.md
- Manifiesto: mview.json
- Vista
summary: transform.mjs, view.html - README
Vista que falla junto a otra válida
- Fuente: sales.csv
- Manifiesto: mview.json
- Vista
good: transform.mjs, view.html - Vista
broken: transform.mjs, view.html - README
Convención de paquetes
<nombre>.mview/
├── mview.json
└── views/
└── <view-id>/
├── transform.mjs
└── view.htmlUna 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,225Manifiesto
{
"$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.txtFuente 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.mjsEl test debe:
- Validar el manifiesto.
- Copiar la fuente a un directorio temporal.
- Ejecutar adapter y transform.
- Comparar el modelo con
expected/data.json. - Cambiar la copia temporal de la fuente.
- Ejecutar refresh sin modificar el paquete.
- Confirmar que el modelo cambió.
- 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,BigInto 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.
