Files
chemavx-seo-tools/seo_exceptions.py
T
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

161 lines
8.9 KiB
Python

"""
Accepted SEO exceptions — consciously-waived findings for SPECIFIC posts.
This is an A-side (auditor) REPORTING filter, NOT a rule change. The shared rule
engine (seo_rules.py) still CHECKS every rule on every post; the auditor simply
moves a matching violation into a separate "Accepted exceptions (won't fix —
documented)" section so the headline "issues to fix" count reflects only
genuinely-actionable items. Each waiver carries its REASON (no silent skips).
Structure: EXCEPTIONS[site][rule_family][slug] = "documented reason"
El ámbito por SITIO ("en"/"es") es deliberado: los waivers se indexaban solo por
slug, así que 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 enterase. Hoy no hay colisiones (los slugs ES van en español), pero
nada lo impedía. Con el ámbito, los dos sitios son independientes de verdad.
Scope:
- A waiver applies ONLY to the named rule family for the named slug. Any OTHER
rule the same post violates still surfaces as a real issue.
- A waiver is matched against a Violation whose `rule` equals the family key or
starts with "<family>." — so "internal_links" covers "internal_links.too_few".
- If a post later stops violating the rule (e.g. a link is added and it clears
the threshold), there's simply no violation to waive — the waiver is moot.
Tool C (seo_validate.py, pre-publish validator) deliberately does NOT consult
this registry: exceptions are per-EXISTING-post decisions; a brand-new draft is
held to the full standard.
"""
EXCEPTIONS = {
"en": {
# internal_links < 2 — these posts are intentionally left as-is because there
# is no honest 2nd internal-link target in their prose; forcing one would hurt
# quality. (Decided 2026-06-23.)
"internal_links": {
"roswell-1947-ufo-incident":
"1 link (Spielberg); no 2nd entity in prose maps to an article — "
"forcing a link would hurt quality.",
"manises-1979-spain-ufo-incident":
"1 link (Canary Islands); no 2nd entity in prose maps to an article.",
"betty-barney-hill-1961-abduction":
"1 link (alien abduction → travis-walton, thematic); no 2nd honest target.",
"malmstrom-1967-ufo-nuclear-missiles-incident":
"1 link (Grusch); the only entity named in the prose.",
"rendlesham-forest-incident-1980":
"0 links; genuinely isolated — no entity in prose maps to one of our articles.",
"phoenix-lights-1997":
"0 links; only thematic candidates are stretches — left as-is.",
"canary-islands-1976-ufo-sighting":
"1 link (reciprocal → manises); no honest 2nd entity in prose — "
"Spanish case, self-contained.",
"chiles-military-declassified-uap-files-cefaas-groundbreaking-investigation-of-unexplained-aerial-phenomena":
"1 link (→ pursue-release-3, the 294 files); no 2nd verbatim entity "
"in prose — self-contained Chilean case.",
"belgium-1989-1990-nato-fighters-ufo-wave":
"1 link (→ 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). "
"(Revisado 2026-07-20, en el remate previo a publicar.)",
"levelland-1957-texas-ufo-vehicle-interference":
"1 link (→ project-blue-book, la única entidad del corpus nombrada en "
"la prosa; lo demás es Hynek y 'ball lightning', sin artículo propio). "
"El enlace previo a AARO era un destino de conveniencia: 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ó a lo más cercano disponible. Repuntado al "
"destino correcto el 2026-07-18.",
"hundreds-watched-in-broad-daylight-the-westall-ufo-mystery-that-australia-still-cant-explain":
"0 links; la prosa no nombra ningún otro caso del corpus — episodio "
"escolar australiano, autocontenido. (Revisado 2026-07-18, antes de "
"su publicación del 6-ago.)",
"colares-1977-brazil-air-force-ufo-investigation":
"1 link (→ varginha, el otro caso brasileño); lo único más que nombra "
"es 'Operation Saucer', que es su propia operación y no tiene artículo. "
"(Revisado 2026-07-18, antes de su publicación del 24-ago.)",
"tassili-najjer-cave-paintings-ancient-astronaut-theory":
"1 link (→ gimbal-gofast); ningún otro caso del corpus aparece en la "
"prosa — arqueología sahariana, autocontenida. (Revisado 2026-07-18.)",
"missing-scientists-theory-youtube-white-house-2026":
"1 link (→ grusch-capitol); trata de la cadena mediática, no de casos "
"UAP — no hay 2ª entidad del corpus en la prosa. (Revisado 2026-07-18.)",
"grusch-capitol-june-9-2026-uap-disclosure":
"1 link (→ la investigación completa, que es lo correcto). Es el ADELANTO "
"del evento, con canonical_url apuntando a esa investigación: sus señales "
"se consolidan allí, así que forzar enlaces aquí no aporta. "
"(Revisado 2026-07-18, ver project-en-seo-recovery.)",
"tehran-1976-iranian-f4-ufo-incident":
"0 links; prose names no case we have an article for (only Klass, "
"Jafari, the DIA report) — self-contained. Thematic candidates "
"(Malmstrom, Levelland, Manises) appear nowhere in the text. "
"(Reviewed 2026-07-18.)",
},
},
# El blog ES se auditó por primera vez el 2026-07-20 (al crear ghst-es) y
# arrastra backlog: 28 posts sin alt-text, 27 con enlazado flojo. Es deuda
# acumulada por no haberse revisado nunca, NO regresión — se irá waiveando o
# arreglando post a post, con la misma vara de medir que el EN.
"es": {
"internal_links": {
"roswell-1947-preguntas-sin-respuesta-78-anos":
"1 enlace (→ el Roswell de mayo, sobre «Ese relato ya lo cubrimos», "
"que es exactamente lo que dice la prosa). No hay 2ª entidad honesta: "
"el artículo solo nombra a Marcel, el Proyecto Mogul y Walter Haut, y "
"ninguno tiene artículo propio. (Revisado 2026-07-21.)",
},
# Los dos Roswell del ES comparten caso y año, así que topic.collision va a
# seguir avisando siempre. 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 (2.278 palabras, 3 clics / 152 impr.)
# y el de julio cubre otra cosa — el vídeo de 2025 de los Archivos Nacionales,
# el diario de Marcel y la confesión póstuma de Haut. El 2026-07-21 el de julio
# se retituló a su ángulo real (dejó de presentarse como "Roswell 1947: El
# Accidente OVNI...") y se enlazaron entre sí en ambos sentidos.
"topic": {
"roswell-1947-el-misterio-que-cambio-la-historia-de-la-ufologia":
"Complementario, no duplicado: es el relato de referencia y enlaza al "
"de julio como «las preguntas que 78 años no han resuelto». "
"(Decidido 2026-07-21 con datos de Search Console.)",
"roswell-1947-preguntas-sin-respuesta-78-anos":
"Complementario, no duplicado: retitulado a su ángulo propio (el vídeo "
"y el diario) y enlaza al de mayo sobre «Ese relato ya lo cubrimos». "
"(Decidido 2026-07-21 con datos de Search Console.)",
},
},
}
def accepted_reason(rule, slug, site="en"):
"""Devuelve la razón documentada si (rule, slug) está waiveado EN ESE SITIO.
Empareja la familia de reglas con una Violation cuyo `rule` sea la clave
misma o "<clave>.<detalle>" (la familia "internal_links" cubre
"internal_links.too_few").
`site` por defecto "en" para no romper a seo_audit.py, que es EN-only.
Quien audite el ES DEBE pasar site="es" explícitamente.
"""
for family, slugs in EXCEPTIONS.get(site, {}).items():
if (rule == family or rule.startswith(family + ".")) and slug in slugs:
return slugs[slug]
return None
def collisions():
"""Slugs waiveados en más de un SITIO. Debe estar SIEMPRE vacío: si algo
aparece aquí, hay una excepción ambigua que revisar a mano.
Cuenta una vez por sitio, no por familia de reglas: un mismo post puede
tener waivers en varias familias del mismo blog sin que eso sea ambiguo
(el par Roswell del ES está waiveado en `internal_links` y en `topic`).
"""
from collections import Counter
c = Counter()
for fams in EXCEPTIONS.values():
site_slugs = set()
for slugs in fams.values():
site_slugs.update(slugs.keys())
c.update(site_slugs)
return sorted(s for s, n in c.items() if n > 1)