infografía: el verificador no veía un cargo falso, y por ahí salió a la imagen

La infografía de Socorro salió con «Lt. Colonel Hector Quintanilla» cuando
el artículo dice «Major» las cinco veces que lo nombra. El rango NO lo
inventó el modelo que maqueta: venía en el guion, y el verificador lo dejó
pasar por dos motivos independientes.

1. RE_ARRANQUE veía el punto de «Lt.» y daba por hecho que «Colonel» iba en
   mayúscula por empezar frase, así que la eximía de comprobar. La única
   palabra capaz de cazar el error era justo la que se saltaba. Ahora una
   lista de abreviaturas conocidas anula esa exención.

2. _en_articulo comparaba por SUBCADENA: «Lt» daba positivo por estar dentro
   de built, adults y consultant. Pasa a exigir frontera de palabra por la
   izquierda — se conserva la laxitud por la derecha para que «Zamora's» del
   artículo siga casando con «Zamora» del guion.

Y se añade la comprobación que faltaba, cargos_fuera(): los cargos y rangos
se verifican en FRASE ENTERA. El fallo real no fue una palabra inventada
sino una COMBINACIÓN falsa de dos palabras que sí estaban por separado, y
eso un verificador token a token no puede verlo nunca.

Medido antes de dejarlo puesto, sobre los DOS guiones reales de hoy (156
líneas): caza la avería, deja pasar la versión corregida, y cero falsos
positivos de cargo. Por el camino de producción, filtra() tira solo las dos
frases que llevan el rango malo y no toca el resto.

Once tests. Incluye los dos lados: que pode lo que debe y que NO pode lo
que no debe, porque un verificador que se pasa deja el guion cojo y obliga a
rehacerlo a mano.

