From b58a3ce118dc84fb58c4c6b8140242cf25731fe3 Mon Sep 17 00:00:00 2001 From: ChemaVX Date: Sun, 5 Jul 2026 15:56:50 +0000 Subject: [PATCH] =?UTF-8?q?fix(ghost):=20find=5Fdraft=5Fby=5Ftitle=20compa?= =?UTF-8?q?ra=20t=C3=ADtulos=20normalizados,=20no=20igualdad=20exacta?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- src/generator/generator.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/generator/generator.py b/src/generator/generator.py index 301b815..a0fab1b 100644 --- a/src/generator/generator.py +++ b/src/generator/generator.py @@ -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: """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.""" @@ -438,7 +446,9 @@ class GhostPublisher: (p.ej. decode roto), el draft existe aunque el caller viera una 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 - 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 token = self._make_token() url = ( @@ -458,8 +468,9 @@ class GhostPublisher: if resp.status != 200: return None data = await resp.json() + wanted = _norm_title(title) for p in data.get("posts", []): - if p.get("title") != title: + if _norm_title(p.get("title") or "") != wanted: continue if since is not None: created = _parse_ghost_ts(p.get("created_at") or "")