Commit Graph
17 Commits
Author SHA1 Message Date
ChemaVX b0d5e1bd0c seo_watch: check de mayúsculas en el ES + arregla los checks de enlaces del ES
El check nuevo detecta encabezados en Title Case inglés, que en español no se
usa. Solo corre sobre el ES (en el EN es lo correcto). Umbral de 2 palabras
sospechosas por encabezado: con 1 sola, un nombre propio que falte en las
listas daría un falso positivo. Verificado en los dos sentidos — 0 avisos
sobre los 271 encabezados del ES ya limpios, 208 sobre el corpus de ayer, 0
sobre el corpus EN. Hace falta porque la fuga es intermitente: el 2026-07-21
había 219 encabezados sucios en 23 de 29 posts, pero otros del mismo mes
salieron limpios, así que no basta con limpiar una vez.

Y de paso, un agujero que apareció leyendo el código para meter el check:
internal_urls() tenía theexclusionzone.com escrito a fuego. Desde que el
vigilante se extendió al ES (ayer), los checks de enlaces rotos, no canónicos
y prematuros buscaban en el corpus español un dominio que no aparece en él —
o sea, veían 0 enlaces y no podían fallar nunca. Ahora el dominio sale de
R.SITE_HOST, como el resto.

Nada más arreglarlo encontró su primer hallazgo real: los-villares enlazaba a
www.zonadeexclusion.com, y en el ES el canónico es el apex (al revés que en el
EN). Salto 301 evitable, ya corregido en Ghost.
2026-07-21 07:41:18 +00:00
ChemaVX ea6c24927a seo_exceptions: waivers del par Roswell ES + arregla collisions()
El par Roswell del blog ES se resolvió por la vía que propone la propia regla
(«retitle to a distinct angle» + «interlink deliberately»), no fusionando: el
de mayo es el relato de referencia (3 clics / 152 impresiones en 3 meses) y el
de julio cubre otra cosa — el vídeo de 2025 de los Archivos Nacionales, el
diario de Marcel y la confesión de Haut — pero se presentaba con título de
artículo general y se llevó 1 impresión en 9 días. Retitulado y enlazados en
ambos sentidos; siguen compartiendo caso y año, así que topic.collision va a
avisar siempre: waiver documentado.

Y de paso un fallo mío de ayer: collisions() contaba una vez por FAMILIA en
vez de por SITIO, así que un post con waivers en dos familias del mismo blog
salía como colisión entre sitios. Este par lo destapó. Verificado en los dos
sentidos: vacío ahora, y detecta el duplicado si se inyecta a mano.
2026-07-21 07:27:50 +00:00
ChemaVX 31c9d3e7b8 seo_rules: el detector de colisiones ya entiende español
El motor se calibró con el corpus inglés y al estrenarlo en zonadeexclusion
daba 6 avisos, 4 de ellos falsos: «que», «los» y «del» pasaban el filtro de
3 caracteres y contaban como identidad de caso. Kenneth Arnold 1947 «colisionaba»
con Roswell 1947 sin compartir más que el año y un artículo determinado.

Dos cambios, y van juntos a propósito:

- Stopwords ES (glue + genéricos del dominio, espejo del bloque inglés).
- _deaccent en el tokenizador: [a-z0-9]+ PARTE en cualquier acento, así que
  "Pentágono" era {pent, gono} y "Fenómenos" era {fen, menos} — basura de 3
  caracteres que ninguna lista de stopwords puede cubrir, y que además nunca
  casaba con el slug de Ghost (que ya viene sin acentos). Sin esto, la mitad
  de las stopwords nuevas no llegarían a aplicarse.

Medido sobre los dos corpus antes de tocar nada: ES 6 → 2 avisos, EN idéntico
(mismos 4 pares: PURSUE 3/4 y el par Grusch). Peaje asumido: «los» deja de
identificar a Los Alamos en EN, pero «alamos» sigue ahí y el informe no cambia.

Los 2 avisos ES que quedan son reales: dos artículos publicados sobre Roswell
1947 sin canonical entre ellos, la misma canibalización que ya arreglamos en
el EN con el par Grusch. Queda pendiente de decisión editorial.
2026-07-21 07:15:40 +00:00
ChemaVXandClaude Opus 4.8 7b92079c41 seo_exceptions: ámbito por sitio — los dos blogs, independientes de verdad
Los waivers se indexaban SOLO por slug. Hoy no hay colisiones (los slugs ES
van en español), pero nada lo impedía: un slug repetido entre blogs habría
aplicado en silencio la excepción del sitio equivocado, y ese post habría
dejado de auditarse sin que nadie se enterara. Riesgo latente, no activo — pero
'no pasa hoy' no es lo mismo que 'no puede pasar'.

  EXCEPTIONS[site][familia][slug] = razón
  accepted_reason(rule, slug, site='en')

