fix(short): dos citas en el campo de una son una frase que nadie dijo
Build & Deploy ResearchOwl / build-and-push (push) Successful in 8s
Build & Deploy ResearchOwl / build-and-push (push) Successful in 8s
El comprobador de fundamento une las líneas de `quote` antes de buscarlas, y eso cerró la forma con la que falló Socorro en agosto (`366ded1`): `“LIKE ALUMINUM` + `SMOOTH, NO WINDOWS”` unidas son una sola frase, no aparece en ninguna fuente, y se rechaza. Pero la unión se derrota poniéndole a cada línea su propio par de comillas. Entonces son DOS citas, cada una fundamentada por su lado, y pasa en silencio mientras el fotograma dibuja la frase de nadie. Medido sobre los cinco Shorts generados hoy: le pasaba a dos. Se comprueba la FORMA, no el contenido — dos aperturas son dos citas, diga lo que diga la fuente — y se comprueba en `validate_spec`, o sea que cuesta un reintento y no una revisión humana. Es regla del canal, no de shortsmith: allí renderiza igual. Va como error duro y no como nota editorial porque el fallo que evita es el peor que tiene este sistema: una cita fabricada con material auténtico y atribuida a alguien con nombre y apellidos. La comprobación es universal por nombre de prop, no por plantilla: una plantilla nueva con un `quote` de varias líneas queda cubierta sin tocar nada. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -232,6 +232,48 @@ def _track_map_waypoints_inside(props: dict, path: str) -> list[str]:
|
|||||||
return []
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
#: Comillas de apertura. Una cita bien partida abre UNA vez.
|
||||||
|
_OPENING_QUOTES = "“«„‟"
|
||||||
|
|
||||||
|
|
||||||
|
def _quote_is_one_span(props: dict, path: str) -> list[str]:
|
||||||
|
"""Una `quote` de varias líneas es UN span partido, no dos citas.
|
||||||
|
|
||||||
|
Esta regla NO es de shortsmith: allí renderiza igual. Es del canal, y es de
|
||||||
|
las duras, porque el fallo que evita es el peor que tiene este sistema —
|
||||||
|
una cita fabricada con material auténtico y atribuida a una persona con
|
||||||
|
nombre y apellidos.
|
||||||
|
|
||||||
|
El comprobador de fundamento ya une las líneas antes de buscarlas, y eso
|
||||||
|
cerró la forma con la que falló Socorro en agosto (`“LIKE ALUMINUM` +
|
||||||
|
`SMOOTH, NO WINDOWS”`): unidas son una sola frase, no aparece en ninguna
|
||||||
|
fuente, y se rechaza. Pero la unión se puede derrotar poniéndole a cada
|
||||||
|
línea su propio par de comillas: entonces son DOS citas, cada una
|
||||||
|
fundamentada por su lado, y pasa en silencio — mientras el fotograma dibuja
|
||||||
|
la frase de nadie. Le pasó a la sesión 162 el 2026-08-13 con
|
||||||
|
`“GRAY, LIKE ZINC”` + `“TWO SAUCERS GLUED AT THE RIM”`.
|
||||||
|
|
||||||
|
Por eso se mira la FORMA y no el contenido: dos aperturas son dos citas,
|
||||||
|
diga lo que diga la fuente.
|
||||||
|
"""
|
||||||
|
quote = props.get("quote")
|
||||||
|
if not isinstance(quote, list) or len(quote) < 2:
|
||||||
|
return []
|
||||||
|
joined = " ".join(str(line) for line in quote)
|
||||||
|
openings = sum(joined.count(glyph) for glyph in _OPENING_QUOTES)
|
||||||
|
if openings < 2:
|
||||||
|
return []
|
||||||
|
return [f"{path}.quote: son {openings} citas, y este campo es UNA cita "
|
||||||
|
"partida en líneas — leídas seguidas forman una frase que nadie "
|
||||||
|
"dijo. Elige un solo span verbatim y pártelo donde tenga que "
|
||||||
|
"partirse, o quita las comillas y cuenta el hecho en llano"]
|
||||||
|
|
||||||
|
|
||||||
|
#: Comprobaciones que se aplican a TODAS las plantillas, por nombre de prop. Van
|
||||||
|
#: aparte de las de abajo para que una plantilla nueva con un campo `quote` de
|
||||||
|
#: varias líneas quede cubierta sin tocar nada — el mismo pacto que el contrato.
|
||||||
|
UNIVERSAL_CHECKS = [_quote_is_one_span]
|
||||||
|
|
||||||
#: template -> comprobaciones extra. Una plantilla sin entrada no tiene reglas
|
#: template -> comprobaciones extra. Una plantilla sin entrada no tiene reglas
|
||||||
#: cruzadas, que es el caso de casi todas.
|
#: cruzadas, que es el caso de casi todas.
|
||||||
CROSS_FIELD_CHECKS = {
|
CROSS_FIELD_CHECKS = {
|
||||||
@@ -244,7 +286,7 @@ def _check_cross_field(template: str, props: Any, path: str) -> list[str]:
|
|||||||
if not isinstance(props, dict):
|
if not isinstance(props, dict):
|
||||||
return []
|
return []
|
||||||
errors: list[str] = []
|
errors: list[str] = []
|
||||||
for check in CROSS_FIELD_CHECKS.get(template, ()):
|
for check in (*UNIVERSAL_CHECKS, *CROSS_FIELD_CHECKS.get(template, ())):
|
||||||
errors.extend(check(props, path))
|
errors.extend(check(props, path))
|
||||||
return errors
|
return errors
|
||||||
|
|
||||||
|
|||||||
@@ -246,6 +246,45 @@ class TestCrossFieldRules:
|
|||||||
"waypoints": [{"label": "A", "lat": 2.0, "lon": -1.0}]}}),
|
"waypoints": [{"label": "A", "lat": 2.0, "lon": -1.0}]}}),
|
||||||
templates)
|
templates)
|
||||||
|
|
||||||
|
def test_two_quotes_welded_into_one_field_are_rejected(self):
|
||||||
|
"""El peor fallo del sistema: una frase que nadie dijo, hecha con
|
||||||
|
material auténtico y firmada por alguien con nombre y apellidos.
|
||||||
|
|
||||||
|
El comprobador de fundamento une las líneas antes de buscarlas, y eso
|
||||||
|
caza la forma con la que falló Socorro. Pero la unión se derrota
|
||||||
|
poniéndole a cada línea su propio par de comillas: entonces son dos
|
||||||
|
citas, cada una fundamentada por su lado, y pasa en silencio. Caso real
|
||||||
|
de la sesión 162.
|
||||||
|
"""
|
||||||
|
errors = errors_of(spec_with(shot(
|
||||||
|
"scale_bars", duration=25.0,
|
||||||
|
bars=[{"label": "A", "value": 1}],
|
||||||
|
quote=["“GRAY, LIKE ZINC”", "“TWO SAUCERS GLUED AT THE RIM”"])))
|
||||||
|
assert any("es UNA cita partida en líneas" in e for e in errors)
|
||||||
|
|
||||||
|
def test_a_span_broken_across_lines_is_the_normal_case(self):
|
||||||
|
"""La forma buena: abre en la primera línea y cierra en la última. Es
|
||||||
|
como está escrito el ejemplo de referencia, así que rechazarla rompería
|
||||||
|
el propio prompt."""
|
||||||
|
validate_spec(spec_with(shot(
|
||||||
|
"scale_bars", duration=25.0,
|
||||||
|
bars=[{"label": "A", "value": 1}],
|
||||||
|
quote=["“TWICE THE SIZE OF", "AN AIRCRAFT CARRIER”"])), TEMPLATES)
|
||||||
|
|
||||||
|
def test_a_quote_without_marks_is_left_alone(self):
|
||||||
|
validate_spec(spec_with(shot(
|
||||||
|
"scale_bars", duration=25.0,
|
||||||
|
bars=[{"label": "A", "value": 1}],
|
||||||
|
quote=["LANDING TRACE", "CONFIRMED BY LAB"])), TEMPLATES)
|
||||||
|
|
||||||
|
def test_the_reference_example_survives_the_quote_rule(self):
|
||||||
|
"""Si el ejemplo no pasara su propia regla, volveríamos a enseñar el
|
||||||
|
fallo que la regla intenta evitar."""
|
||||||
|
spec = json.loads(EXAMPLE.read_text())
|
||||||
|
permissive = {name: {"type": "object"} for name in
|
||||||
|
{s["template"] for s in spec["shots"]}}
|
||||||
|
validate_spec(spec, permissive)
|
||||||
|
|
||||||
def test_a_bad_schema_hides_the_cross_field_noise(self):
|
def test_a_bad_schema_hides_the_cross_field_noise(self):
|
||||||
"""Con props mal tipadas, la regla cruzada diría algo que no es el fallo
|
"""Con props mal tipadas, la regla cruzada diría algo que no es el fallo
|
||||||
real y taparía el que sí lo es."""
|
real y taparía el que sí lo es."""
|
||||||
|
|||||||
Reference in New Issue
Block a user