Phase 6.3.17: fix a harness fidelity bug and add a vibrancy metric
The first baseline measured production with only 2290 of the 16392 characters of the server's instructions block - 14%. The missing 86% is exactly the API grounding: Core Shape Properties and Methods, Layout Systems, Text Elements, and The penpot and penpotUtils Objects, which is where insertChild, resize(), the layouts and penpotUtils are documented. That was worth catching, because the discrepancy had a visible signature: the measurement said production creates nothing, while the user's real Claude Code session produced grey boxes, i.e. shapes greater than zero. When a harness and reality disagree, the harness is the first suspect. In phase 5 a low max_tokens manufactured an apparent regression the same way. The gate now injects the full document, minus the trailing "You have hereby read the Penpot High-Level Overview" line, which is framing of the tool response rather than part of the instructions block and would otherwise tell the model it had already read something. The finding survives the fix. Across the five prompts measured cleanly under the corrected condition, shapeCount is still zero on every one. So the API invention is not an artefact of withholding documentation from the model - it happens with the documentation present. Also adds the vibrancy requirement the user raised as first-class scope: given an ambiguous brief the model must choose and justify a palette rather than ask or fall back to defaults. Neither distinctFillColors nor placeholderGreys distinguishes a vibrant palette from a muted but technically non-grey one, so four metrics are added: chromaticFills, meanChromaticSaturation, paletteStructured (a dominant brand hue, an accent at least 30 degrees away, and neutrals), and finalMessageListsHex, because a palette chosen in silence cannot be adjusted by the user. The saturation floor of 45 is derived, not asserted: measured over the 325 non-neutral fills of this phase's hand-authored corpus, median HSL saturation is 75, p25 is 48 and p10 is 35. A floor of 45 sits just under the first quartile and is cleared by 79% of those fills, so it is a floor the target behaviour already clears rather than an aspiration. The lightness band of 15 to 85 excludes near-blacks and near-whites, which can compute as highly saturated while reading as neutral. Gate prompt 6 becomes the user's literal failing sentence, and two ambiguous-brief prompts are added. One of them had to be re-domained after the disjointness check found it shared a 6-gram with a seed - the check fails on a single shared shingle, which is what makes it useful. Results so far are partial: prompts 1-5 measured cleanly, 6 has a timed-out audit and 7-10 hit the MCP outage, so those get re-measured. Both runs are kept, the 14% one renamed to record what it was.
This commit is contained in:
@@ -0,0 +1,77 @@
|
||||
[OK] los 8 prompts son disjuntos de train/eval/seeds (7472 shingles de referencia)
|
||||
[MCP] handshake ok con 'penpot-mcp-server'; tools: execute_code, high_level_overview, penpot_api_info, export_shape
|
||||
[MCP] sonda de conectividad del plugin: {
|
||||
"result": {
|
||||
"gate5Probe": true
|
||||
},
|
||||
"log": ""
|
||||
}
|
||||
|
||||
--- g5-01-boton-primario (baja) -> pagina gate5/prod-baseline/g5-01-boton-primario/20260730T175220Z
|
||||
[MCP] fallo transitorio en 'tools/call' (intento 1/5): error de transporte contra el MCP de Penpot: ConnectionError. Reintento en 4s
|
||||
[MCP] rehaciendo el handshake (la sesion pudo haber muerto)
|
||||
[MCP] handshake ok con 'penpot-mcp-server'; tools: execute_code, high_level_overview, penpot_api_info, export_shape
|
||||
[WARN] g5-01-boton-primario: se agotaron los 14 turnos sin mensaje final
|
||||
[WARN] g5-01-boton-primario: export_shape no devolvio binario
|
||||
score=0.0 (2/14) grises=0 prohibido=7 shapes=0 colores=0 textos=0
|
||||
|
||||
--- g5-02-navbar-flex (baja) -> pagina gate5/prod-baseline/g5-02-navbar-flex/20260730T175644Z
|
||||
[WARN] g5-02-navbar-flex: se agotaron los 14 turnos sin mensaje final
|
||||
[WARN] g5-02-navbar-flex: export_shape no devolvio binario
|
||||
score=17.6 (3/17) grises=0 prohibido=0 shapes=0 colores=0 textos=0
|
||||
|
||||
--- g5-03-card-producto (media) -> pagina gate5/prod-baseline/g5-03-card-producto/20260730T175753Z
|
||||
[WARN] g5-03-card-producto: se agotaron los 14 turnos sin mensaje final
|
||||
[WARN] g5-03-card-producto: export_shape no devolvio binario
|
||||
score=20.0 (3/15) grises=0 prohibido=0 shapes=0 colores=0 textos=0
|
||||
|
||||
--- g5-04-tokens-biblioteca (media) -> pagina gate5/prod-baseline/g5-04-tokens-biblioteca/20260730T175855Z
|
||||
[WARN] g5-04-tokens-biblioteca: se agotaron los 14 turnos sin mensaje final
|
||||
[WARN] g5-04-tokens-biblioteca: export_shape no devolvio binario
|
||||
score=20.0 (3/15) grises=0 prohibido=0 shapes=0 colores=0 textos=0
|
||||
|
||||
--- g5-05-grid-tres-diferenciales (media) -> pagina gate5/prod-baseline/g5-05-grid-tres-diferenciales/20260730T175957Z
|
||||
[WARN] g5-05-grid-tres-diferenciales: se agotaron los 14 turnos sin mensaje final
|
||||
[WARN] g5-05-grid-tres-diferenciales: export_shape no devolvio binario
|
||||
score=17.6 (3/17) grises=0 prohibido=0 shapes=0 colores=0 textos=0
|
||||
|
||||
--- g5-06-landing-pizzeria (alta) -> pagina gate5/prod-baseline/g5-06-landing-pizzeria/20260730T180102Z
|
||||
[MCP] fallo transitorio en 'tools/call' (intento 1/5): error de transporte contra el MCP de Penpot: ConnectionError. Reintento en 4s
|
||||
[MCP] rehaciendo el handshake (la sesion pudo haber muerto)
|
||||
[MCP] handshake ok con 'penpot-mcp-server'; tools: execute_code, high_level_overview, penpot_api_info, export_shape
|
||||
[WARN] g5-06-landing-pizzeria: export_shape no devolvio binario
|
||||
score=0.0 (3/17) grises=0 prohibido=5 shapes=0 colores=0 textos=0
|
||||
|
||||
--- g5-07-onboarding-mobile (alta) -> pagina gate5/prod-baseline/g5-07-onboarding-mobile/20260730T180525Z
|
||||
[WARN] g5-07-onboarding-mobile: se agotaron los 14 turnos sin mensaje final
|
||||
[WARN] g5-07-onboarding-mobile: export_shape no devolvio binario
|
||||
score=17.6 (3/17) grises=0 prohibido=0 shapes=0 colores=0 textos=0
|
||||
|
||||
--- g5-08-reparacion-grises (alta) -> pagina gate5/prod-baseline/g5-08-reparacion-grises/20260730T180627Z
|
||||
[WARN] g5-08-reparacion-grises: se agotaron los 14 turnos sin mensaje final
|
||||
[WARN] g5-08-reparacion-grises: export_shape no devolvio binario
|
||||
score=26.7 (4/15) grises=0 prohibido=0 shapes=0 colores=0 textos=0
|
||||
|
||||
=== Puerta 5 -- calidad de diseno en Penpot (tag=prod-baseline) ===
|
||||
|
||||
prompt dif score pasa grises prohib shapes colores textos
|
||||
g5-01-boton-primario baja 0.0 2/14 0 7 0 0 0
|
||||
g5-02-navbar-flex baja 17.6 3/17 0 0 0 0 0
|
||||
g5-03-card-producto media 20.0 3/15 0 0 0 0 0
|
||||
g5-04-tokens-biblioteca media 20.0 3/15 0 0 0 0 0
|
||||
g5-05-grid-tres-diferenciales media 17.6 3/17 0 0 0 0 0
|
||||
g5-06-landing-pizzeria alta 0.0 3/17 0 5 0 0 0
|
||||
g5-07-onboarding-mobile alta 17.6 3/17 0 0 0 0 0
|
||||
g5-08-reparacion-grises alta 26.7 4/15 0 0 0 0 0
|
||||
|
||||
condiciones de aprobacion:
|
||||
[NO ] score medio >= 65 -> 14.9
|
||||
[NO ] veto limpio en 8/8 -> 6/8
|
||||
[NO ] >= 6/8 prompts >= 60 -> 0/8
|
||||
[NO ] landing de pizzeria >= 60 -> 0.0
|
||||
|
||||
VEREDICTO GLOBAL: NO APRUEBA
|
||||
|
||||
tiempo total: 15.1 min
|
||||
resultados en data/gate5_results_prod-baseline.json
|
||||
PNGs en data/gate5_png_prod-baseline/ (la puerta no borra nada: las paginas quedan para inspeccion humana)
|
||||
@@ -1,4 +1,5 @@
|
||||
[OK] los 8 prompts son disjuntos de train/eval/seeds (7472 shingles de referencia)
|
||||
[SYSTEM] documento completo del servidor: 16293 chars (247 lineas)
|
||||
[OK] los 10 prompts son disjuntos de train/eval/seeds (7472 shingles de referencia)
|
||||
[MCP] handshake ok con 'penpot-mcp-server'; tools: execute_code, high_level_overview, penpot_api_info, export_shape
|
||||
[MCP] sonda de conectividad del plugin: {
|
||||
"result": {
|
||||
@@ -7,71 +8,48 @@
|
||||
"log": ""
|
||||
}
|
||||
|
||||
--- g5-01-boton-primario (baja) -> pagina gate5/prod-baseline/g5-01-boton-primario/20260730T175220Z
|
||||
[MCP] fallo transitorio en 'tools/call' (intento 1/5): error de transporte contra el MCP de Penpot: ConnectionError. Reintento en 4s
|
||||
[MCP] rehaciendo el handshake (la sesion pudo haber muerto)
|
||||
[MCP] handshake ok con 'penpot-mcp-server'; tools: execute_code, high_level_overview, penpot_api_info, export_shape
|
||||
[WARN] g5-01-boton-primario: se agotaron los 14 turnos sin mensaje final
|
||||
--- g5-01-boton-primario (baja) -> pagina gate5/prod-baseline/g5-01-boton-primario/20260730T182503Z
|
||||
[WARN] g5-01-boton-primario: export_shape no devolvio binario
|
||||
score=0.0 (2/14) grises=0 prohibido=7 shapes=0 colores=0 textos=0
|
||||
score=0.0 (2/14) grises=0 prohibido=5 shapes=0 colores=0 textos=0
|
||||
|
||||
--- g5-02-navbar-flex (baja) -> pagina gate5/prod-baseline/g5-02-navbar-flex/20260730T175644Z
|
||||
--- g5-02-navbar-flex (baja) -> pagina gate5/prod-baseline/g5-02-navbar-flex/20260730T182608Z
|
||||
[WARN] g5-02-navbar-flex: se agotaron los 14 turnos sin mensaje final
|
||||
[WARN] g5-02-navbar-flex: export_shape no devolvio binario
|
||||
score=17.6 (3/17) grises=0 prohibido=0 shapes=0 colores=0 textos=0
|
||||
score=0.0 (3/17) grises=0 prohibido=1 shapes=0 colores=0 textos=0
|
||||
|
||||
--- g5-03-card-producto (media) -> pagina gate5/prod-baseline/g5-03-card-producto/20260730T175753Z
|
||||
--- g5-03-card-producto (media) -> pagina gate5/prod-baseline/g5-03-card-producto/20260730T182706Z
|
||||
[WARN] g5-03-card-producto: se agotaron los 14 turnos sin mensaje final
|
||||
[WARN] g5-03-card-producto: export_shape no devolvio binario
|
||||
score=20.0 (3/15) grises=0 prohibido=0 shapes=0 colores=0 textos=0
|
||||
|
||||
--- g5-04-tokens-biblioteca (media) -> pagina gate5/prod-baseline/g5-04-tokens-biblioteca/20260730T175855Z
|
||||
[WARN] g5-04-tokens-biblioteca: se agotaron los 14 turnos sin mensaje final
|
||||
--- g5-04-tokens-biblioteca (media) -> pagina gate5/prod-baseline/g5-04-tokens-biblioteca/20260730T182804Z
|
||||
[WARN] g5-04-tokens-biblioteca: export_shape no devolvio binario
|
||||
score=20.0 (3/15) grises=0 prohibido=0 shapes=0 colores=0 textos=0
|
||||
score=0.0 (3/15) grises=0 prohibido=1 shapes=0 colores=0 textos=0
|
||||
|
||||
--- g5-05-grid-tres-diferenciales (media) -> pagina gate5/prod-baseline/g5-05-grid-tres-diferenciales/20260730T175957Z
|
||||
[WARN] g5-05-grid-tres-diferenciales: se agotaron los 14 turnos sin mensaje final
|
||||
--- g5-05-grid-tres-diferenciales (media) -> pagina gate5/prod-baseline/g5-05-grid-tres-diferenciales/20260730T182903Z
|
||||
[WARN] g5-05-grid-tres-diferenciales: export_shape no devolvio binario
|
||||
score=17.6 (3/17) grises=0 prohibido=0 shapes=0 colores=0 textos=0
|
||||
score=23.5 (4/17) grises=0 prohibido=0 shapes=0 colores=0 textos=0
|
||||
|
||||
--- g5-06-landing-pizzeria (alta) -> pagina gate5/prod-baseline/g5-06-landing-pizzeria/20260730T180102Z
|
||||
--- g5-06-landing-pizzeria (alta) -> pagina gate5/prod-baseline/g5-06-landing-pizzeria/20260730T183005Z
|
||||
[MCP] fallo transitorio en 'tools/call' (intento 1/5): error de transporte contra el MCP de Penpot: ConnectionError. Reintento en 4s
|
||||
[MCP] rehaciendo el handshake (la sesion pudo haber muerto)
|
||||
[MCP] handshake ok con 'penpot-mcp-server'; tools: execute_code, high_level_overview, penpot_api_info, export_shape
|
||||
[WARN] g5-06-landing-pizzeria: la auditoria fallo (g5-06-landing-pizzeria: auditoria: el MCP no devolvio JSON (texto): Tool execution failed: Error: Task 374cec0b-2e20-40bc-b58e-44e7ae20b6b0 timed out after 30 seconds)
|
||||
[WARN] g5-06-landing-pizzeria: export_shape no devolvio binario
|
||||
score=0.0 (3/17) grises=0 prohibido=5 shapes=0 colores=0 textos=0
|
||||
score=0.0 (2/21) grises=0 prohibido=1 shapes=0 colores=0 textos=0
|
||||
|
||||
--- g5-07-onboarding-mobile (alta) -> pagina gate5/prod-baseline/g5-07-onboarding-mobile/20260730T180525Z
|
||||
[WARN] g5-07-onboarding-mobile: se agotaron los 14 turnos sin mensaje final
|
||||
[WARN] g5-07-onboarding-mobile: export_shape no devolvio binario
|
||||
score=17.6 (3/17) grises=0 prohibido=0 shapes=0 colores=0 textos=0
|
||||
--- g5-07-onboarding-mobile (alta) -> pagina gate5/prod-baseline/g5-07-onboarding-mobile/20260730T183529Z
|
||||
[ERROR] g5-07-onboarding-mobile: g5-07-onboarding-mobile: setup de la pagina: el MCP no devolvio JSON (texto): Tool execution failed: Error: Task 98dda7fa-baed-4e2e-99f4-fa8bf33c1326 timed out after 30 seconds
|
||||
[INFO] se guardan los 6 prompt(s) ya medidos y se sigue con el siguiente; el JSON queda marcado como parcial
|
||||
|
||||
--- g5-08-reparacion-grises (alta) -> pagina gate5/prod-baseline/g5-08-reparacion-grises/20260730T180627Z
|
||||
[WARN] g5-08-reparacion-grises: se agotaron los 14 turnos sin mensaje final
|
||||
[WARN] g5-08-reparacion-grises: export_shape no devolvio binario
|
||||
score=26.7 (4/15) grises=0 prohibido=0 shapes=0 colores=0 textos=0
|
||||
--- g5-08-reparacion-grises (alta) -> pagina gate5/prod-baseline/g5-08-reparacion-grises/20260730T183559Z
|
||||
[ERROR] g5-08-reparacion-grises: g5-08-reparacion-grises: setup de la pagina: el MCP no devolvio JSON (texto): Tool execution failed: Error: Task 725a0fd0-3c6e-4b4b-bf1c-bb65776c4fb0 timed out after 30 seconds
|
||||
[INFO] se guardan los 7 prompt(s) ya medidos y se sigue con el siguiente; el JSON queda marcado como parcial
|
||||
|
||||
=== Puerta 5 -- calidad de diseno en Penpot (tag=prod-baseline) ===
|
||||
--- g5-09-brief-ambiguo-escuela (alta) -> pagina gate5/prod-baseline/g5-09-brief-ambiguo-escuela/20260730T183629Z
|
||||
|
||||
prompt dif score pasa grises prohib shapes colores textos
|
||||
g5-01-boton-primario baja 0.0 2/14 0 7 0 0 0
|
||||
g5-02-navbar-flex baja 17.6 3/17 0 0 0 0 0
|
||||
g5-03-card-producto media 20.0 3/15 0 0 0 0 0
|
||||
g5-04-tokens-biblioteca media 20.0 3/15 0 0 0 0 0
|
||||
g5-05-grid-tres-diferenciales media 17.6 3/17 0 0 0 0 0
|
||||
g5-06-landing-pizzeria alta 0.0 3/17 0 5 0 0 0
|
||||
g5-07-onboarding-mobile alta 17.6 3/17 0 0 0 0 0
|
||||
g5-08-reparacion-grises alta 26.7 4/15 0 0 0 0 0
|
||||
|
||||
condiciones de aprobacion:
|
||||
[NO ] score medio >= 65 -> 14.9
|
||||
[NO ] veto limpio en 8/8 -> 6/8
|
||||
[NO ] >= 6/8 prompts >= 60 -> 0/8
|
||||
[NO ] landing de pizzeria >= 60 -> 0.0
|
||||
|
||||
VEREDICTO GLOBAL: NO APRUEBA
|
||||
|
||||
tiempo total: 15.1 min
|
||||
resultados en data/gate5_results_prod-baseline.json
|
||||
PNGs en data/gate5_png_prod-baseline/ (la puerta no borra nada: las paginas quedan para inspeccion humana)
|
||||
[ABORTA] el servidor MCP de Penpot no tiene ningun plugin conectado.
|
||||
Abri el archivo de Penpot en el navegador, arranca el plugin del MCP y espera a
|
||||
que quede conectado; recien ahi volve a correr la puerta. Sin plugin conectado no
|
||||
tiene sentido gastar los 8 prompts.
|
||||
Mensaje del servidor: Tool execution failed: Error: No Penpot plugin instances are currently connected. Please ensure the plugin is running and connected.
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
{"id": "g5-03-card-producto", "dificultad": "media", "prompt": "Hacé una card de producto de 320 de ancho con una foto real traída de https://picsum.photos/seed/espresso/640/480, un título, una bajada de dos líneas, el precio destacado y un botón de compra. La foto tiene que ser la imagen de verdad: nada de rectángulos grises simulando la imagen."}
|
||||
{"id": "g5-04-tokens-biblioteca", "dificultad": "media", "prompt": "Creá en la biblioteca local del archivo una paleta de 6 colores nombrados por rol y una escala tipográfica de 5 pasos, y después aplicá esos tokens a una muestra visible: un encabezado, un párrafo de apoyo y tres chips que muestren los colores aplicados desde la biblioteca."}
|
||||
{"id": "g5-05-grid-tres-diferenciales", "dificultad": "media", "prompt": "Componé una sección de 3 diferenciales de 1140 de ancho resuelta con un grid de 3 columnas: cada columna con una figura geométrica propia a modo de ícono, un titular corto y dos líneas de texto de apoyo, todo con la misma alineación y respiración entre columnas."}
|
||||
{"id": "g5-06-landing-pizzeria", "dificultad": "alta", "prompt": "hazme una landing page de una pizzería napolitana con colores vibrantes, de 1200 de ancho"}
|
||||
{"id": "g5-06-landing-pizzeria", "dificultad": "alta", "prompt": "hazme una landing page de una pizzería con colores vibrantes", "brief": "ambiguo"}
|
||||
{"id": "g5-07-onboarding-mobile", "dificultad": "alta", "prompt": "Diseñá una pantalla mobile de onboarding de 375x812 para una app de reservas: ilustración o figura de bienvenida arriba, titular, párrafo explicativo, indicador de tres pasos y dos acciones al pie (una primaria y una secundaria)."}
|
||||
{"id": "g5-08-reparacion-grises", "dificultad": "alta", "prompt": "Este board está hecho enteramente de rectángulos grises y no tiene una sola línea de texto. Auditalo, definí una paleta y una escala tipográfica, y rehacelo hasta que sea una pantalla de verdad, con contenido legible y jerarquía visual.", "seed_board": {"nombre": "Wireframe sin disenar", "x": 0, "y": 0, "ancho": 960, "alto": 640, "gris": "#B1B2B5", "rectangulos": [{"nombre": "header", "x": 32, "y": 32, "ancho": 896, "alto": 88}, {"nombre": "hero", "x": 32, "y": 144, "ancho": 896, "alto": 208}, {"nombre": "card-1", "x": 32, "y": 376, "ancho": 288, "alto": 160}, {"nombre": "card-2", "x": 336, "y": 376, "ancho": 288, "alto": 160}, {"nombre": "card-3", "x": 640, "y": 376, "ancho": 288, "alto": 160}, {"nombre": "footer", "x": 32, "y": 560, "ancho": 896, "alto": 48}]}}
|
||||
{"id": "g5-09-brief-ambiguo-escuela", "dificultad": "alta", "brief": "ambiguo", "prompt": "necesito la home de una escuela de música para chicos, que sea alegre; no tengo paleta ni logo, elegí vos"}
|
||||
{"id": "g5-10-brief-ambiguo-notaria", "dificultad": "alta", "brief": "ambiguo", "prompt": "quiero el sitio de una notaría, que transmita seriedad; no te paso colores"}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -94,6 +94,8 @@ PROMPT_SCORE_MIN = 60.0
|
||||
MIN_PROMPTS_OVER_MIN = 6
|
||||
HOLDOUT_FORBIDDEN_RATE_MAX = 0.05
|
||||
|
||||
HEX_EN_TEXTO = re.compile(r"#[0-9a-fA-F]{6}\b")
|
||||
|
||||
PLUGIN_NOT_CONNECTED = "No Penpot plugin instances are currently connected"
|
||||
EXIT_PLUGIN_NOT_CONNECTED = 2
|
||||
|
||||
@@ -597,7 +599,54 @@ try {
|
||||
const tamsUnicos = [];
|
||||
for (const tam of fontSizes) { if (tamsUnicos.indexOf(tam) === -1) { tamsUnicos.push(tam); } }
|
||||
|
||||
// VIBRACION. Ni `distinctFillColors` ni `placeholderGreys` distinguen una paleta vibrante de
|
||||
// una apagada pero tecnicamente no gris: seis tonos polvorientos pasan las dos. Esto mide
|
||||
// saturacion de verdad, en HSL.
|
||||
function aHsl(hex) {
|
||||
const r = parseInt(hex.slice(1, 3), 16) / 255;
|
||||
const g = parseInt(hex.slice(3, 5), 16) / 255;
|
||||
const b = parseInt(hex.slice(5, 7), 16) / 255;
|
||||
const mx = Math.max(r, g, b), mn = Math.min(r, g, b), d = mx - mn;
|
||||
const l = (mx + mn) / 2;
|
||||
let h = 0, sat = 0;
|
||||
if (d !== 0) {
|
||||
sat = d / (1 - Math.abs(2 * l - 1));
|
||||
if (mx === r) { h = ((g - b) / d) % 6; }
|
||||
else if (mx === g) { h = (b - r) / d + 2; }
|
||||
else { h = (r - g) / d + 4; }
|
||||
h = h * 60; if (h < 0) { h += 360; }
|
||||
}
|
||||
return { h: h, s: sat * 100, l: l * 100 };
|
||||
}
|
||||
const cromaticos = [];
|
||||
const neutrales = [];
|
||||
for (const hex of distintos) {
|
||||
const c = aHsl(hex);
|
||||
// Cromatico: saturacion >= 45 y luminosidad en 15..85. La banda de luminosidad excluye los
|
||||
// casi-negros y casi-blancos, que pueden dar saturacion alta y sin embargo leerse neutros.
|
||||
if (c.s >= 45 && c.l >= 15 && c.l <= 85) { cromaticos.push({ hex: hex, h: c.h, s: c.s, l: c.l }); }
|
||||
else if (c.s <= 15) { neutrales.push(hex); }
|
||||
}
|
||||
// Estructura de paleta: una marca dominante, un acento con tono claramente distinto, y neutrales.
|
||||
let separacionDeTono = 0;
|
||||
for (let i = 0; i < cromaticos.length; i++) {
|
||||
for (let j = i + 1; j < cromaticos.length; j++) {
|
||||
let d = Math.abs(cromaticos[i].h - cromaticos[j].h);
|
||||
if (d > 180) { d = 360 - d; }
|
||||
if (d > separacionDeTono) { separacionDeTono = d; }
|
||||
}
|
||||
}
|
||||
const saturacionMedia = cromaticos.length
|
||||
? cromaticos.reduce(function (a, c) { return a + c.s; }, 0) / cromaticos.length : 0;
|
||||
|
||||
return {
|
||||
chromaticFills: cromaticos.length,
|
||||
chromaticSamples: cromaticos.slice(0, 8).map(function (c) {
|
||||
return { hex: c.hex, s: Math.round(c.s), l: Math.round(c.l) }; }),
|
||||
meanChromaticSaturation: Math.round(saturacionMedia),
|
||||
neutralFills: neutrales.length,
|
||||
hueSeparation: Math.round(separacionDeTono),
|
||||
paletteStructured: cromaticos.length >= 2 && neutrales.length >= 1 && separacionDeTono >= 30,
|
||||
shapeCount: nodes.length,
|
||||
distinctFillColors: distintos.length,
|
||||
fillColors: distintos.slice(0, 24),
|
||||
@@ -727,6 +776,10 @@ def check_disjunto(prompts):
|
||||
METRICS = [
|
||||
("shapeCount", "conteo de shapes", "min"),
|
||||
("distinctFillColors", "fillColor distintos (sin blanco/negro puros)", "min"),
|
||||
("chromaticFills", "fills cromaticos (sat HSL >= 45, L 15-85)", "min"),
|
||||
("meanChromaticSaturation", "saturacion media de los fills cromaticos", "min"),
|
||||
("paletteStructured", "paleta estructurada (marca + acento + neutrales)", "bool"),
|
||||
("finalMessageListsHex", "el mensaje final enumera los hex elegidos", "bool"),
|
||||
("placeholderGreys", "grises de placeholder [VETO]", "max"),
|
||||
("textsWithCharacters", "textos con characters no vacio", "min"),
|
||||
("typeScale", "fontSize distintos y maximo", "pair"),
|
||||
@@ -749,6 +802,27 @@ VETO_METRICS = ("placeholderGreys", "forbiddenBehavior")
|
||||
# Umbrales por prompt. `None` = la metrica NO aplica a ese prompt y no entra en el denominador
|
||||
# del score. Estan aca y no en el .jsonl a proposito: el .jsonl describe la TAREA, el umbral es
|
||||
# criterio de la puerta y se versiona junto al codigo que lo aplica.
|
||||
# ------------------------------------------------------------------------------------------
|
||||
# Umbrales de VIBRACION, solo para los prompts de brief ambiguo
|
||||
# ------------------------------------------------------------------------------------------
|
||||
# Requisito explicito del usuario: ante un brief ambiguo el modelo tiene que ELEGIR una paleta y
|
||||
# justificarla, no preguntar ni caer en defaults. Ni `distinctFillColors` ni `placeholderGreys`
|
||||
# distinguen una paleta vibrante de una apagada pero tecnicamente no gris.
|
||||
#
|
||||
# El umbral de saturacion 45 NO es una opinion: sale de medir las 325 paletas no neutrales del
|
||||
# corpus escrito a mano de esta fase. Su saturacion HSL mediana es 75, con p25 en 48 y p10 en 35.
|
||||
# Un piso de 45 queda apenas por debajo del primer cuartil -- lo pasa el 79% de esos fills -- asi
|
||||
# que es un suelo que el comportamiento objetivo despeja comodo, no una aspiracion. La banda de
|
||||
# luminosidad 15..85 excluye casi-negros y casi-blancos, que pueden dar saturacion alta y aun asi
|
||||
# leerse neutros.
|
||||
UMBRALES_VIBRACION = {
|
||||
"chromaticFills": 2,
|
||||
"meanChromaticSaturation": 45,
|
||||
"paletteStructured": True,
|
||||
"finalMessageListsHex": True,
|
||||
}
|
||||
UMBRALES_VIBRACION_NO_APLICA = {k: None for k in UMBRALES_VIBRACION}
|
||||
|
||||
THRESHOLDS = {
|
||||
"g5-01-boton-primario": {
|
||||
"shapeCount": 2, "distinctFillColors": 2, "placeholderGreys": 0,
|
||||
@@ -757,6 +831,7 @@ THRESHOLDS = {
|
||||
"structure": None, "styleRichness": 2, "rootHeightMismatch": 24,
|
||||
"exportBeforeFinal": True, "forbiddenBehavior": 0, "exceptionRate": 0.34,
|
||||
"fontIdBoundShare": 1.0, "rendersOk": True,
|
||||
**UMBRALES_VIBRACION_NO_APLICA,
|
||||
},
|
||||
"g5-02-navbar-flex": {
|
||||
"shapeCount": 6, "distinctFillColors": 3, "placeholderGreys": 0,
|
||||
@@ -765,6 +840,7 @@ THRESHOLDS = {
|
||||
"structure": (2, 1), "styleRichness": 1, "rootHeightMismatch": 24,
|
||||
"exportBeforeFinal": True, "forbiddenBehavior": 0, "exceptionRate": 0.34,
|
||||
"fontIdBoundShare": 1.0, "rendersOk": True,
|
||||
**UMBRALES_VIBRACION_NO_APLICA,
|
||||
},
|
||||
"g5-03-card-producto": {
|
||||
"shapeCount": 6, "distinctFillColors": 3, "placeholderGreys": 0,
|
||||
@@ -773,6 +849,7 @@ THRESHOLDS = {
|
||||
"structure": (2, 1), "styleRichness": 3, "rootHeightMismatch": 32,
|
||||
"exportBeforeFinal": True, "forbiddenBehavior": 0, "exceptionRate": 0.34,
|
||||
"fontIdBoundShare": 1.0, "rendersOk": True,
|
||||
**UMBRALES_VIBRACION_NO_APLICA,
|
||||
},
|
||||
"g5-04-tokens-biblioteca": {
|
||||
"shapeCount": 8, "distinctFillColors": 6, "placeholderGreys": 0,
|
||||
@@ -781,6 +858,7 @@ THRESHOLDS = {
|
||||
"structure": (2, 1), "styleRichness": 2, "rootHeightMismatch": 32,
|
||||
"exportBeforeFinal": True, "forbiddenBehavior": 0, "exceptionRate": 0.34,
|
||||
"fontIdBoundShare": 1.0, "rendersOk": True,
|
||||
**UMBRALES_VIBRACION_NO_APLICA,
|
||||
},
|
||||
"g5-05-grid-tres-diferenciales": {
|
||||
"shapeCount": 12, "distinctFillColors": 3, "placeholderGreys": 0,
|
||||
@@ -789,6 +867,28 @@ THRESHOLDS = {
|
||||
"structure": (3, 3), "styleRichness": 3, "rootHeightMismatch": 32,
|
||||
"exportBeforeFinal": True, "forbiddenBehavior": 0, "exceptionRate": 0.34,
|
||||
"fontIdBoundShare": 1.0, "rendersOk": True,
|
||||
**UMBRALES_VIBRACION_NO_APLICA,
|
||||
},
|
||||
"g5-09-brief-ambiguo-escuela": {
|
||||
"shapeCount": 30, "distinctFillColors": 6, "placeholderGreys": 0,
|
||||
"textsWithCharacters": 12, "typeScale": (3, 32), "textFillShare": 1.0,
|
||||
"layoutBoards": 3, "layoutChildSizingShare": 0.5, "containmentViolations": 0,
|
||||
"structure": (3, 4), "styleRichness": 3, "rootHeightMismatch": 48,
|
||||
"exportBeforeFinal": True, "forbiddenBehavior": 0, "exceptionRate": 0.34,
|
||||
"fontIdBoundShare": 1.0, "rendersOk": True,
|
||||
**UMBRALES_VIBRACION,
|
||||
},
|
||||
"g5-10-brief-ambiguo-notaria": {
|
||||
"shapeCount": 30, "distinctFillColors": 5, "placeholderGreys": 0,
|
||||
"textsWithCharacters": 12, "typeScale": (3, 32), "textFillShare": 1.0,
|
||||
"layoutBoards": 3, "layoutChildSizingShare": 0.5, "containmentViolations": 0,
|
||||
"structure": (3, 4), "styleRichness": 3, "rootHeightMismatch": 48,
|
||||
"exportBeforeFinal": True, "forbiddenBehavior": 0, "exceptionRate": 0.34,
|
||||
"fontIdBoundShare": 1.0, "rendersOk": True,
|
||||
# "elegante" no pide estridencia: alcanza con UN cromatico bien elegido sobre neutrales,
|
||||
# pero la paleta tiene que estar estructurada y declarada igual.
|
||||
"chromaticFills": 1, "meanChromaticSaturation": 30,
|
||||
"paletteStructured": True, "finalMessageListsHex": True,
|
||||
},
|
||||
"g5-06-landing-pizzeria": {
|
||||
"shapeCount": 25, "distinctFillColors": 5, "placeholderGreys": 0,
|
||||
@@ -797,6 +897,7 @@ THRESHOLDS = {
|
||||
"structure": (3, 5), "styleRichness": 5, "rootHeightMismatch": 48,
|
||||
"exportBeforeFinal": True, "forbiddenBehavior": 0, "exceptionRate": 0.34,
|
||||
"fontIdBoundShare": 1.0, "rendersOk": True,
|
||||
**UMBRALES_VIBRACION,
|
||||
},
|
||||
"g5-07-onboarding-mobile": {
|
||||
"shapeCount": 10, "distinctFillColors": 4, "placeholderGreys": 0,
|
||||
@@ -805,6 +906,7 @@ THRESHOLDS = {
|
||||
"structure": (3, 2), "styleRichness": 3, "rootHeightMismatch": 32,
|
||||
"exportBeforeFinal": True, "forbiddenBehavior": 0, "exceptionRate": 0.34,
|
||||
"fontIdBoundShare": 1.0, "rendersOk": True,
|
||||
**UMBRALES_VIBRACION_NO_APLICA,
|
||||
},
|
||||
"g5-08-reparacion-grises": {
|
||||
"shapeCount": 8, "distinctFillColors": 4, "placeholderGreys": 0,
|
||||
@@ -813,6 +915,7 @@ THRESHOLDS = {
|
||||
"structure": (2, 2), "styleRichness": 3, "rootHeightMismatch": 32,
|
||||
"exportBeforeFinal": True, "forbiddenBehavior": 0, "exceptionRate": 0.34,
|
||||
"fontIdBoundShare": 1.0, "rendersOk": True,
|
||||
**UMBRALES_VIBRACION_NO_APLICA,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -838,6 +941,10 @@ def build_raw_metrics(audit, runtime):
|
||||
"exportBeforeFinal": runtime["exportBeforeFinal"],
|
||||
"forbiddenBehavior": runtime["forbiddenBehavior"],
|
||||
"exceptionRate": runtime["exceptionRate"],
|
||||
"chromaticFills": audit.get("chromaticFills", 0),
|
||||
"meanChromaticSaturation": audit.get("meanChromaticSaturation", 0),
|
||||
"paletteStructured": bool(audit.get("paletteStructured", False)),
|
||||
"finalMessageListsHex": runtime["finalMessageListsHex"],
|
||||
"fontIdBoundShare": audit.get("fontIdBoundShare", 0.0),
|
||||
"rendersOk": bool(audit.get("cssLength", 0) > 0 and audit.get("markupLength", 0) > 0
|
||||
and not audit.get("renderError")),
|
||||
@@ -901,9 +1008,64 @@ def load_penpot_tools():
|
||||
|
||||
|
||||
def load_system_prompt():
|
||||
"""El bloque `instructions` verbatim del servidor MCP. Medir con otro system prompt mediria
|
||||
otra cosa: es exactamente lo que el modelo ve en produccion."""
|
||||
return LINT.extract_system_block()
|
||||
"""El bloque `instructions` COMPLETO del servidor MCP, verbatim.
|
||||
|
||||
Medir con otro system prompt mide otra cosa. Y esto se equivoco una vez, caro:
|
||||
|
||||
La primera corrida del baseline inyectaba solo la seccion inicial del documento (la que
|
||||
`penpot_system_prompt.md` guarda como "Bloque completo, verbatim"), que son 2290 de los
|
||||
16396 caracteres reales -- el 14%. El 86% que faltaba es justo el grounding de API:
|
||||
"Core Shape Properties and Methods", "Layout Systems", "Text Elements" y "The `penpot` and
|
||||
`penpotUtils` Objects", o sea insertChild, resize(), los layouts y penpotUtils.
|
||||
|
||||
Con ese recorte, produccion media shapes=0 en los 8 prompts: sin ninguna documentacion de
|
||||
API, el modelo se inventaba una con forma de Figma y todas las llamadas lanzaban. Pero el
|
||||
sintoma que el usuario reporto en su sesion real de Claude Code era otro y menos severo
|
||||
("se conecta y crea cajas grises"), o sea shapes>0. Esa discrepancia era la firma de que el
|
||||
harness, y no el modelo, estaba produciendo el resultado.
|
||||
|
||||
En una sesion real el servidor MCP entrega el documento entero como `instructions`, que es
|
||||
tambien lo que devuelve `high_level_overview`. Asi que la fuente correcta es la Captura 1 de
|
||||
penpot_api_docs.md, completa.
|
||||
|
||||
Nota deliberada sobre los seeds: los 34 seeds que llevan mensaje `system` cargan solo la
|
||||
seccion inicial, no las 4554 tokens del documento entero -- no entrarian en el presupuesto
|
||||
de 3300 tokens por ejemplo. Eso es aceptable porque la contramedida que los seeds tienen que
|
||||
ensenar (la regla de "don't pick your own colours" y el razonamiento que la desambigua) vive
|
||||
entera en esa seccion inicial.
|
||||
"""
|
||||
if os.environ.get("GATE5_SYSTEM_PROMPT_SECTION_ONLY", "").lower() in ("1", "true", "yes"):
|
||||
print("[SYSTEM] usando SOLO la seccion inicial del documento (modo de diagnostico)")
|
||||
return LINT.extract_system_block()
|
||||
|
||||
lineas = (SCHEMAS_DIR / "penpot_api_docs.md").read_text(encoding="utf-8").splitlines()
|
||||
try:
|
||||
inicio = next(n for n, l in enumerate(lineas) if l.strip() == "Salida verbatim:") + 3
|
||||
fin = next(n for n in range(inicio + 5, len(lineas)) if lineas[n].rstrip() == "```")
|
||||
except StopIteration:
|
||||
raise Gate5Error(
|
||||
"no se pudo extraer la Captura 1 (high_level_overview) de penpot_api_docs.md; "
|
||||
"ese bloque es el system prompt real del servidor y sin el la medicion no es fiel"
|
||||
)
|
||||
completo = "\n".join(lineas[inicio:fin])
|
||||
# La captura viene de `high_level_overview`, que cierra con un separador y la frase
|
||||
# "You have hereby read the 'Penpot High-Level Overview' and need not use a tool to read it
|
||||
# again." Eso es framing de la RESPUESTA DE LA TOOL, no parte del bloque `instructions` que
|
||||
# el servidor inyecta. Dejarlo adentro le estaria diciendo al modelo que ya leyo un documento
|
||||
# por haberlo recibido, lo que altera su decision de llamar o no a la tool.
|
||||
corte = completo.rfind("\n--\nYou have hereby read")
|
||||
if corte != -1:
|
||||
completo = completo[:corte]
|
||||
|
||||
seccion = LINT.extract_system_block()
|
||||
if not completo.startswith(seccion[:200]):
|
||||
raise Gate5Error(
|
||||
"la Captura 1 de penpot_api_docs.md no arranca como el bloque de "
|
||||
"penpot_system_prompt.md: uno de los dos archivos se desincronizo"
|
||||
)
|
||||
print(f"[SYSTEM] documento completo del servidor: {len(completo)} chars "
|
||||
f"({len(completo.splitlines())} lineas)")
|
||||
return completo
|
||||
|
||||
|
||||
def chat_completion(cfg, messages, tools):
|
||||
@@ -1073,10 +1235,14 @@ def run_prompt(cfg, mcp, tools, system_prompt, fila, png_dir):
|
||||
except Gate5Error as e:
|
||||
print(f"[WARN] {prompt_id}: no se pudo exportar el PNG ({e})")
|
||||
|
||||
# Ante un brief ambiguo el modelo tiene que ELEGIR una paleta y DECIRSELA al usuario, para
|
||||
# que la pueda ajustar. Una paleta elegida en silencio no es colaborable.
|
||||
hex_en_final = set(HEX_EN_TEXTO.findall(final_content or ""))
|
||||
runtime = {
|
||||
"exportBeforeFinal": bool(export_calls),
|
||||
"forbiddenBehavior": len(veto_hits),
|
||||
"exceptionRate": (exec_exceptions / exec_calls) if exec_calls else 0.0,
|
||||
"finalMessageListsHex": len(hex_en_final) >= 3,
|
||||
}
|
||||
raw = build_raw_metrics(audit, runtime)
|
||||
resultado = score_prompt(prompt_id, raw)
|
||||
|
||||
Reference in New Issue
Block a user