Commit Graph
10 Commits
Author SHA1 Message Date
ChemaVXandClaude Opus 5 8b81ef87e4 seo: arreglar la capitalización ES en el ORIGEN, y el motor apuntando al blog malo
Build & Deploy ResearchOwl / build-and-push (push) Successful in 9s
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>
2026-07-29 17:28:56 +00:00
ChemaVXandClaude Opus 5 3d8cba6567 ghost: resolver los tags por ID, que mandarlos por nombre partía el ES en dos
ALLOWED_TAGS son SLUGS y Ghost casa los tags de un post por NOMBRE. Mandarlos
como {"name": slug} funcionaba en EN por casualidad — allí los tags se llaman
igual que su slug ("military-cases") — y en ES rompía: no hay ningún tag
llamado "casos-militares" (se llama "Casos Militares"), así que Ghost creaba
uno nuevo con ese nombre y, con el slug ya pillado, lo dejaba en
`casos-militares-2`.

Encontrado el 2026-07-29 al preparar el hub: 5 tags duplicados, 7 posts
repartidos entre dos archivos flacos cada uno, y los cinco `-2` ofrecidos a
Google en sitemap-tags.xml. Los datos ya están fusionados en Ghost; esto es
para que no vuelva.

Se resuelve el slug a ID contra la Admin API, que es lo único no ambiguo justo
en el punto donde falla la ambigüedad. Un slug que no exista se DESCARTA con
aviso en vez de crearse: la lista es cerrada, así que no existir significa que
está mal escrita, y crear el tag es exactamente el bug. Si no resuelve nada, o
si Ghost no contesta, se cae al comportamiento anterior antes que publicar un
post sin ninguna categoría.

7 tests nuevos, incluido uno para EN — donde el bug era invisible por la
coincidencia nombre==slug y el resolutor no debe depender de ella.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 16:22:14 +00:00
ChemaVXandClaude Opus 4.8 6029bebae5 fix(seo): el canónico del ES es el APEX, no www
Build & Deploy ResearchOwl / build-and-push (push) Successful in 10s
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>
2026-07-21 15:47:39 +00:00
ChemaVXandClaude Fable 5 dadc030d16 feat(seo): aviso de colisión de tema en /generate blog en
Build & Deploy ResearchOwl / build-and-push (push) Successful in 7s
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>
2026-07-10 11:17:33 +00:00
ChemaVXandClaude Fable 5 fbe8ae1885 fix(scraper): _unwrap_news_link normaliza //url y descarta apiclick sin URL usable
Un ?url= protocol-relative (//publisher.com/...) caía por el startswith('http')
y devolvía el link apiclick.aspx crudo, que pasaba blacklist y relevancia y
acababa raspado como página de redirect/consent de Bing. Ahora se antepone
https: a los protocol-relative y, si no se puede extraer URL válida de un
apiclick, se devuelve '' para que el caller lo salte.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-05 15:58:40 +00:00
ChemaVXandClaude Fable 5 a23810b9cc feat(scraper): seed de noticias Bing News RSS tras ENABLE_NEWS_SEED (off)
Build & Deploy ResearchOwl / build-and-push (push) Successful in 7s
Nuevo _seed_news: registra el feed de Bing News como fuente rss y
_extract_rss (F3) siembra sus entries — sin lógica de parseo duplicada.
_unwrap_news_link extrae la URL real del publisher del ?url= de
apiclick.aspx.

Bing y no Google News como pedía el plan: verificado en vivo que los
links de news.google.com/rss/articles/ acaban en muro de consent (UE) y
el id AU_yqL solo se resuelve vía batchexecute interno — habría sembrado
URLs muertas. Los de Bing llevan la URL real en texto plano.

Flag off por defecto: se activará deliberadamente vía deployment.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 10:59:00 +00:00
ChemaVXandClaude Fable 5 f210b34e5f feat(scraper): F3 — extractor RSS + detección de feeds con word-boundary
Build & Deploy ResearchOwl / build-and-push (push) Successful in 7s
detect_source_type ya clasificaba 'rss' pero no había rama en
_process_source: los feeds caían a _extract_web y trafilatura no saca
nada del XML (se descartaban por cortos). Ahora _extract_rss parsea el
feed (feedparser en hilo, como el monitor de noticias) y siembra sus
entries como fuentes nuevas: filtro de relevancia por título/URL,
blacklist, dedupe, cap de 20 y respeto de max_depth. El feed en sí se
marca skipped — es puro descubrimiento, no tiene contenido que trocear.

De paso, la detección pasa de substring puro (clasificaba /feedback y
/atomic como rss) a RSS_RE con límites de palabra. Tests de ambos.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 15:43:50 +00:00
ChemaVXandClaude Fable 5 19efc70539 feat(seo): fijar vocabulario de tags ES en el autofill
Build & Deploy ResearchOwl / build-and-push (push) Successful in 6s
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>
2026-07-03 07:52:26 +00:00
ChemaVXandClaude Opus 4.8 94dc0316f9 chore: add .gitignore y dejar de trackear bytecode .pyc
Build & Deploy ResearchOwl / build-and-push (push) Successful in 1m8s
- .gitignore para Python, .env, *.db y artefactos de editor/OS
- git rm --cached de todos los __pycache__/*.pyc previamente trackeados

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 14:38:46 +00:00
ChemaVX ba08536337 feat: initial ResearchOwl
Build & Deploy ResearchOwl / build (push) Failing after 1m38s
2026-04-27 13:49:07 +00:00