Capa 1 del plan del 2026-07-29: lo que se arregla en el generador se arregla
una vez; lo que se arregla con un vigilante es trabajo para siempre.
1) MAYÚSCULA DE ORACIÓN EN ES. El prompt no decía nada de capitalización, solo
«write in SPANISH». Un modelo entrenado en inglés escribe Title Case en
cuanto le pides un «SEO title»: por eso hubo que corregir a mano NOVENTA Y
UN campos del blog ES.
Va en el prompt y NO en un validador, y esto es una decisión medida, no una
comodidad. Distinguir «Lo que Revelan» (mal) de «el Roswell de Pennsylvania»
(bien) exige saber qué palabra es nombre propio. Probé dos detectores
deterministas contra el corpus real antes de escribir ninguno:
- por proporción de palabras capitalizadas: 100% de falsos positivos en
títulos densos en topónimos («Kecksburg 1965: el Roswell de Pennsylvania»
da 2/2)
- por vocabulario en minúscula del corpus: se deja LA MITAD de los malos
(«Ocultan» nunca aparece en minúscula) y marca «Proyecto Libro Azul» y
«Ejército del Aire» como si fueran errores
Un gate que rechaza borradores válidos es peor que la avería. La red debajo
sigue siendo seo_watch.check_title_case, que sí tiene el corpus delante y
desde hoy corre a diario.
2) EL ARTÍCULO, COMO DATO. Va entre <ARTICLE>…</ARTICLE> y los dos prompts
declaran que lo de dentro no son instrucciones. El cuerpo se redacta a
partir de fuentes scrapeadas: una página con «ignore previous instructions»
entraba en el mensaje sin que nadie lo mirara.
3) EL MOTOR APUNTABA AL BLOG EQUIVOCADO. El comentario de autofill decía que
rules contaba CERO enlaces internos en ES por estar clavado al host del EN,
y que no se podía arreglar por el vendorizado byte a byte. Ya se puede: el
canónico ganó usar_sitio() y el vendorizado se resincronizó — llevaba
desfasado desde el 21-jul, o sea que la CI habría fallado en el próximo
build. _check_con_sitio() apunta el motor al idioma correcto y RESTAURA el
global, con la condición de carrera documentada por si algún día se
paraleliza la generación.
6 tests nuevos (30 en total). Lo que NO se puede verificar aquí: que el prompt
funcione de verdad. Eso solo lo dice el primer borrador ES que genere, y quien
lo va a comprobar es el vigilante diario.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Cada enlace interno que el generador escribía en un borrador español apuntaba
a www.zonadeexclusion.com y se comía un 301. El canónico de los dos blogs está
INVERTIDO —EN es www, ES es el apex—, la misma inversión que provocó el 522 y
de la que ya avisa el wrapper ghst-es.
No es teórico: el enlace www suelto que apareció en los-villares salió de aquí.
El corpus ES está limpio hoy, pero el próximo /generate con enlaces lo habría
reintroducido. Nada río abajo lo paraba: seo_watch solo ve el enlace DESPUÉS de
publicar, y ningún test cubría el host.
Añado los dos tests que faltaban, uno por idioma, para fijar la inversión. El
del ES falla contra el valor viejo con el href entero en el mensaje.
Sin tocar rules.py, así que el vendor-sync sigue en verde. Queda documentado el
matiz que este cambio NO arregla: rules.internal_links cuenta cero enlaces en
ES bajo cualquiera de los dos hosts, porque ese módulo está clavado al host EN
(se vendoriza byte a byte y la CI lo verifica). Inofensivo hoy —
internal_links.too_few no bloquea en borrador.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Al publicar el draft EN en Ghost, el título propuesto se compara contra
los posts published+scheduled del sitio (corpus vía Admin API — incluye
la cola programada, justo el caso del doble Kecksburg del 2026-07-10) con
el topic_collision vendorizado. Si colisiona, el notice de Telegram lleva
un bloque "🚨 Posible colisión de tema" en las tres rutas (live, dryrun,
bare). Nunca bloquea: el draft se crea igual, el humano decide (fusionar,
retitular o enlazar a propósito). Solo lang=en (stopwords inglesas).
Títulos ajenos saneados de entidades Markdown (regla de _safe_send).
Aislamiento: cualquier fallo del check → notice sin bloque y warning en
logs, jamás rompe la publicación.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Re-copia del canónico (chemavx-seo-tools bae1cc9) vía make sync-seo:
añade topic_collision(post, corpus) — detección de posts que se
canibalizan (caso+año, hooks ≥70%, slugs ≥50%). Aditivo: el autofill
del bot no lo usa aún; disponible para integrarlo en /generate.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
find_draft_by_title y fetch_published_menu duplicaban línea a línea el GET
admin de Ghost (token, URL, ClientSession, dict de headers, status check,
resp.json), y el dict Authorization/Accept-Version/Accept-Encoding estaba
copiado en 3 sitios — el incidente brotli ya demostró que el fix por copia
se deja sitios. Ahora los headers se construyen en un único punto
(_admin_headers, con SAFE_ACCEPT_ENCODING de config) y el GET en _admin_get;
publish_draft usa los mismos headers. aiohttp pasa a import de módulo en
generator.py (era 'import aiohttp as _aio' repetido dentro de dos métodos).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Blindaje contra el default de aiohttp: si algún día se reinstala un
backend brotli, las sesiones sin Accept-Encoding volverían a anunciar
br y el decode roto de aiohttp 3.14 rompería la lectura de respuestas
de Ghost (duplicando drafts). Con el header fijado no puede regresar.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Cierra el TODO de ALLOWED_TAGS: el allow-list ES son los 6 tags del
núcleo vivo de zonadeexclusion (Admin API, 2026-07-03), espejo de la
lista EN confirmada: uap, desclasificados, casos-militares,
casos-clasicos, investigacion, casos-espana. Quedan fuera la cola de
tags de 1 post que el modelo inventó antes de constreñir y el duplicado
investigacion-2 (colisión de mayúsculas en Ghost).
La regla anti-legacy 'never use "investigacion"' del prompt pasa a ser
solo-EN: en ES ese slug es el tag canónico del allow-list.
Tests de las funciones puras (_coerce, _system_prompt) por idioma.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
src/seo/rules.py is a byte-for-byte copy of the canonical seo_rules.py
(git.chemavx.xyz/chemavx/chemavx-seo-tools). CI step "Verify vendored SEO engine"
clones the canonical and fails the build on any divergence; `make sync-seo` /
`make check-seo-sync` cover the local-canonical workflow. Nothing imports this in
a runtime path yet — generate_seo_fields integration comes next behind SEO_AUTOFILL.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>