El default 'en' mantiene funcionando a seo_audit.py sin tocarlo (es EN-only).
seo_watch y seo_finish pasan el sitio activo explícitamente.

Añadido collisions(): lista los slugs waiveados en más de un sitio. Debe estar
siempre vacío; si algo aparece ahí, hay una excepción ambigua que revisar.

Verificado: mismo slug waiveado en EN se audita con normalidad en ES; los 16
waivers EN intactos; seo_audit, seo_watch (ambos sitios) y seo_finish (ambos
sitios) siguen dando los mismos resultados que antes del cambio.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 16:16:47 +00:00
ChemaVX f9f90f8999 seo-watch.service: la descripción ya cubre los dos blogs 2026-07-20 16:10:26 +00:00
ChemaVXandClaude Opus 4.8 f772703051 seo_watch: extiende el vigilante al blog ES (--site en|es|both)
El ES nunca se había auditado: no había wrapper hasta hoy (ghst-es). Corre
ambos por defecto y emite un informe con una sección por sitio.

Detalles del diseño:
  * canónico invertido entre sitios (EN www, ES apex) → SITES parametriza url,
    cli y host.
  * seo_rules.SITE_HOST se reasigna en runtime desde use_site(): ese fichero se
    vendoriza byte a byte en ResearchOwl y la CI lo verifica, así que no se
    puede parametrizar en origen.
  * huella de estado por sitio (dict), para que un cambio en uno no reabra la
    notificación del otro por accidente.
  * un sitio caído no tumba el informe del otro: run_site va en try/except y
    el error se reporta como hallazgo de ese sitio.

Primera pasada real: EN 0 hallazgos; ES 152 violaciones del auditor (28 sin
alt-text — todos —, 27 con enlazado flojo, 21 sin OG/Twitter). Ninguna dispara
Telegram: las violaciones de reglas son inventario, no regresión — solo avisan
los enlaces rotos/no canónicos/prematuros/envejecidos y el sitemap.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 16:09:45 +00:00
ChemaVXandClaude Opus 4.8 34263469e4 seo_finish: soporte para el sitio ES (--site en|es)
El blog ES no tenía forma de llegar desde las herramientas: solo existía el
wrapper ghst-en. Creado ghst-es (gemelo, namespace zona-exclusion, token
efímero del pod, nada en disco) y parametrizado seo_finish.

Ojo al canónico, que va al revés en cada sitio: EN es www, ES es el apex.

seo_rules.SITE_HOST está fijado a EN y NO se parametriza ahí a propósito: ese
fichero se vendoriza byte a byte dentro de ResearchOwl y la CI lo verifica.
Se reasigna en tiempo de ejecución desde use_site(), que deja intacto el
contrato compartido.

Los ficheros de trabajo (imagen convertida, volcado de texto) y los backups
llevan ahora prefijo de sitio, para que un mismo slug en ambos idiomas no se
pise.

Verificado contra los dos sitios: ES detecta las 8 violaciones del borrador
nuevo (todas por dryrun, que ya no escribe SEO) y EN sigue dando 0 en el
artículo belga publicado hoy.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 16:06:43 +00:00
ChemaVXandClaude Opus 4.8 8c83fb81fc waiver: belgium-1989-1990 (remate previo a publicar)
Primer artículo pasado por el flujo nuevo (generar → revisión editorial →
imagen → remate). 1 enlace (→ gimbal-gofast, por la referencia a los vídeos
de la Navy); la prosa no nombra ningún otro caso del corpus: la oleada belga
se narra con sus propias fuentes (SOBEPS, De Brouwer, el memo de la DIA).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 11:24:58 +00:00
ChemaVXandClaude Opus 4.8 2833144653 seo_watch: detecta enlaces envejecidos (check 4)
El corpus crece 2 posts/semana, así que un enlace apunta al mejor destino que
existía el día en que se escribió — y envejece. Caso real que motivó el check
(2026-07-18): levelland enlazaba el anchor 'Project Blue Book' al artículo de
AARO porque cuando se generó (30-jun) el de Blue Book no existía (1-jul).
No fue un fallo del generador: fue el mejor destino disponible, un día antes
de que apareciera el correcto.

Heurística deliberadamente conservadora, por la misma razón que el resto del
job: un vigilante desatendido que grita en falso se acaba ignorando.
  * solo marca si el anchor contiene TODOS los tokens distintivos de otro post
  * no marca si el anchor ya casa con el destino actual (el enlace está bien)
  * ignora posts con <2 tokens distintivos — p.ej. roswell-1947 se reduce a
    {roswell}, que casa con demasiadas cosas

