fix(ghost): find_draft_by_title compara títulos normalizados, no igualdad exacta

Si Ghost normaliza el título al guardar (trunca a 255 un H1 largo del LLM,
colapsa whitespace), la igualdad exacta no encontraba el draft recién creado
y la recuperación fallaba justo en el escenario que debe cubrir. _norm_title
aplica el mismo colapso+truncado a ambos lados de la comparación.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
ChemaVX
2026-07-05 15:56:50 +00:00
co-authored by Claude Fable 5
parent 634f38e016
commit b58a3ce118
+13 -2
View File
@@ -385,6 +385,14 @@ def _seo_dryrun_message(ghost: "GhostPublisher", post: dict, seo: dict, inserted
) )
def _norm_title(t: str) -> str:
"""Normaliza un título para compararlo con lo que Ghost almacenó: colapsa
whitespace y trunca a 255 chars (límite de posts.title en Ghost — un H1
largo del LLM puede volver truncado/normalizado, y la igualdad exacta
fallaría justo en los casos que el guard debe cubrir)."""
return " ".join((t or "").split())[:255]
def _parse_ghost_ts(ts: str) -> float | None: def _parse_ghost_ts(ts: str) -> float | None:
"""created_at de la Admin API (ISO 8601, p.ej. 2026-07-04T20:26:00.000+00:00 """created_at de la Admin API (ISO 8601, p.ej. 2026-07-04T20:26:00.000+00:00
o sufijo Z) → epoch. None si no parsea.""" o sufijo Z) → epoch. None si no parsea."""
@@ -438,7 +446,9 @@ class GhostPublisher:
(p.ej. decode roto), el draft existe aunque el caller viera una (p.ej. decode roto), el draft existe aunque el caller viera una
excepción. Con `since` (epoch) solo casan drafts creados en este excepción. Con `since` (epoch) solo casan drafts creados en este
intento — un draft viejo del mismo título de una run anterior NO cuenta intento — un draft viejo del mismo título de una run anterior NO cuenta
como "ya publicado". Best-effort: None si no hay match o si falla.""" como "ya publicado". La comparación usa _norm_title (whitespace
colapsado + truncado a 255) para sobrevivir la normalización de Ghost.
Best-effort: None si no hay match o si falla."""
import aiohttp as _aio import aiohttp as _aio
token = self._make_token() token = self._make_token()
url = ( url = (
@@ -458,8 +468,9 @@ class GhostPublisher:
if resp.status != 200: if resp.status != 200:
return None return None
data = await resp.json() data = await resp.json()
wanted = _norm_title(title)
for p in data.get("posts", []): for p in data.get("posts", []):
if p.get("title") != title: if _norm_title(p.get("title") or "") != wanted:
continue continue
if since is not None: if since is not None:
created = _parse_ghost_ts(p.get("created_at") or "") created = _parse_ghost_ts(p.get("created_at") or "")