Incidente 2026-09-11 · patch mixto serializado en wiki_update_page corrompe dos páginas wiki
Cuándo
11-09-2026. El daño ocurrió durante una pasada del Bibliotecario-Ingest de la estación que procesaba el commit 4a80cc7a (PR #172, entrega 2 del Oráculo híbrido) y se mantuvo hasta su restauración manual el mismo día (PR #174). El fix de raíz llegó unas horas después en el commit 7777cb1f (PR #175).
Síntomas visibles
Dos páginas de la wiki Supercontexto quedaron con su fichero .md reducido a una sola línea de JSON crudo en vez de contenido markdown:
feature--workspace--busqueda-texto-completo(commit668a702f)decision--20260910--busqueda-texto-completo-wikis(commitc180252d)
El front-matter y el cuerpo real desaparecieron de la vista; en su lugar, el fichero contenía el objeto patch completo tal como lo había serializado el llamador.
Causa raíz
El Ingest mandó a wiki_update_page un patch MIXTO serializado como string: un JSON con sources (metadata) y content (el cuerpo real, markdown de más de 200 caracteres). La guarda looksLikeSerializedMetadata de functions/api/mcp/handlers/wiki/crud.ts tenía un corte deliberado por longitud: si el content dentro del JSON superaba 200 caracteres, la guarda lo dejaba pasar como “patch mixto legítimo” hacia el camino de string del handler legacy — y ese camino escribe el patch recibido entero como cuerpo de la página, en vez de repartir content al cuerpo y el resto a metadata (que es lo que hace el camino de objeto).
En la práctica, cualquier patch mixto con cuerpo largo —el caso normal cuando el Ingest actualiza una página con contenido nuevo— se colaba por esa excepción y corrompía la página.
Fix aplicado
Commit 7777cb1fdeee8f9b450607246dc2c669d36b9462 (PR #175): se elimina el corte por longitud de content en looksLikeSerializedMetadata. Ahora cualquier objeto serializado cuyas claves sean todas de metadata conocida o content se rescata como objeto — el camino de objeto ya sabe repartir content → cuerpo y el resto → metadata. Un cuerpo markdown de verdad (no JSON) o un JSON con claves desconocidas siguen sin activar la guarda.
Test de regresión nuevo, test/wiki-update-patch-guard.test.ts, con 3 casos: patch solo-metadata, patch mixto con cuerpo largo (el caso que causó el incidente), y no-JSON / claves desconocidas (no debe tocarse).
Lecciones
- Una guarda “de rescate” con una excepción por longitud es una guarda a medias: el caso que se dejaba pasar sin rescatar (patch mixto largo) es precisamente el más común en el uso real del Ingest, que casi siempre manda cuerpos de más de 200 caracteres.
- Es el mismo patrón de fondo que [[decision—20260707—normalizar-sources-related-mcp]]: un llamador MCP serializa el patch antes de que el handler lo reciba, y el handler necesita reconocerlo y desempaquetarlo en vez de tratarlo como texto literal.
- La cobertura de test es lo que fija el contrato para que la próxima persona (o agente) que toque
looksLikeSerializedMetadatano reintroduzca el mismo corte parcial.
Preventivos futuros
- Los llamadores de
wiki_update_page(incluido este mismo Ingest) deberían preferir pasarpatchcomo objeto nativo cuando el cliente MCP lo permita, en vez de depender de que la guarda rescate un string serializado — la guarda es una red de seguridad, no la vía principal. - Cualquier cambio futuro en
looksLikeSerializedMetadataque añada un corte o heurística parcial debe venir acompañado de un test de regresión, como los tres de este commit.
Recurrencia 24-09-2026 · mega-auditoría T43 (10 páginas más, corrupción antigua nunca detectada)
La mega-auditoría T43 del 24-09-2026 (PR #187, commit 0d806b01a0d83a2d7a6fd70021a35995f4ece5d2) encontró 10 páginas más de la wiki Help con el mismo síntoma — el .md entero reducido a {"content": "...markdown escapado..."} —, pero con una diferencia importante respecto al incidente de arriba: no son corrupciones nuevas. Sus created_at van de abril a agosto de 2026 (entity--monitoring--model--aiinsight desde el 21-04, entity--racks--service--safe-fs desde el 02-06, entity--core--service--log-action y runbook--infra--netbird-proxies desde el 11-06, hasta feature--auth--email-welcome-activation-link del 04-08) — es decir, llevaban corrompidas desde su creación o su última edición, meses antes incluso del incidente del 11-09 de arriba, y nadie lo notó porque no había ningún chequeo repo-side que lo detectara: solo salió a la luz por una auditoría manual página a página.
Páginas afectadas: concept--general--como-detecta-el-frontend-si-el-local-agent-esta-co, crearack--racks--trash-and-restore, decision--20260713--visio-confirm-hardening, entity--core--function--serve-media-gated, entity--core--service--log-action, entity--monitoring--model--aiinsight, entity--monitoring--test--metrics-read-write-contract, entity--racks--service--safe-fs, feature--auth--email-welcome-activation-link, runbook--infra--netbird-proxies.
Causa raíz de esta tanda: una variante hermana del bug de arriba, no la misma. El propio commit la señala como “la trampa conocida de wiki_update_page con patch como objeto” — el footgun documentado desde el 16-05-2026 (footguns_wiki_update_page_patch_object en la memoria compartida del equipo, no en esta wiki): cuando patch llega como objeto con un campo content, el handler históricamente escribía el objeto entero tal cual en vez de extraer content. Es un camino de código distinto al looksLikeSerializedMetadata corregido el 11-09 (ese actuaba sobre un patch string que parecía JSON); aquí el patch ya llegaba como objeto. Mismo síntoma final, dos bugs de fondo distintos, cada uno cerrado por separado.
Fix aplicado (esta tanda): a diferencia del fix del 11-09 (en el handler MCP, functions/api/mcp/handlers/wiki/crud.ts), este fix vive en el repo, no en el servidor MCP: scripts/validate_wiki.py gana un chequeo de error que rechaza cualquier página cuyo cuerpo empiece por {"content". Corre en modo --staged (pre-commit) y en modo --full (CI), así que de aquí en adelante esta clase de corrupción — venga del handler, de un patch mal formado, o de cualquier otra vía futura no prevista — bloquea el commit en vez de quedarse invisible durante meses.
Lección añadida: un fix en el handler MCP solo cierra el camino de código concreto que lo causó; no repara lo que ya está corrupto ni detecta variantes por otras vías. La defensa que de verdad cierra el hueco es la del contenido resultante (repo-side, agnóstica de qué camino de código lo produjo), no solo la del código que lo produce.
Véase también
- [[decision—20260707—normalizar-sources-related-mcp]]
- [[concept—workflow—wiki-create-page-footgun]]
- [[feature—workspace—busqueda-texto-completo]]
- [[decision—20260910—busqueda-texto-completo-wikis]]
- [[entity—bib-ingest—tool—wiki-index-metadata]]
- [[feature—biblioteca—durable-metadata-sync]]
- [[decision—20260422—ingest-3-tier]]