Volver a la wiki

Normalización de sources/related en MCP handlers — blindaje del build de Astro

Problema

Tres incidencias registradas (s56, s61, s202 · 2026-07-07):

Esto sucedió en tres ocasiones en el ciclo de desarrollo porque:

  1. El handler aceptaba cualquier cosa en sources (cero validación).
  2. El error se producía en una etapa posterior (Astro build), no en la API.
  3. No había tests unitarios de coerción de tipo.

Decisión

Blindaje en la puerta de wiki_create_page y wiki_update_page: antes de cualquier commit o actualización en D1, normalizar sources y related al shape esperado por el schema Astro. Errores de normalización deben ser rechazados inmediatamente con un mensaje claro que apunte al índice del campo problemático, en lugar de descartarse en silencio.

Principios

  1. Fail early, fail loud: la validación ocurre en el handler MCP, no en el build. El caller recibe error con contexto.
  2. Coerciones amables: strings → {type: 'code', ref, last_seen: hoy}. Type fuera del enum → 'code' (espejo del .catch del schema). Números (SHA parseados como int) → string.
  3. Alias aceptados: path y file como sinónimos de ref en sources. slug y ref como sinónimos en related.
  4. Entradas vacías se descartan sin error: strings vacíos o con solo espacios se ignoran. Pero objetos sin ref o slug reciben un error claro.
  5. Metadata durable: como related es metadata que se persiste en el front-matter del .md, la normalización debe ser idempotente y transparente.

Implementación

normalizeSources(input: unknown)

Exportada en functions/api/mcp/handlers/wiki.ts:

function normalizeSources(
  input: unknown
): { ok: true; sources: FrontMatter['sources'] } | { ok: false; error: string }

normalizeRelated(input: unknown)

Exportada en functions/api/mcp/handlers/wiki.ts:

function normalizeRelated(
  input: unknown
): { ok: true; related: string[] } | { ok: false; error: string }

Integración en wikiCreatePage

Antes de construir el FrontMatter final, se normalizan:

const srcNorm = normalizeSources(frontMatterIn.sources);
if (!srcNorm.ok) return JSON.stringify({ error: srcNorm.error });

const relNorm = normalizeRelated(frontMatterIn.related);
if (!relNorm.ok) return JSON.stringify({ error: relNorm.error });

const fm: FrontMatter = {
  // ...
  sources: srcNorm.sources,
  related: relNorm.related,
  // ...
};

Integración en wikiUpdatePage

Igual que en wikiCreatePage, pero dentro del bloque de update_metadata:

if (key === 'sources') {
  const norm = normalizeSources(p[key]);
  if (!norm.ok) return JSON.stringify({ error: norm.error });
  metaUpdates[key] = JSON.stringify(norm.sources);
} else if (key === 'related') {
  const norm = normalizeRelated(p[key]);
  if (!norm.ok) return JSON.stringify({ error: norm.error });
  metaUpdates[key] = JSON.stringify(norm.related);
}

Testing

Archivo nuevo: test/wiki-normalize-front-matter.test.ts con 13 tests unitarios:

normalizeSources:

  1. Regresión s56/s61/s202: normaliza strings a {type:code, ref, last_seen}.
  2. Deja pasar objetos válidos sin tocarlos.
  3. Conserva last_seen cuando el caller lo pasa.
  4. Type fuera del enum cae a 'code'.
  5. Coerce de ref numérico (SHA parseado como int).
  6. Acepta alias path/file como ref.
  7. Descarta strings vacíos sin error.
  8. Rechaza entradas irrecuperables con error que apunta al índice.
  9. null/undefined → array vacío; no-array → error.

normalizeRelated:

  1. Deja pasar strings y trimma espacios.
  2. Aplana objetos {slug}/{ref} al string.
  3. Rechaza entradas irrecuperables con error que apunta al índice.
  4. null/undefined → array vacío; no-array → error.

Lecciones aprendidas

  1. El schema Astro es una línea Maginot: no está en el handler. El error ocurre 2-3 minutos después en el build de CF Pages. Necesitamos validación preventiva en la puerta.
  2. Los callers son variados: algunos pasan sources como strings (del usuario, o parseados directamente de YAML/JSON). La normalización amable (no el rechazo) es más robusta.
  3. Los errores silenciosos matan la confianza: “El commit salió OK” pero el deploy falló. Hay que garantizar que si algo falla, el error es visible e inmediato.
  4. Tests de regresión: 3 incidencias de la misma clase. Estos tests aseguran que no vuelva a ocurrir.

Véase también

Subir