Phase 6.3.17: run gate 5 in batches and stop burning prompts on a degraded plugin
Measured across three consecutive runs: the Penpot plugin reliably handles 5 or 6 heavy prompts and then degrades to 30-second timeouts on createPage, always with the same shape - the first few work, the rest fail in setup without exception. That is not random flakiness. Prompts now run in batches of 4 with a clean handshake between batches, giving the server a recovery point before the deterioration sets in. A setup failure no longer burns the prompt as measured-with-score-None: it goes on a pending list, the batch stops, and the run exits 3 with the exact GATE5_ONLY line to resume. Insisting past the first timeout only spends pages and dirties the JSON, since once the plugin starts timing out the rest fail identically. On the 30-second timeout the user asked to raise: it is the MCP server's own limit on a plugin task, not a client timeout, so it cannot be raised from here. What can be done is not to approach it. generateStyle with includeChildren plus generateMarkup serialise the whole subtree and are by far the most expensive part of the audit, so above 400 nodes they are skipped and rendersOk becomes not-applicable rather than risking the entire audit - and with it the prompt's measurement - timing out. That required fixing the scoring too: a metric whose VALUE is None is now not-applicable, like one whose threshold is None. Counting "could not measure" as a failure would have penalised exactly the large designs the gate is meant to reward.
This commit is contained in:
@@ -99,6 +99,10 @@ AUDIT_MAX_RETRIES = int(os.environ.get("GATE5_AUDIT_RETRIES", "5"))
|
|||||||
# inspeccion manual con GATE5_KEEP_PAGES=1.
|
# inspeccion manual con GATE5_KEEP_PAGES=1.
|
||||||
LIMPIAR_PAGINAS = os.environ.get("GATE5_KEEP_PAGES", "").lower() not in ("1", "true", "yes")
|
LIMPIAR_PAGINAS = os.environ.get("GATE5_KEEP_PAGES", "").lower() not in ("1", "true", "yes")
|
||||||
|
|
||||||
|
# Tamano de lote. Ver el comentario del bucle en run_full(): el plugin se degrada tras 5 o 6
|
||||||
|
# prompts pesados, asi que 4 deja margen y da un punto de re-handshake antes del deterioro.
|
||||||
|
BATCH_SIZE = int(os.environ.get("GATE5_BATCH_SIZE", "4"))
|
||||||
|
|
||||||
# El prompt que reproduce el fallo exacto de produccion. Tiene su propia condicion de
|
# El prompt que reproduce el fallo exacto de produccion. Tiene su propia condicion de
|
||||||
# aprobacion: si este no llega a 60, la puerta no aprueba aunque el promedio alcance.
|
# aprobacion: si este no llega a 60, la puerta no aprueba aunque el promedio alcance.
|
||||||
FLAGSHIP_PROMPT_ID = "g5-06-landing-pizzeria"
|
FLAGSHIP_PROMPT_ID = "g5-06-landing-pizzeria"
|
||||||
@@ -635,8 +639,18 @@ if (principal) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let cssLen = 0, markupLen = 0, renderError = null;
|
// generateStyle con includeChildren y generateMarkup son, de lejos, lo mas caro de la auditoria:
|
||||||
try {
|
// serializan el subarbol entero. El servidor MCP corta cualquier tarea del plugin a los 30
|
||||||
|
// segundos -- ese limite es SUYO, no del cliente, asi que no se puede subir desde aca; lo unico
|
||||||
|
// que se puede hacer es no acercarse. Por encima de MAX_NODOS_RENDER se saltea y se deja
|
||||||
|
// constancia, en vez de arriesgar que se caiga la auditoria ENTERA y con ella la medicion del
|
||||||
|
// prompt. `rendersOk` queda en null (no aplica) y no entra al denominador del score.
|
||||||
|
const MAX_NODOS_RENDER = 400;
|
||||||
|
let cssLen = 0, markupLen = 0, renderError = null, renderSalteado = false;
|
||||||
|
if (nodes.length > MAX_NODOS_RENDER) {
|
||||||
|
renderSalteado = true;
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
const objetivo = principal ? [principal] : top;
|
const objetivo = principal ? [principal] : top;
|
||||||
if (objetivo.length > 0) {
|
if (objetivo.length > 0) {
|
||||||
const css = penpot.generateStyle(objetivo, { type: "css", includeChildren: true }) || "";
|
const css = penpot.generateStyle(objetivo, { type: "css", includeChildren: true }) || "";
|
||||||
@@ -644,8 +658,9 @@ try {
|
|||||||
cssLen = String(css).length;
|
cssLen = String(css).length;
|
||||||
markupLen = String(markup).length;
|
markupLen = String(markup).length;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
renderError = String((e && e.message) || e);
|
renderError = String((e && e.message) || e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const tamsUnicos = [];
|
const tamsUnicos = [];
|
||||||
@@ -749,7 +764,8 @@ const resultado = {
|
|||||||
rootHeightMismatch: desajusteAltura,
|
rootHeightMismatch: desajusteAltura,
|
||||||
cssLength: cssLen,
|
cssLength: cssLen,
|
||||||
markupLength: markupLen,
|
markupLength: markupLen,
|
||||||
renderError: renderError
|
renderError: renderError,
|
||||||
|
renderSkipped: renderSalteado
|
||||||
};
|
};
|
||||||
|
|
||||||
// El servidor MCP valida el resultado contra un schema y rechaza la llamada ENTERA si algun
|
// El servidor MCP valida el resultado contra un schema y rechaza la llamada ENTERA si algun
|
||||||
@@ -1062,8 +1078,13 @@ def build_raw_metrics(audit, runtime):
|
|||||||
"paletteStructured": bool(audit.get("paletteStructured", False)),
|
"paletteStructured": bool(audit.get("paletteStructured", False)),
|
||||||
"finalMessageListsHex": runtime["finalMessageListsHex"],
|
"finalMessageListsHex": runtime["finalMessageListsHex"],
|
||||||
"fontIdBoundShare": audit.get("fontIdBoundShare", 0.0),
|
"fontIdBoundShare": audit.get("fontIdBoundShare", 0.0),
|
||||||
"rendersOk": bool(audit.get("cssLength", 0) > 0 and audit.get("markupLength", 0) > 0
|
# Si la auditoria salteo el render por tamano, la metrica NO APLICA (None): no se puede
|
||||||
and not audit.get("renderError")),
|
# afirmar que el arbol renderiza ni que no renderiza, y contarla como fallo castigaria
|
||||||
|
# justo a los disenos grandes, que es lo contrario de lo que la puerta quiere premiar.
|
||||||
|
"rendersOk": (None if audit.get("renderSkipped")
|
||||||
|
else bool(audit.get("cssLength", 0) > 0
|
||||||
|
and audit.get("markupLength", 0) > 0
|
||||||
|
and not audit.get("renderError"))),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -1088,7 +1109,11 @@ def score_prompt(prompt_id, raw):
|
|||||||
veto = []
|
veto = []
|
||||||
for clave, etiqueta, comparador in METRICS:
|
for clave, etiqueta, comparador in METRICS:
|
||||||
umbral = umbrales.get(clave)
|
umbral = umbrales.get(clave)
|
||||||
ok = evaluar(clave, comparador, raw[clave], umbral)
|
# Una metrica NO APLICA si su umbral es None (el prompt no la exige) o si su VALOR es
|
||||||
|
# None (la auditoria no la pudo computar, p.ej. el render salteado por tamano). Contar
|
||||||
|
# "no se pudo medir" como fallo castigaria al diseno grande, que es lo contrario de lo
|
||||||
|
# que la puerta premia.
|
||||||
|
ok = None if raw[clave] is None else evaluar(clave, comparador, raw[clave], umbral)
|
||||||
detalle.append({
|
detalle.append({
|
||||||
"metrica": clave, "etiqueta": etiqueta, "valor": raw[clave],
|
"metrica": clave, "etiqueta": etiqueta, "valor": raw[clave],
|
||||||
"umbral": umbral, "aplica": ok is not None, "pasa": ok,
|
"umbral": umbral, "aplica": ok is not None, "pasa": ok,
|
||||||
@@ -1689,18 +1714,54 @@ def run_full(cfg, tools, system_prompt):
|
|||||||
|
|
||||||
t0 = time.time()
|
t0 = time.time()
|
||||||
resultados = []
|
resultados = []
|
||||||
for fila in prompts:
|
pendientes = []
|
||||||
|
corte_por_degradacion = False
|
||||||
|
|
||||||
|
# LOTES. El plugin de Penpot aguanta de forma confiable unos 5 o 6 prompts pesados y despues
|
||||||
|
# se degrada hasta dar timeouts de 30 s en `createPage`. Medido en tres corridas seguidas,
|
||||||
|
# siempre con la misma forma: los primeros funcionan, el resto falla en el setup sin
|
||||||
|
# excepcion. Procesar en lotes chicos con un handshake limpio en el medio le da al servidor
|
||||||
|
# un punto de recuperacion, y si igual se degrada se corta ahi en vez de quemar los que
|
||||||
|
# faltan de a uno.
|
||||||
|
for inicio in range(0, len(prompts), BATCH_SIZE):
|
||||||
|
lote = prompts[inicio:inicio + BATCH_SIZE]
|
||||||
|
n_lote = inicio // BATCH_SIZE + 1
|
||||||
|
total_lotes = (len(prompts) + BATCH_SIZE - 1) // BATCH_SIZE
|
||||||
|
if inicio > 0:
|
||||||
|
print(f"\n[LOTE] handshake limpio antes del lote {n_lote}/{total_lotes}")
|
||||||
|
mcp.session_id = None
|
||||||
|
try:
|
||||||
|
mcp.handshake()
|
||||||
|
except Gate5Error as e:
|
||||||
|
print(f"[LOTE] el re-handshake fallo ({e}); se corta y quedan pendientes "
|
||||||
|
f"{len(prompts) - inicio} prompt(s)")
|
||||||
|
pendientes.extend(f["id"] for f in prompts[inicio:])
|
||||||
|
corte_por_degradacion = True
|
||||||
|
break
|
||||||
|
print(f"\n[LOTE] {n_lote}/{total_lotes}: {[f['id'] for f in lote]}")
|
||||||
|
|
||||||
|
for i, fila in enumerate(lote):
|
||||||
try:
|
try:
|
||||||
resultados.append(run_prompt(cfg, mcp, tools, system_prompt, fila, png_dir))
|
resultados.append(run_prompt(cfg, mcp, tools, system_prompt, fila, png_dir))
|
||||||
except PluginNotConnected:
|
except PluginNotConnected:
|
||||||
volcar(resultados, time.time() - t0)
|
volcar(resultados, time.time() - t0)
|
||||||
raise
|
raise
|
||||||
except Gate5Error as e:
|
except Gate5Error as e:
|
||||||
|
# Un fallo de setup NO quema el prompt: queda PENDIENTE, no medido con score
|
||||||
|
# None. Y se corta el lote, porque una vez que el plugin empieza a dar timeouts
|
||||||
|
# los que siguen fallan todos igual -- insistir solo gasta paginas y ensucia el
|
||||||
|
# JSON.
|
||||||
print(f"[ERROR] {fila['id']}: {e}")
|
print(f"[ERROR] {fila['id']}: {e}")
|
||||||
print(f"[INFO] se guardan los {len(resultados)} prompt(s) ya medidos y se sigue con "
|
restantes = [f["id"] for f in lote[i:]] + \
|
||||||
f"el siguiente; el JSON queda marcado como parcial")
|
[f["id"] for f in prompts[inicio + BATCH_SIZE:]]
|
||||||
resultados.append({"id": fila["id"], "error": str(e), "score": None})
|
print(f"[LOTE] se corta el lote. Quedan {len(restantes)} prompt(s) PENDIENTES "
|
||||||
|
f"(no medidos, no quemados): {restantes}")
|
||||||
|
pendientes.extend(restantes)
|
||||||
|
corte_por_degradacion = True
|
||||||
|
break
|
||||||
volcar(resultados, time.time() - t0)
|
volcar(resultados, time.time() - t0)
|
||||||
|
if corte_por_degradacion:
|
||||||
|
break
|
||||||
dt = time.time() - t0
|
dt = time.time() - t0
|
||||||
|
|
||||||
aprueba, veredicto = resumen(fusionar(resultados), cfg["GATE5_TAG"],
|
aprueba, veredicto = resumen(fusionar(resultados), cfg["GATE5_TAG"],
|
||||||
@@ -1708,7 +1769,8 @@ def run_full(cfg, tools, system_prompt):
|
|||||||
print(f"\n tiempo total: {dt / 60:.1f} min")
|
print(f"\n tiempo total: {dt / 60:.1f} min")
|
||||||
|
|
||||||
volcar(resultados, dt, veredicto_parcial=veredicto, completo=True)
|
volcar(resultados, dt, veredicto_parcial=veredicto, completo=True)
|
||||||
sin_medir = [r["id"] for r in fusionar(resultados) if r.get("score") is None]
|
sin_medir = sorted(set(pendientes) |
|
||||||
|
{r["id"] for r in fusionar(resultados) if r.get("score") is None})
|
||||||
if sin_medir:
|
if sin_medir:
|
||||||
print(f"\n[PLUGIN DEGRADADO] {len(sin_medir)} prompt(s) no se pudieron medir. El plugin "
|
print(f"\n[PLUGIN DEGRADADO] {len(sin_medir)} prompt(s) no se pudieron medir. El plugin "
|
||||||
f"de Penpot responde pero se degrada tras 5 o 6 prompts pesados.")
|
f"de Penpot responde pero se degrada tras 5 o 6 prompts pesados.")
|
||||||
@@ -1716,8 +1778,8 @@ def run_full(cfg, tools, system_prompt):
|
|||||||
print(f" GATE5_ONLY={','.join(sin_medir)}")
|
print(f" GATE5_ONLY={','.join(sin_medir)}")
|
||||||
return EXIT_PLUGIN_DEGRADADO
|
return EXIT_PLUGIN_DEGRADADO
|
||||||
print(f" resultados en {salida.relative_to(REPO_ROOT)}")
|
print(f" resultados en {salida.relative_to(REPO_ROOT)}")
|
||||||
print(f" PNGs en {png_dir.relative_to(REPO_ROOT)}/ (la puerta no borra nada: las paginas "
|
print(f" PNGs en {png_dir.relative_to(REPO_ROOT)}/ (la pagina de cada prompt se vacia tras "
|
||||||
f"quedan para inspeccion humana)")
|
f"exportar su PNG; solo se conserva la de un prompt cuyo PNG haya fallado)")
|
||||||
return 0 if aprueba else 1
|
return 0 if aprueba else 1
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user