# data-engine

Contenido unificado de un negocio: **notebooks, wikis, logs y artifacts** sobre un mismo modelo, multi-tenant, con cada cambio guardado como en git (historial, diff, revert, tags). REST + MCP (todo el engine, más tools dedicados para artifacts). Spec completa: `/openapi.json` (Swagger UI en `/docs`). Cumple el **contrato v1** de engines de nicetry.

## Auth

- Rutas de tenant: `/{tenant}/...` con el header `X-Engine-Key: <api key del tenant>`. Sin una key válida, 401 (exista o no el tenant); con la key de otro tenant, 403.
- Alta de tenants: `/admin/tenants` con `x-admin-api-key`. La api key se devuelve una sola vez (al crear o al rotar con `POST /admin/tenants/{slug}/rotate-key`). `DELETE /admin/tenants/{slug}` da 409 si el tenant tiene datos, salvo con el cuerpo `{"confirm": "<slug>"}`.
- `/r/{token}/` es público: el `renderToken` del artifact es la credencial.

## Convenciones

- **`X-Actor: <system>:<id>`** en toda escritura (`panel:ana@empresa.com`, `agent:<rutina>`, `loops:<slug>`). Queda en el commit como `actor`. Si falta, vale el `actor` del cuerpo JSON, y si tampoco está, `unknown:`. Por ahora un valor con otra forma se registra tal cual; cuando el engine pase a modo estricto, en una escritura un actor que falta o no cumple el formato dará 400. Las lecturas nunca lo exigen.
- **`X-Request-Id`**: se respeta el que llega o se genera uno; vuelve siempre en la respuesta.
- **Errores**: `{ "error": "...", "code": "...", "details": [...], "requestId": "..." }`. `code` es estable: `validation_error`, `invalid_json`, `unauthorized`, `forbidden`, `not_found`, `conflict`, `idempotency_conflict`, `payload_too_large`, `unsupported_media_type`, `rate_limited` (con `Retry-After`), `unavailable` (la base), `internal`.
- **Listados** (`GET /{tenant}/<entidad>`, `/log`, `/search`, `/tags`): `?limit` (100 por defecto, hasta 500) y `?cursor`. El cuerpo es el de siempre; el cursor de la página siguiente viene en el header `X-Next-Cursor`, ausente en la última.
- **`Idempotency-Key`** en los POST: con la misma key y el mismo cuerpo dentro de 24 h devuelve la respuesta original (`Idempotent-Replayed: true`) sin volver a crear; con otro cuerpo, 409 `idempotency_conflict`.
- Un `slug` explícito que ya existe da 409; sin `slug`, se deriva del título y se desambigua con un sufijo.

## Modelo

- **Contenedor** (`/containers`): `kind` = `notebook` | `wiki` | `log` | `artifact` (no cambia), `title`, `slug` (único por kind), `folder` (agrupación libre), `description`, `body` (markdown), `tags`, `meta`. Se lo puede nombrar por id o por `<kind>:<slug>` (p.ej. `/containers/notebook:ideas`).
- **Tipo de ítem** (`/item-types`): los tipos de entidad de una wiki, con su lista de `attributes`. Un log tiene uno fijo (`thread`) y un notebook otro (`source`); se crean solos con el primer ítem.
- **Ítem** (`/items`): `identifier`, `content` (markdown), `summary`, `attributes` (nombre → texto; un nombre nuevo se suma al tipo), `meta`. Es un objeto de wiki, un hilo de log o una fuente de notebook.
- **Entrada** (`/entries`): append-only, no se edita. En un notebook es una nota (sin ítem); en un log, un mensaje de un hilo (`itemId` obligatorio, hasta 500 por hilo). `occurredAt` se puede fijar.
- **Archivo**: `PUT|GET|DELETE /{tenant}/containers/{id}/files/{path}`, con el cuerpo crudo y su Content-Type. Solo en notebooks y artifacts; 10 MB por archivo, 30 por contenedor. Los bytes se guardan por hash: una versión que no cambió un archivo no lo duplica.
- **Inbox** (`/inbox`): texto o archivo (`PUT /inbox/{id}/file`) con un destino sugerido y estado `pendiente` | `triado` | `descartado`. Solo la estructura.

Qué admite cada kind:

| kind | ítems | entradas | archivos | render |
|---|---|---|---|---|
| notebook | fuentes | notas | sí | — |
| wiki | objetos, por tipo | — | — | — |
| log | hilos | mensajes por hilo | — | — |
| artifact | — | — | sí | si tiene `meta.render` |

Hasta 100 contenedores por kind por tenant.

## Artifacts y render

Un artifact es un contenedor con archivos. Si tiene `meta.render = { "entry": "index.html", "format": "html" | "markdown" | "react" }`, el engine le asigna un `renderToken` y lo sirve en `/r/{renderToken}/`: el entry renderizado (markdown y react con un visor mínimo por CDN) y los demás archivos en `/r/{renderToken}/{path}`, así un html resuelve sus assets relativos. Todo se sirve con `Content-Security-Policy: sandbox`. `POST /{tenant}/containers/{id}/render-token` invalida el link y da uno nuevo. El token no aparece en listados ni en el historial.

## Lecturas útiles

- `GET /{tenant}/containers/{id}/tree`: un contenedor con sus tipos, ítems, entradas y archivos.
- `GET /{tenant}/search?q=&kind=`: busca en contenedores, ítems y entradas, sin distinguir mayúsculas ni tildes.

## Escrituras e historial

- Toda escritura acepta `author` y `message` (en el body; en query string para DELETE y archivos) y genera un commit.
- `POST /{tenant}/batch`: varias operaciones en un commit, todas o ninguna; `$ref:<nombre>` apunta a lo creado antes en el mismo lote.
- `GET /{tenant}/log`, `GET /{tenant}/commits/{ref}`, `GET /{tenant}/diff?from=&to=`, `POST /{tenant}/commits/{ref}/revert` (409 si hubo cambios posteriores, salvo `force`), `/{tenant}/tags`, y `GET /{tenant}/<entidad>/{id}/history`. Un ref es `head`, el id de un commit o el nombre de un tag.
- Borrar un contenedor borra todo lo que tiene adentro en el mismo commit, y un revert lo restaura.

## MCP

`/{tenant}/mcp` (Streamable HTTP, stateless, mismo `X-Engine-Key`). `help` devuelve este documento y `api` llama a cualquier ruta REST del tenant (`method`, `path` relativo al tenant, `query`, `body`), con la misma key y el mismo `X-Actor`, así todo el engine es accesible por MCP. Devuelve `<status>`, después `next-cursor: <c>` si hay más (se sigue con `query.cursor`) y `deprecation: …` si la ruta está deprecada, y al final el cuerpo. Además, para artifacts de un solo archivo (`kind` = html | markdown | react): `list_artifacts`, `get_artifact`, `create_artifact`, `update_artifact`, `delete_artifact`, que devuelven `render_url` armada.