También: hreflang-parejas.md con los slugs nuevos de los dos remates de hoy
(socorro-1964-zamora-ufo-landing y cash-landrum-1980-demanda-ovni-tribunal-federal),
corregidos por el propio agente del remate al leer la nota que lo pedía.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
ChemaVX
2026-07-30 13:36:34 +00:00
co-authored by Claude Opus 5
parent 91d2de7c4f
commit 1f07e88c12
3 changed files with 215 additions and 5 deletions
+144
View File
@@ -0,0 +1,144 @@
#!/usr/bin/env python3
"""Tests de infografia_brief.py — que el guion no contradiga al artículo.
Todo lo de aquí es la cicatriz de un fallo REAL del 2026-07-30: la infografía
de Socorro salió publicando «Lt. Colonel Hector Quintanilla» cuando el artículo
dice «Major» las cinco veces que lo nombra. El rango no lo inventó el modelo que
maqueta la imagen: venía en el guion, y el verificador lo dejó pasar por DOS
motivos independientes, así que hay dos redes que probar.
python3 -m pytest test_infografia_brief.py -q
python3 test_infografia_brief.py # sin pytest
"""
import os
import sys
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import infografia_brief as I
# Artículo de mentira con la forma del real: nombra a Quintanilla como Major, y
# contiene «consultant» y «built», que es donde se escondía «Lt» por subcadena.
ART = I.pliega(
"The Socorro UFO Landing of 1964 left four depressions and no explanation. "
"Project Blue Book chief, Major Hector Quintanilla, dispatched his "
"scientific consultant, Dr. J. Allen Hynek, to the site. Sergeant Sam "
"Chavez reached the arroyo first. The shack had been built years earlier. "
"Zamora's patrol car was parked on the ridge. Captain Richard T. Holder "
"of White Sands Missile Range arrived that evening."
)
# ────────────── red 1: la abreviatura ya no regala el pase ──────────────
def test_el_punto_de_una_abreviatura_no_exime_a_la_palabra_siguiente():
"""«Lt. Colonel»: RE_ARRANQUE veía el punto de «Lt.» y daba por hecho que
«Colonel» iba en mayúscula por empezar frase, así que NO lo comprobaba. La
única palabra capaz de cazar el error era justo la que se saltaba."""
nombres, _, _ = I.revisa("Blue Book chief Lt. Colonel Hector Quintanilla spoke", ART)
assert "Colonel" in nombres, nombres
def test_un_punto_de_verdad_sigue_eximiendo():
"""La excepción no puede cargarse la regla: detrás de un punto de final de
frase, una mayúscula sí es posicional y no debe marcarse."""
nombres, _, _ = I.revisa("The object left. Nothing else was found.", ART)
assert nombres == [], nombres
# ────────────── red 2: comparar por palabra, no por subcadena ──────────────
def test_un_token_corto_no_se_esconde_dentro_de_otra_palabra():
"""«Lt» daba positivo por estar dentro de *built*, *adults* y *consultant*.
Cuanto más corto el token más fácil colarse — y los tokens cortos son
justamente las abreviaturas de cargo."""
assert not I._en_articulo("Lt", ART)
assert "built" in ART and "consultant" in ART # la trampa sigue ahí
def test_los_plurales_y_genitivos_siguen_valiendo():
"""La comparación se ata solo por la IZQUIERDA a propósito: el artículo dice
«Zamora's» y el guion «Zamora», y eso no es contaminación."""
assert I._en_articulo("Zamora", ART)
assert I._en_articulo("Quintanilla", ART)
# ────────────── red 3: el cargo se comprueba en FRASE ──────────────
def test_un_cargo_falso_se_caza_aunque_sus_palabras_esten_sueltas():
"""El fallo real no fue una palabra inventada sino una COMBINACIÓN falsa.
Token a token no se puede ver nunca: hay que comparar la frase entera."""
assert I.cargos_fuera("Lt. Colonel Hector Quintanilla said so", ART) == \
["Lt. Colonel Hector Quintanilla"]
def test_el_cargo_correcto_no_salta():
assert I.cargos_fuera("Major Hector Quintanilla said so", ART) == []
def test_otros_cargos_reales_del_articulo_no_saltan():
"""Si marcara cargos legítimos, el informe se volvería inservible y se
dejaría de leer — que es como se pierde el aviso de verdad."""
for frase in ("Sergeant Sam Chavez reached the site",
"Captain Richard T. Holder of White Sands",
"his scientific consultant, Dr. J. Allen Hynek"):
assert I.cargos_fuera(frase, ART) == [], frase
def test_la_abreviatura_no_cuenta_como_cargo_distinto():
"""«Lt. Col.» y «Lt Col» son el mismo cargo: si el punto los separase,
cualquier artículo que abrevie distinto daría falso positivo."""
art = I.pliega("Lt. Col. Hector Quintanilla ran the program.")
assert I.cargos_fuera("Lt Col Hector Quintanilla ran it", art) == []
# ────────────── camino de producción: filtra() poda lo justo ──────────────
def _guion(rango):
return {
"titular": "Socorro UFO Landing, 1964",
"subtitulo": "Four depressions and no explanation",
"entradilla": "Sergeant Sam Chavez reached the arroyo first.",
"cronologia": [
{"marca": "within 24 hours",
"texto": f"Blue Book chief {rango} Hector Quintanilla dispatched Hynek."},
{"marca": "within minutes",
"texto": "Sergeant Sam Chavez reached the arroyo first."}],
"paneles": [{"titulo": "The witness", "vinetas": [
"Zamora's patrol car was parked on the ridge",
f"{rango} Hector Quintanilla classified it"]}],
"franja": [],
}
def test_filtra_tira_solo_las_frases_con_el_cargo_falso():
inf = {"avisos": [], "caidas": []}
out = I.filtra(_guion("Lt. Colonel"), ART, inf)
assert len(out["cronologia"]) == 1, out["cronologia"]
assert sum(len(p["vinetas"]) for p in out["paneles"]) == 1
assert out["titular"], "no debía tocar el titular"
assert any("Colonel" in c for c in inf["caidas"]), inf["caidas"]
def test_filtra_no_toca_nada_si_el_cargo_es_el_bueno():
"""La otra mitad: un verificador que poda de más es tan inútil como uno que
no poda — deja el guion cojo y obliga a rehacerlo a mano."""
inf = {"avisos": [], "caidas": []}
out = I.filtra(_guion("Major"), ART, inf)
assert len(out["cronologia"]) == 2, inf["caidas"]
assert sum(len(p["vinetas"]) for p in out["paneles"]) == 2, inf["caidas"]
assert inf["caidas"] == [], inf["caidas"]
if __name__ == "__main__":
fallos = 0
for nombre, fn in sorted(globals().items()):
if not nombre.startswith("test_"):
continue
try:
fn()
print(f"{nombre}")
except AssertionError as exc:
fallos += 1
print(f"{nombre}: {exc}")
print(f"\n{'✓ todo pasa' if not fallos else f'{fallos} fallo(s)'}")
sys.exit(1 if fallos else 0)