Commit Graph
15 Commits
Author SHA1 Message Date
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