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>
This commit is contained in:
ChemaVX
2026-07-20 16:16:47 +00:00
co-authored by Claude Opus 4.8
parent f9f90f8999
commit 7b92079c41
3 changed files with 42 additions and 14 deletions
+35 -7
View File
@@ -7,7 +7,13 @@ 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[rule_family][slug] = "documented reason"
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
@@ -23,6 +29,7 @@ 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.)
@@ -83,17 +90,38 @@ EXCEPTIONS = {
"(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):
"""Return the documented waiver reason if (rule, slug) is an accepted
exception, else None.
def accepted_reason(rule, slug, site="en"):
"""Devuelve la razón documentada si (rule, slug) está waiveado EN ESE SITIO.
Matches a rule-family key to a Violation whose `rule` is the key itself or
"<key>.<detail>" (e.g. family "internal_links" matches "internal_links.too_few").
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.items():
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)