capa semántica: enganchada a remate-watch + vigilante de canibalización

1) remate-watch ya no elige los enlaces internos a ojo. El paso 3 del prompt
   pregunta primero a seo_semantic (afines), que le da los publicados más
   parecidos y le marca los que el borrador YA enlaza. Se le dice
   explícitamente que es una sugerencia y no una orden, y que si nada pasa de
   ~0,55 lo diga en el informe en vez de forzar un enlace malo: un enlace
   forzado es peor que ninguno.

   Para que eso funcione, seo_semantic sabe ahora trabajar con un post que NO
   está en el corpus: `ghst post list` sin filtro devuelve solo publicados y
   programados, así que el borrador que remate-watch está preparando no
   aparecía. Se baja suelto por slug y se embebe al vuelo. Verificado por
   equivalencia: los vecinos de un post embebido al vuelo son idénticos, hasta
   el cuarto decimal, a los del mismo post cacheado.

2) canibal-watch: a diario a las 07:00 UTC (una hora después de que publique
   la cola de agosto). Telegram SOLO si hay algo nuevo.

   Solo avisa de pares que además NO se enlazan. Un par muy parecido pero
   cosido con un enlace ya le dice a Google cuál manda; avisar de él es ruido.
   Los enlazados se guardan igual en el estado, porque hacen falta para
   detectar que MÁS TARDE pierdan el enlace (una edición que se lleve por
   delante la jerarquía). Con el filtro, el primer aviso baja de 19 líneas a 5
   accionables.

   Probado por el camino real, arrancándolo con systemctl y no desde una
   shell con el PATH bueno: primera pasada avisa (5 pares), segunda se calla,
   y con una regresión simulada en el estado la detecta.

No escribe en el blog. Coser o fusionar lo decide Jose.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
ChemaVX
2026-07-25 21:06:59 +00:00
co-authored by Claude Opus 5
parent 9e3dfc1039
commit 2512144be5
5 changed files with 202 additions and 4 deletions
+40 -2
View File
@@ -190,11 +190,49 @@ def matriz(regs):
return slugs, M @ M.T
def post_suelto(sitio, slug):
"""Un post por slug, INCLUIDOS los borradores.
Hace falta porque `ghst post list` sin filtro devuelve solo publicados y
programados: el corpus NO tiene borradores. Y el caso de uso principal —
sugerirle enlaces a remate-watch mientras prepara un borrador— es
exactamente ese. Se embebe al vuelo contra el caché, sin guardarlo.
"""
import tempfile
cli = SITIOS[sitio]["cli"]
fd, path = tempfile.mkstemp(suffix=".json", prefix="seo_semantic_")
os.close(fd)
try:
subprocess.run(f'{cli} post list --filter "slug:{slug}" --limit all '
f'--formats html --json > {path}',
shell=True, capture_output=True, text=True, timeout=240)
with open(path) as f:
d = json.load(f)
posts = d.get("posts", d) if isinstance(d, dict) else d
return posts[0] if posts else None
except (json.JSONDecodeError, IndexError):
return None
finally:
os.unlink(path)
def afines(sitio, slug, n, refrescar=False):
regs = carga(sitio, refrescar)
externo = None
if slug not in regs:
sys.exit(f"'{slug}' no está en el corpus de {sitio.upper()} "
f"({len(regs)} artículos). ¿Sitio equivocado?")
p = post_suelto(sitio, slug)
if p is None:
sys.exit(f"'{slug}' no existe en {sitio.upper()} "
f"(ni publicado, ni programado, ni borrador). "
f"¿Sitio equivocado?")
externo = {"vec": embed([texto_de(p)], endpoint())[0],
"enlaza_a": enlaces_de(p, SITIOS[sitio]["base"]),
"estado": p.get("status")}
print(f"{slug}» está en {externo['estado']}, fuera del corpus: "
f"embebido al vuelo)", file=sys.stderr)
regs = dict(regs)
regs[slug] = {**externo, "slug": slug, "titulo": p.get("title"),
"url": f"{SITIOS[sitio]['base']}/{slug}/"}
slugs, S = matriz(regs)
i = slugs.index(slug)
fila = [(S[i][j], slugs[j]) for j in range(len(slugs))