Problema
Tres incidencias registradas (s56, s61, s202 · 2026-07-07):
- Callers de
wiki_create_pagepasabanfront_matter.sourcescomo array de strings (paths de archivos) en lugar del shape esperado:{type: 'code'|'commit'|'doc'|'wiki'|'memory'|'query', ref: string, last_seen?: string}. - El commit salía “OK” — la función retornaba
{status: "created"}sin errores aparentes. - Pero el schema Astro en
src/content.config.tsexige objetos{type, ref, last_seen?}(type en enum). - El build de CF Pages reventaba silenciosamente: durante el
cf-pages-deploy(cron de OPS), la validación de Zod fallaba pero el error quedaba enterrado en los logs. - El deploy del sitio cae en silencio: el usuario final no ve un error claro; simplemente, la página wiki no aparece en la web.
Esto sucedió en tres ocasiones en el ciclo de desarrollo porque:
- El handler aceptaba cualquier cosa en
sources(cero validación). - El error se producía en una etapa posterior (Astro build), no en la API.
- 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
- Fail early, fail loud: la validación ocurre en el handler MCP, no en el build. El caller recibe error con contexto.
- Coerciones amables: strings →
{type: 'code', ref, last_seen: hoy}. Type fuera del enum →'code'(espejo del.catchdel schema). Números (SHA parseados como int) → string. - Alias aceptados:
pathyfilecomo sinónimos derefensources.slugyrefcomo sinónimos enrelated. - Entradas vacías se descartan sin error: strings vacíos o con solo espacios se ignoran. Pero objetos sin
refoslugreciben un error claro. - Metadata durable: como
relatedes 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 }
-
Input: cualquier valor (undefined, null, array, string, objeto).
-
Output:
{ok: true, sources: [...]}si todo es normalizable.{ok: false, error: "..."}con índice del campo que falla.
-
Lógica:
null/undefined→[].- No-array → error.
- Cada entrada:
- String/número →
{type: 'code', ref: string, last_seen: ISO hoy}. - Objeto con
ref/path/file→{type: <'code' si inválido>, ref, last_seen?}. - Objeto sin
ref→ error con índice. - Vacío (string trimmado = "") → skip sin error.
- String/número →
normalizeRelated(input: unknown)
Exportada en functions/api/mcp/handlers/wiki.ts:
function normalizeRelated(
input: unknown
): { ok: true; related: string[] } | { ok: false; error: string }
-
Input: cualquier valor.
-
Output:
{ok: true, related: [...]}si todo es normalizable.{ok: false, error: "..."}con índice.
-
Lógica:
null/undefined→[].- No-array → error.
- Cada entrada:
- String/número → slug trimmado.
- Objeto con
slugoref→ extrae y trimma. - Vacío → skip sin error.
- Sin slug/ref → error con índice.
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:
- Regresión s56/s61/s202: normaliza strings a
{type:code, ref, last_seen}. - Deja pasar objetos válidos sin tocarlos.
- Conserva
last_seencuando el caller lo pasa. - Type fuera del enum cae a
'code'. - Coerce de ref numérico (SHA parseado como int).
- Acepta alias
path/filecomoref. - Descarta strings vacíos sin error.
- Rechaza entradas irrecuperables con error que apunta al índice.
null/undefined→ array vacío; no-array → error.
normalizeRelated:
- Deja pasar strings y trimma espacios.
- Aplana objetos
{slug}/{ref}al string. - Rechaza entradas irrecuperables con error que apunta al índice.
null/undefined→ array vacío; no-array → error.
Lecciones aprendidas
- 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.
- Los callers son variados: algunos pasan
sourcescomo strings (del usuario, o parseados directamente de YAML/JSON). La normalización amable (no el rechazo) es más robusta. - 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.
- Tests de regresión: 3 incidencias de la misma clase. Estos tests aseguran que no vuelva a ocurrir.
Véase también
- [[entity—mcp—handler—github]]
- [[feature—biblioteca—candidatos-semanticos-wiki-enrich]]