Phase 6.3: fix the augmentation, close the holdout leak, stop rewarding invented parameters
Dataset build (05, 06): perturb_value is gone. It rewrote only tool_calls.arguments and left the tool results and the final answer saying something else, which is how data/train.jsonl ended up with 30 self-contradictory examples where the call says issue_number 82 and the answer says issue #77. Variation now comes from hand-written meta.paraphrases, or from meta.variation applied atomically across every field of the example at once. Nothing is substituted unless the seed declares it: guessing which number in a string is safe to change is what produced the contradictions in the first place. Prefix injection survives only as a fallback and only where the verb form can actually be conjugated, and there is a hard assert that no user turn matches the broken "Necesito que ¿Podés..." shape that 68 v1 prompts had. The penpot bucket is exempt from substitution entirely, since its payloads are code. Also asserts the bucket cannot collapse (verified: the old seeds give 320 rows from 83 unique trajectories and the build now fails) and scans for forbidden API patterns by importing them from the linter, so there is one source of truth. 06 now actually exits 1 on over-length rows. It printed [FILTERED], incremented a counter, and left the row in the file, which 10_train.py then trained on since it has no max_seq_length and batch 1. Gate 2 (32): reject any argument key absent from the schema, as its own failure category. It only checked required fields, so an invented scale or filePath passed - the gate was actively rewarding the exact behaviour this phase removes. Verified: export_shape with scale=2 now fails as unknown_argument, while a valid call still passes. Holdout (31, 35): rebalanced to penpot 60 / 35 each, added 20 real design templates, and replaced the full-string equality check with 6-gram shingles. Measured: a light paraphrase of a train.jsonl prompt scores 43% overlap and now fails the build, where the old check let it through at "not equal". Value pools are asserted disjoint from the corpus. The "2x resolution" template stays, relabelled as an invented-argument probe now that gate 2 can detect one; the createBoolean template stays because the API is real and the new B2 seeds teach it. Also dedupes: the old holdout had 15 duplicate prompts out of 200, i.e. 15 wasted measurements. Note: rebalancing the holdout means the 192/200 gate 2 baseline from phase 5 no longer applies to it, so that baseline has to be re-measured against production on the new file before it can be compared to. Gate 3 (33): 11 content checklists for the non-obvious conventions of the other MCPs - GFM table separators in Docmost, the update_page staleness retry, commit message shape, never merging the PR, dict-not-XML tool arguments. That is the most likely regression no gate currently covers. Mix builder: added the anti-collapse guard, so 420 new-portion rows that are really 96 trajectories repeated cannot pass unnoticed.
This commit is contained in:
@@ -0,0 +1,289 @@
|
||||
"""Fase 6 -- genera data/holdout_penpot_design.jsonl: ~60 prompts de diseno de Penpot,
|
||||
held-out, deterministicos (seed=44).
|
||||
|
||||
Son los prompts sobre los que se mide la **tasa de API prohibida** de la puerta 5, que es el
|
||||
disparador de fallback de la fase: si sobre estos 60 prompts el checkpoint v2-bf16 usa API
|
||||
inexistente en mas del 5% de los casos (mas de 3 de 60), no se sigue con la cuantizacion y
|
||||
se cambia a reentrenar desde el base con los seeds ya corregidos.
|
||||
|
||||
python3 scripts/35_build_penpot_design_holdout.py
|
||||
|
||||
Cada linea: {"id", "prompt", "categoria", "expect"}. No lleva schemas de tools: la puerta 5
|
||||
corre un loop de agente real contra el MCP en vivo y arma ella misma el payload.
|
||||
|
||||
DISJUNCION
|
||||
----------
|
||||
Se verifica contra data/train.jsonl, data/eval.jsonl y todos los seeds de
|
||||
data/raw/seeds/ con el MISMO chequeo de shingles de 6-gramas que usa
|
||||
scripts/31_build_holdout_prompts.py -- se importa de ahi a proposito, para que no haya dos
|
||||
definiciones de "contaminado" que puedan divergir. Como el nombre del modulo empieza con un
|
||||
digito no se puede importar con `import`; se carga por ruta con importlib.
|
||||
|
||||
Los prompts mezclan a proposito dos registros: pedidos vagos ("algo lindo para...") y
|
||||
pedidos con medidas, colores y copy exactos. El modelo tiene que rendir en los dos: el fallo
|
||||
de produccion que origina la fase ("hazme una landing de una pizzeria con colores vibrantes")
|
||||
es del registro vago.
|
||||
"""
|
||||
import importlib.util
|
||||
import json
|
||||
import random
|
||||
from pathlib import Path
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parent.parent
|
||||
SEEDS_DIR = REPO_ROOT / "data" / "raw" / "seeds"
|
||||
TRAIN_PATH = REPO_ROOT / "data" / "train.jsonl"
|
||||
EVAL_PATH = REPO_ROOT / "data" / "eval.jsonl"
|
||||
OUT_PATH = REPO_ROOT / "data" / "holdout_penpot_design.jsonl"
|
||||
BUILDER_PATH = REPO_ROOT / "scripts" / "31_build_holdout_prompts.py"
|
||||
|
||||
SEED = 44
|
||||
TARGET_TOTAL = 60
|
||||
|
||||
|
||||
def load_builder():
|
||||
"""Carga 31_build_holdout_prompts.py por ruta (el nombre empieza con digito)."""
|
||||
spec = importlib.util.spec_from_file_location("holdout_builder", BUILDER_PATH)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
BUILDER = load_builder()
|
||||
normalize = BUILDER.normalize
|
||||
shingles = BUILDER.shingles
|
||||
shingle_overlap = BUILDER.shingle_overlap
|
||||
assert_no_contamination = BUILDER.assert_no_contamination
|
||||
MAX_SHINGLE_OVERLAP = BUILDER.MAX_SHINGLE_OVERLAP
|
||||
|
||||
# Pools propios de este holdout: ni los seeds ni el holdout de la puerta 2 los usan.
|
||||
# La disjuncion se asierta contra el corpus antes de generar nada.
|
||||
DOMINIOS = [
|
||||
"una escuela de circo",
|
||||
"un servicio de reparacion de relojes",
|
||||
"una cooperativa de miel",
|
||||
"un estudio de doblaje",
|
||||
"una tienda de kayaks",
|
||||
"un laboratorio de analisis de suelos",
|
||||
"una academia de tango",
|
||||
"un albergue para perros mayores",
|
||||
"una fabrica de velas de soja",
|
||||
"un museo de maquinas de escribir",
|
||||
"una consultora de eficiencia energetica",
|
||||
"un vivero de orquideas",
|
||||
]
|
||||
COLORES = ["#134e4a", "#3b0764", "#312e81", "#713f12", "#581c87", "#155e75"]
|
||||
ANCHOS = [1180, 1240, 1320, 900]
|
||||
MOBILE = ["390x844", "412x915", "360x780"]
|
||||
# Fuentes reales de Google Fonts (Penpot expone 1914), elegidas entre las que NINGUN seed
|
||||
# nombra: si el holdout pidiera una fuente que el training ya usa, no mediria nada nuevo.
|
||||
TIPOGRAFIAS = ["Libre Baskerville", "Karla", "Asap"]
|
||||
|
||||
# (categoria, plantilla, expect). `expect` describe que tiene que pasar para que el prompt
|
||||
# cuente como resuelto; la puerta 5 lo usa para elegir el subconjunto de metricas aplicable.
|
||||
PLANTILLAS = [
|
||||
# --- composicion multiseccion: la clase del fallo de produccion --------------------
|
||||
("composicion_multiseccion",
|
||||
"Hazme una landing completa de {dominio}, {ancho} px de ancho, con encabezado, hero, tres beneficios, testimonios y pie. Ponele color, nada de cajas grises.",
|
||||
"composicion-multiseccion-con-color"),
|
||||
("composicion_multiseccion",
|
||||
"Necesito la home de {dominio} en {ancho} px: hero con foto de fondo, una franja de numeros y un bloque final de contacto. Paleta a tu criterio, que sea vibrante.",
|
||||
"composicion-multiseccion-con-color"),
|
||||
("composicion_multiseccion",
|
||||
"Armame algo lindo para {dominio}, una pagina de presentacion. Vos elegis todo.",
|
||||
"composicion-multiseccion-con-color"),
|
||||
("composicion_multiseccion",
|
||||
"Pagina de precios de {dominio} de {ancho} px: encabezado, tres planes con el del medio destacado en {color}, preguntas frecuentes y pie.",
|
||||
"composicion-multiseccion-con-color"),
|
||||
("composicion_multiseccion",
|
||||
"Quiero la pantalla principal del panel interno de {dominio}, {ancho} px, con barra lateral, seis tarjetas de indicadores y una tabla de movimientos.",
|
||||
"composicion-multiseccion-con-color"),
|
||||
("composicion_multiseccion",
|
||||
"Landing de un evento de {dominio}: fecha grande, cuenta regresiva, grilla de oradores y formulario de inscripcion, {ancho} px de ancho.",
|
||||
"composicion-multiseccion-con-color"),
|
||||
# --- componente acotado -----------------------------------------------------------
|
||||
("componente",
|
||||
"Un boton primario y uno secundario para {dominio}, ambos con estados normal y deshabilitado, usando {color} como acento.",
|
||||
"componente-con-estilo-explicito"),
|
||||
("componente",
|
||||
"Card de producto para {dominio}: foto arriba, titulo en {tipografia}, precio tachado y precio final, y un boton. 320 de ancho.",
|
||||
"componente-con-estilo-explicito"),
|
||||
("componente",
|
||||
"Hazme una barra de navegacion de {dominio} de {ancho}x88 con logo, cinco enlaces y un boton de accion a la derecha.",
|
||||
"componente-con-estilo-explicito"),
|
||||
("componente",
|
||||
"Un aviso de cookies para el sitio de {dominio}: texto corto, dos botones y fondo {color}.",
|
||||
"componente-con-estilo-explicito"),
|
||||
("componente",
|
||||
"Necesito una tarjeta de perfil de {dominio} con avatar circular, nombre, rol y tres datos de contacto.",
|
||||
"componente-con-estilo-explicito"),
|
||||
("componente",
|
||||
"Necesito una tabla de {ancho} px para {dominio}: encabezado oscuro, cinco filas con fondo alternado y una columna de estado con etiquetas de color.",
|
||||
"componente-con-estilo-explicito"),
|
||||
# --- pantallas mobile -------------------------------------------------------------
|
||||
("imagen",
|
||||
"Pantalla de {mobile} para la app de {dominio}: foto real de portada traida de una URL, titulo encima y boton abajo.",
|
||||
"imagen-por-uploadmediaurl"),
|
||||
("imagen",
|
||||
"Quiero una galeria de cuatro fotos reales de internet en la app de {dominio}, formato {mobile}, con leyenda en cada una.",
|
||||
"imagen-por-uploadmediaurl"),
|
||||
("imagen",
|
||||
"Meteme una imagen de fondo en el hero de {dominio} y encima un velo oscuro para que se lea el titular en blanco.",
|
||||
"imagen-por-uploadmediaurl"),
|
||||
("imagen",
|
||||
"Para la ficha de {dominio} necesito la foto del producto ocupando toda la tarjeta sin deformarse, {ancho} px de ancho.",
|
||||
"imagen-por-uploadmediaurl"),
|
||||
# --- layout: flex y grid ----------------------------------------------------------
|
||||
("layout",
|
||||
"Fila de tres tarjetas de {dominio} que se repartan el ancho de {ancho} px en partes iguales, con separacion pareja entre ellas.",
|
||||
"layout-con-sizing-declarado"),
|
||||
("layout",
|
||||
"Grilla de {ancho} px con seis casillas para el catalogo de {dominio}, tres por fila, todas del mismo alto.",
|
||||
"layout-con-sizing-declarado"),
|
||||
("layout",
|
||||
"Una columna de {dominio} donde el titulo va arriba, el texto en el medio y el boton abajo del todo, y el contenedor se ajusta al contenido.",
|
||||
"layout-con-sizing-declarado"),
|
||||
("layout",
|
||||
"Pie de pagina de {ancho} px para {dominio} con cuatro columnas de enlaces y una linea final de derechos.",
|
||||
"layout-con-sizing-declarado"),
|
||||
("layout",
|
||||
"Pantalla de {mobile} de {dominio} con una lista vertical de siete items, cada uno con icono a la izquierda y texto a la derecha.",
|
||||
"layout-con-sizing-declarado"),
|
||||
# --- reparacion de disenos grises -------------------------------------------------
|
||||
("reparacion",
|
||||
"En la pagina hay un board de {dominio} que quedo con puros rectangulos grises y sin una sola palabra. Convertilo en un diseno de verdad.",
|
||||
"reparacion-sin-grises"),
|
||||
("reparacion",
|
||||
"Este board de {dominio} tiene todos los textos del mismo tamano y todo el mismo gris. Dale jerarquia y una paleta.",
|
||||
"reparacion-sin-grises"),
|
||||
("reparacion",
|
||||
"Revisa el board de {dominio} que arme yo, decime que esta mal de contraste y arreglalo.",
|
||||
"reparacion-sin-grises"),
|
||||
("reparacion",
|
||||
"El hero de {dominio} tiene el texto ilegible sobre la foto. Arreglalo sin cambiar la foto.",
|
||||
"reparacion-sin-grises"),
|
||||
# --- consulta de API antes de actuar ----------------------------------------------
|
||||
("consulta_api",
|
||||
"Antes de tocar nada, fijate en la documentacion de la API como se crea un texto en Penpot y despues poneme un titulo de {dominio} en 48 px.",
|
||||
"consulta-api-antes-de-actuar"),
|
||||
("consulta_api",
|
||||
"Como hago para que un hijo de un board con layout ocupe todo el ancho disponible? Mostramelo aplicado a una seccion de {dominio}.",
|
||||
"consulta-api-antes-de-actuar"),
|
||||
("consulta_api",
|
||||
"Quiero usar la tipografia {tipografia} en el titulo de {dominio}. Verifica que quede realmente aplicada, no solo seteada.",
|
||||
"consulta-api-antes-de-actuar"),
|
||||
("consulta_api",
|
||||
"Guarda la paleta de {dominio} como colores de la biblioteca del archivo y despues aplicalos a dos formas.",
|
||||
"consulta-api-antes-de-actuar"),
|
||||
("consulta_api",
|
||||
"Exportame el board de {dominio} para verlo, y decime si algo quedo fuera de los limites.",
|
||||
"consulta-api-antes-de-actuar"),
|
||||
]
|
||||
|
||||
|
||||
def load_corpus():
|
||||
"""Textos de usuario y shingles de train + eval + todos los seeds."""
|
||||
paths = [TRAIN_PATH, EVAL_PATH]
|
||||
paths += sorted(SEEDS_DIR.glob("*.jsonl"))
|
||||
paths += sorted((SEEDS_DIR / "_parts").glob("*.jsonl"))
|
||||
return BUILDER.load_reference_corpus(paths)
|
||||
|
||||
|
||||
def load_corpus_text():
|
||||
chunks = []
|
||||
for path in [TRAIN_PATH, EVAL_PATH] + sorted(SEEDS_DIR.glob("*.jsonl")) + \
|
||||
sorted((SEEDS_DIR / "_parts").glob("*.jsonl")):
|
||||
if path.exists():
|
||||
chunks.append(path.read_text(encoding="utf-8"))
|
||||
return normalize(" ".join(chunks))
|
||||
|
||||
|
||||
def assert_pools_disjuntos(corpus_text):
|
||||
"""Ningun valor de los pools aparece en los seeds/train/eval ni en el holdout de la
|
||||
puerta 2 (scripts/31_build_holdout_prompts.py)."""
|
||||
pools = {
|
||||
"dominios": DOMINIOS, "colores": COLORES, "mobile": MOBILE, "tipografias": TIPOGRAFIAS,
|
||||
}
|
||||
pools_gate2 = {
|
||||
"dominios": BUILDER.DOMINIOS, "colores": BUILDER.COLORS,
|
||||
"mobile": [], "tipografias": [],
|
||||
}
|
||||
for nombre, valores in pools.items():
|
||||
repetidos = sorted(set(valores) & set(pools_gate2[nombre]))
|
||||
if repetidos:
|
||||
raise AssertionError(
|
||||
f"pool '{nombre}' comparte valores con el holdout de la puerta 2: {repetidos}"
|
||||
)
|
||||
for valor in valores:
|
||||
if normalize(valor) in corpus_text:
|
||||
raise AssertionError(
|
||||
f"valor '{valor}' del pool '{nombre}' aparece en los seeds/train/eval"
|
||||
)
|
||||
repetidos = sorted(set(ANCHOS) & set(BUILDER.WIDTHS))
|
||||
if repetidos:
|
||||
raise AssertionError(f"pool 'anchos' comparte valores con el holdout de la puerta 2: {repetidos}")
|
||||
print(f"[INFO] pools disjuntos verificados ({sum(len(v) for v in pools.values())} valores)")
|
||||
|
||||
|
||||
def render(rng, plantilla):
|
||||
return plantilla.format(
|
||||
dominio=rng.choice(DOMINIOS),
|
||||
color=rng.choice(COLORES),
|
||||
ancho=rng.choice(ANCHOS),
|
||||
mobile=rng.choice(MOBILE),
|
||||
tipografia=rng.choice(TIPOGRAFIAS),
|
||||
)
|
||||
|
||||
|
||||
def main():
|
||||
rng = random.Random(SEED)
|
||||
existing_texts, reference_shingles = load_corpus()
|
||||
print(f"[INFO] corpus de referencia: {len(existing_texts)} turnos de usuario, "
|
||||
f"{len(reference_shingles)} shingles")
|
||||
|
||||
assert_pools_disjuntos(load_corpus_text())
|
||||
|
||||
rows = []
|
||||
seen = set()
|
||||
max_overlap = 0.0
|
||||
# Dos pasadas sobre las 30 plantillas -> 60 prompts, con valores distintos en cada una.
|
||||
for vuelta in range(TARGET_TOTAL // len(PLANTILLAS)):
|
||||
for idx, (categoria, plantilla, expect) in enumerate(PLANTILLAS):
|
||||
for _ in range(200):
|
||||
prompt = render(rng, plantilla)
|
||||
if prompt not in seen:
|
||||
break
|
||||
else:
|
||||
raise AssertionError(f"sin combinaciones nuevas para la plantilla {plantilla!r}")
|
||||
seen.add(prompt)
|
||||
overlap = assert_no_contamination(
|
||||
prompt, existing_texts, reference_shingles, origen=" de diseno"
|
||||
)
|
||||
max_overlap = max(max_overlap, overlap)
|
||||
rows.append({
|
||||
"id": f"pd-{vuelta * len(PLANTILLAS) + idx + 1:03d}",
|
||||
"prompt": prompt,
|
||||
"categoria": categoria,
|
||||
"expect": expect,
|
||||
})
|
||||
|
||||
if len(rows) < TARGET_TOTAL:
|
||||
raise AssertionError(f"solo se generaron {len(rows)} prompts, se esperaban {TARGET_TOTAL}")
|
||||
if len(set(r["prompt"] for r in rows)) != len(rows):
|
||||
raise AssertionError("hay prompts duplicados en el holdout de diseno")
|
||||
|
||||
with open(OUT_PATH, "w", encoding="utf-8") as f:
|
||||
for row in rows:
|
||||
f.write(json.dumps(row, ensure_ascii=False) + "\n")
|
||||
|
||||
print(f"[INFO] {len(rows)} prompts de diseno escritos en {OUT_PATH}")
|
||||
print(f"[INFO] solapamiento maximo de shingles: {max_overlap:.0%} "
|
||||
f"(umbral {MAX_SHINGLE_OVERLAP:.0%})")
|
||||
for categoria in sorted({r["categoria"] for r in rows}):
|
||||
n = sum(1 for r in rows if r["categoria"] == categoria)
|
||||
print(f" {categoria}: {n}")
|
||||
print(f"[INFO] umbral de la puerta 5: tasa de API prohibida <= 5% "
|
||||
f"({int(0.05 * len(rows))}/{len(rows)})")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user