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

128 lines
7.0 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": {},
}
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."""
from collections import Counter
c = Counter()
for fams in EXCEPTIONS.values():
for slugs in fams.values():
c.update(slugs.keys())
return sorted(s for s, n in c.items() if n > 1)