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:
co-authored by
Claude Opus 4.8
parent
f9f90f8999
commit
7b92079c41
+35
-7
@@ -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
|
documented)" section so the headline "issues to fix" count reflects only
|
||||||
genuinely-actionable items. Each waiver carries its REASON (no silent skips).
|
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:
|
Scope:
|
||||||
- A waiver applies ONLY to the named rule family for the named slug. Any OTHER
|
- 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 = {
|
EXCEPTIONS = {
|
||||||
|
"en": {
|
||||||
# internal_links < 2 — these posts are intentionally left as-is because there
|
# 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
|
# is no honest 2nd internal-link target in their prose; forcing one would hurt
|
||||||
# quality. (Decided 2026-06-23.)
|
# quality. (Decided 2026-06-23.)
|
||||||
@@ -83,17 +90,38 @@ EXCEPTIONS = {
|
|||||||
"(Malmstrom, Levelland, Manises) appear nowhere in the text. "
|
"(Malmstrom, Levelland, Manises) appear nowhere in the text. "
|
||||||
"(Reviewed 2026-07-18.)",
|
"(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):
|
def accepted_reason(rule, slug, site="en"):
|
||||||
"""Return the documented waiver reason if (rule, slug) is an accepted
|
"""Devuelve la razón documentada si (rule, slug) está waiveado EN ESE SITIO.
|
||||||
exception, else None.
|
|
||||||
|
|
||||||
Matches a rule-family key to a Violation whose `rule` is the key itself or
|
Empareja la familia de reglas con una Violation cuyo `rule` sea la clave
|
||||||
"<key>.<detail>" (e.g. family "internal_links" matches "internal_links.too_few").
|
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:
|
if (rule == family or rule.startswith(family + ".")) and slug in slugs:
|
||||||
return slugs[slug]
|
return slugs[slug]
|
||||||
return None
|
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)
|
||||||
|
|||||||
+4
-4
@@ -89,10 +89,10 @@ def fetch(slug, cli):
|
|||||||
sys.exit(f"ERROR: no encuentro el post '{slug}' en {cli}")
|
sys.exit(f"ERROR: no encuentro el post '{slug}' en {cli}")
|
||||||
|
|
||||||
|
|
||||||
def violations_of(post):
|
def violations_of(post, site):
|
||||||
out = []
|
out = []
|
||||||
for v in R.check_post(post):
|
for v in R.check_post(post):
|
||||||
if v.severity == R.INFO or X.accepted_reason(v.rule, post.get("slug")):
|
if v.severity == R.INFO or X.accepted_reason(v.rule, post.get("slug"), site):
|
||||||
continue
|
continue
|
||||||
out.append(v)
|
out.append(v)
|
||||||
return out
|
return out
|
||||||
@@ -142,7 +142,7 @@ def cmd_prep(a):
|
|||||||
if v:
|
if v:
|
||||||
print(f" {v[:150]}")
|
print(f" {v[:150]}")
|
||||||
|
|
||||||
viol = violations_of(p)
|
viol = violations_of(p, a.site)
|
||||||
print(f"\n─── violaciones vigentes: {len(viol)} ───")
|
print(f"\n─── violaciones vigentes: {len(viol)} ───")
|
||||||
for v in viol:
|
for v in viol:
|
||||||
print(f" [{v.severity}] {v.rule}: {v.message}")
|
print(f" [{v.severity}] {v.rule}: {v.message}")
|
||||||
@@ -199,7 +199,7 @@ def cmd_apply(a):
|
|||||||
print("✓ aplicado")
|
print("✓ aplicado")
|
||||||
|
|
||||||
after = fetch(a.slug, cfg["cli"])
|
after = fetch(a.slug, cfg["cli"])
|
||||||
viol = violations_of(after)
|
viol = violations_of(after, a.site)
|
||||||
if viol:
|
if viol:
|
||||||
print(f"\n⚠ el post SIGUE con {len(viol)} violación(es):")
|
print(f"\n⚠ el post SIGUE con {len(viol)} violación(es):")
|
||||||
for v in viol:
|
for v in viol:
|
||||||
|
|||||||
+3
-3
@@ -216,14 +216,14 @@ def check_sitemap():
|
|||||||
return len(urls), malos
|
return len(urls), malos
|
||||||
|
|
||||||
|
|
||||||
def check_rules(posts):
|
def check_rules(posts, site):
|
||||||
"""Violaciones accionables (excluye waivers e INFO), agrupadas por regla."""
|
"""Violaciones accionables (excluye waivers e INFO), agrupadas por regla."""
|
||||||
out = {}
|
out = {}
|
||||||
for p in posts:
|
for p in posts:
|
||||||
if p.get("status") != "published":
|
if p.get("status") != "published":
|
||||||
continue
|
continue
|
||||||
for v in R.check_post(p):
|
for v in R.check_post(p):
|
||||||
if v.severity == R.INFO or X.accepted_reason(v.rule, p["slug"]):
|
if v.severity == R.INFO or X.accepted_reason(v.rule, p["slug"], site):
|
||||||
continue
|
continue
|
||||||
out.setdefault(v.rule, []).append(p["slug"])
|
out.setdefault(v.rule, []).append(p["slug"])
|
||||||
return out
|
return out
|
||||||
@@ -287,7 +287,7 @@ def run_site(site):
|
|||||||
roto, nocanon, prematuro = check_links(posts)
|
roto, nocanon, prematuro = check_links(posts)
|
||||||
envejecido = check_link_aging(posts)
|
envejecido = check_link_aging(posts)
|
||||||
n_sm, sm_malos = check_sitemap()
|
n_sm, sm_malos = check_sitemap()
|
||||||
reglas = check_rules(posts)
|
reglas = check_rules(posts, site)
|
||||||
|
|
||||||
body = build_report(roto, nocanon, prematuro, envejecido, n_sm, sm_malos, reglas)
|
body = build_report(roto, nocanon, prematuro, envejecido, n_sm, sm_malos, reglas)
|
||||||
pub = sum(1 for p in posts if p.get("status") == "published")
|
pub = sum(1 for p in posts if p.get("status") == "published")
|
||||||
|
|||||||
Reference in New Issue
Block a user