Test de regresión hecho contra los datos reales pre y post arreglo: caza
levelland antes, 0 hallazgos después (incluido el falso positivo de yellow-sea,
cuyo anchor menciona 'roswell' pero apunta correctamente al post del vídeo).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 10:50:06 +00:00
ChemaVXandClaude Opus 4.8 dc0c1cfa73 levelland: repunta el enlace a su destino correcto + waiver
El enlace llevaba el anchor 'Project Blue Book' pero apuntaba al artículo de
AARO. No era un error del generador: Levelland se creó el 30-jun y el artículo
de Blue Book no existía hasta el 1-jul, así que el autofill enlazó al destino
más cercano disponible. Al día siguiente quedó obsoleto.

Arreglado: el enlace ahora apunta a project-blue-book (anchor y destino
coinciden) y se ha desenlazado el duplicado que se añadió antes hoy sobre la
segunda mención. Queda 1 enlace, de ahí el waiver: la prosa no nombra ninguna
otra entidad con artículo propio (lo demás es Hynek y 'ball lightning').

Blog completo (30 publicados + 9 programados): 0 violaciones reales.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 10:46:16 +00:00
ChemaVXandClaude Opus 4.8 85a78c6014 waivers: westall y colares (revisados antes de publicarse)
Revisión preventiva de la cola programada: 4 posts traían enlazado flojo.
Dos tenían enlace honesto y se aplicó (Project Blue Book, publicado el 1-jul,
así que no hay riesgo de enlace prematuro):
  - shag-harbour (17-ago): '…led to the end of the U.S. Air Force's Project
    Blue Book' → project-blue-book
  - levelland (20-ago): '…led to the closing of Project Blue Book' → idem

Trampa evitada en levelland: su PRIMERA mención de 'Project Blue Book' ya era
el anchor de un enlace a AARO. Insertar ahí habría anidado <a> dentro de <a>.
Se enlazó la segunda mención, que estaba libre; verificado que no hay anidados.

Los otros dos van a waiver por autocontenidos:
  - westall: no nombra ningún otro caso del corpus.
  - colares: solo cita Varginha (ya enlazado) y 'Operation Saucer', que es su
    propia operación y no tiene artículo.

Cola programada: 0 violaciones reales en los 9 posts. El auditor no saltará
según se vayan publicando.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 10:40:32 +00:00
ChemaVXandClaude Opus 4.8 162f8032f3 seo_finish: remate SEO posterior a la revisión editorial (Tool E)
El SEO es DERIVADO del artículo. Hoy ResearchOwl lo genera al crear el
borrador, pero la revisión editorial cambia el texto después → el SEO queda
obsoleto y hay que rehacerlo a mano en cada post. La solución no es parchear
sino re-derivarlo del texto FINAL, y ese es el hueco que cubre esta tool.

Además el alt-text SOLO puede escribirse aquí: cuando autofill.py genera el
SEO la imagen destacada aún no se ha elegido (de ahí su
'feature_image_alt': ""  # human adds later). Ese paso humano no ocurría
nunca — los 9 posts publicados entre el 29-jun y el 16-jul salieron sin alt.

  prep <slug>   paquete de revisión: descarga la imagen destacada y la
                convierte a PNG (Ghost sirve webp), vuelca el texto final,
                lista el SEO actual con longitudes y las violaciones. READ-ONLY.
  apply <slug>  aplica un patch vía ghst-en, con backup previo, y RE-VALIDA
                después (si sigue incumpliendo, lo dice).

Guardas por límites duros, para convertir en error explicado lo que Ghost
devuelve como 422 mudo:
  * feature_image_alt > 191 car. (límite de Ghost, descubierto a base de 422)
  * meta_description > 145 / meta_title > 60 (seo_rules)

El criterio (redactar la meta, describir la imagen) lo pone quien invoca —
humano o Claude vía hermes. El script hace lo mecánico y lo verifica.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 10:14:52 +00:00
ChemaVXandClaude Opus 4.8 c4dba06fb1 waivers de enlazado: tassili, missing-scientists y el adelanto de Grusch
Cierra las 3 incidencias restantes de internal_links.too_few. Ninguno de los
tres nombra en su prosa una entidad con artículo propio (criterio de siempre:
entidad nombrada en el texto, no afinidad temática):

- tassili-najjer: arqueología sahariana, autocontenida.
- missing-scientists: trata de la cadena mediática, no de casos UAP.
- grusch-capitol-june-9 (adelanto): además lleva canonical_url a la
  investigación completa, así que sus señales se consolidan allí; forzar
  enlaces en un preview superado no aporta.

El cuarto caso (roswell-341) SÍ tenía enlace honesto y se aplicó:
'the Air Force's official Roswell explanation' → roswell-1947-ufo-incident.
Nota técnica: ese post usa nodos lexical nativos (no bloques HTML como
Wilson-Davis), así que hubo que partir el nodo de texto e insertar un nodo
'link' con el mismo esquema que el enlace preexistente.

Con esto el auditor queda a CERO violaciones accionables (venía de 20), lo que
hace que cualquier aviso futuro de seo_watch signifique algo de verdad.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 09:04:05 +00:00
ChemaVXandClaude Opus 4.8 744ed9fceb seo_watch: vigilante semanal de regresiones SEO (Tool D)
Chequeos deterministas sobre el sitio EN, READ-ONLY, con aviso a Telegram
solo cuando hay hallazgos Y algo cambia respecto a la ejecución anterior
(un informe semanal de 'todo bien' se acaba ignorando):

  1. enlaces internos rotos (destino inexistente)
  2. enlaces internos no canónicos (apex/http/sin barra → salto 301 evitable)
  3. enlaces PREMATUROS — destino programado para DESPUÉS del post que lo
     enlaza. Caso real detectado el 2026-07-18: el post Wilson-Davis (3-ago)
     enlazaba a MJ-12 (13-ago) → habría sido 404 durante 10 días justo en la
     ventana de rastreo del post recién publicado.
  4. sitemap: toda URL listada debe dar 200 directo
  5. violaciones accionables del motor de reglas (respeta seo_exceptions)

Por qué no lleva Gemini: el 2026-07-18 se midió con datos reales — de 6
sugerencias de enlazado interno, 0 cumplían el estándar 'entidad nombrada en
la prosa', y no vio las 3 entidades que había servidas en una sola frase.
Es fiable extrayendo (12/12 slugs correctos, cero alucinaciones) pero no
juzgando. Se deja fuera del job hasta tener un uso donde aporte de verdad.

Unidades systemd incluidas: timer semanal (lunes 05:30 UTC, antes de la
publicación de las 06:00). Instaladas en /etc/systemd/system/ y habilitadas.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-19 15:35:29 +00:00
ChemaVXandClaude Opus 4.8 b5ce7638bd waivers de internal_links: tehran-1976 y chile-cefaa
- tehran-1976-iranian-f4-ufo-incident: 0 enlaces, prosa autocontenida — solo
  nombra a Klass, Jafari y el informe de la DIA; ningún caso con artículo
  propio. Los candidatos temáticos (Malmstrom, Levelland, Manises) no
  aparecen en el texto, así que enlazarlos sería forzado. Revisado 2026-07-18.
- chiles-military-...-cefaas: waiver que ya estaba en el working tree sin
  commitear (1 enlace → pursue-release-3; caso chileno autocontenido).

Efecto en el auditor: internal_links accionables 9 → 7 (el otro punto lo
cierra el post Wilson-Davis, que pasa a 2 enlaces reales).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-18 21:29:28 +00:00
ChemaVXandClaude Fable 5 bae1cc949c feat: check de colisión de tema (topic.collision) en rules + validate
Dos posts sobre el mismo caso se canibalizan en la SERP (2026-07-10: se
publicó un segundo Kecksburg con otro ya programado, y había un título
casi gemelo del de Malmstrom en la cola). Nueva función pura
topic_collision(post, corpus) en seo_rules.py — corpus-aware, fuera de
RULES — que dispara por: caso+año compartidos (años < 2020; los de la
era de noticias no son identidad de caso), hooks de título ≥70%
similares (calibrado: el par nuclear da 0.742, el par legítimo más
cercano 0.65), o slugs ≥50% solapados. seo_validate.py obtiene el corpus
via ghst-en (--limit all) y lo reporta como grupo topic_collision;
--no-collision lo omite para follow-ups intencionados.

Calibrado contra los 36 posts reales: dispara solo el duplicado
Kecksburg (HIGH) y el par Grusch preview/evento (MED, intencionado).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 11:06:39 +00:00
ChemaVXandClaude Opus 4.8 beae7c154a Initial commit: SEO rule engine, auditor, validator, exceptions, mirror
Versioning ~/seo-tools as critical SEO infrastructure (was home-dir only).
Canonical home of seo_rules.py — ResearchOwl will vendor a copy with a CI diff
guard against this repo. No secrets; Ghost access is via the ghst-en wrapper.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 08:01:44 +00:00