One parentless commit with only the files that the open seed tasks touch and the modules they import. The full squashed import (SPEC 8.6) is a later step: this tree is not the cutoff tree. T2T-StandIn-Source-Commit: 43f95b7333efe73aeaebf806023f17bffea27aba
575 lines
28 KiB
JavaScript
575 lines
28 KiB
JavaScript
// Elffuss Lab — banco de pruebas del propio producto.
|
|
//
|
|
// Tres cosas, en la misma mesa:
|
|
// 1) ELEGIR algoritmo de generación (directo / RLM map-reduce / RLM con
|
|
// crítica) y ENCENDER o APAGAR cada perilla del compresor de contexto.
|
|
// 2) LANZAR benchmarks reales y ver el progreso caso a caso, en vivo.
|
|
// 3) GUARDAR la corrida con su TRAZA en un historial y un ranking LOCALES:
|
|
// todo se queda en esta máquina, no se envía nada a ningún sitio.
|
|
//
|
|
// Nada de esto simula: el banco de contexto llama al `packHistoryACER` de
|
|
// verdad, el de SWE ejecuta los tests de verdad contra el módulo arreglado, y
|
|
// el de juegos ejecuta el juego generado y lo puntúa mirándolo correr.
|
|
import { DEFAULTS, packHistoryACER, estimateTokens } from './acer-core.js';
|
|
import { TASKS } from './swe-tasks.js';
|
|
|
|
const $ = s => document.querySelector(s);
|
|
const el = (t, c, x) => { const n = document.createElement(t); if (c) n.className = c; if (x != null) n.textContent = x; return n; };
|
|
|
|
// ─────────────────────────── estado ───────────────────────────
|
|
const S = {
|
|
brain: localStorage.getItem('elffusscode.model') || 'rules',
|
|
algo: 'directo',
|
|
algoOpts: { rounds: 1, chunkTokens: 1800, maxChunks: 24 },
|
|
ctx: {}, // overrides sobre DEFAULTS
|
|
bench: 'contexto',
|
|
provider: null,
|
|
running: false, abort: false,
|
|
run: null, // { bench, casos:[], métricas:{} } — la traza
|
|
};
|
|
|
|
// Perillas del compresor que tiene sentido tocar a mano (las demás quedan en su
|
|
// valor por defecto). El «porqué» sale del propio acer-core.
|
|
const CTX_KNOBS = [
|
|
['recuperación', [
|
|
['SEMANTIC', 'bool', 'fusiona embeddings con BM25 — INERTE en el banco de contexto (ese usa el packer sin embeddings)'],
|
|
['MMR', 'bool', 'diversifica lo recuperado (evita repetir lo mismo)'],
|
|
['DEDUP', 'bool', 'quita mensajes casi idénticos'],
|
|
['PIN_QUERY_TERMS', 'bool', 'clava los términos de la pregunta'],
|
|
['SUPERSEDE', 'bool', 'lo nuevo pisa a lo viejo cuando se contradicen'],
|
|
['RECENCY_WEIGHT', 'num', 'peso de lo reciente al puntuar'],
|
|
]],
|
|
['presupuesto', [
|
|
['ELASTIC', 'bool', 'estira/encoge según la presión real'],
|
|
['RECENT', 'num', 'nº de turnos recientes intocables'],
|
|
['TAIL_MIN_FRAC', 'num', 'suelo garantizado para la cola'],
|
|
['HEAD_FRAC', 'num', 'reserva para el arranque (0 = apagada)'],
|
|
['MAX_MSG_CHARS', 'num', 'recorte por mensaje'],
|
|
]],
|
|
['extras', [
|
|
['DATES', 'bool', 'anota «ayer» con la fecha del turno'],
|
|
['SUMMARY', 'bool', 'tarjeta de recuento (cuesta presupuesto)'],
|
|
['AUTO', 'bool', 'deriva las perillas de lo medido en ESTE historial'],
|
|
]],
|
|
];
|
|
|
|
const ALGOS = [
|
|
['directo', 'Directo', 'Una sola inferencia. El baremo contra el que se compara todo.'],
|
|
['rlm-deep', 'RLM · crear y criticar', 'Borrador → crítica → reescritura, N rondas (deepCreate).'],
|
|
['rlm-hard', 'RLM · Hard Work', 'Trocea el material, pregunta a cada trozo y funde (map-reduce).'],
|
|
];
|
|
|
|
const BENCHES = [
|
|
['contexto', 'Contexto (ACER)', 'Determinista y sin GPU: mide qué evidencia SOBREVIVE al compresor y a qué precio en tokens. Reacciona a las perillas al instante.'],
|
|
['swe', 'SWE-bench-style', `${TASKS.length} repos con un bug real: el agente lo arregla con sus tools y se EJECUTA el test. Métrica: resolved/N.`],
|
|
['juegos', 'Juegos (generar y jugar)', 'Genera un juego con el algoritmo elegido, lo ejecuta y lo puntúa: carga sin errores, arranca, se mueve y aguanta jugando.'],
|
|
];
|
|
|
|
const BRAINS = [
|
|
['rules', 'Reglas (sin GPU)', 'Determinista. Valida el arnés sin bajar pesos.'],
|
|
['onnx', 'Elffuss LM (CPU/wasm)', 'LFM2.5-1.2B por transformers.js.'],
|
|
['litert:gemma-e2b', 'Gemma-4 E2B (~2 GB)', 'WebGPU.'],
|
|
['litert:gemma-e4b', 'Gemma-4 E4B (~3 GB)', 'WebGPU. El mejor.'],
|
|
];
|
|
|
|
// ─────────────────────────── consola ───────────────────────────
|
|
function log(msg, cls = '') {
|
|
const t = new Date().toTimeString().slice(0, 8);
|
|
const line = el('div');
|
|
line.appendChild(el('span', 't', t + ' '));
|
|
line.appendChild(el('span', cls, msg));
|
|
$('#log').appendChild(line);
|
|
$('#log').scrollTop = $('#log').scrollHeight;
|
|
}
|
|
const setLed = (id, cls, txt) => { const n = $(id); n.className = 'led ' + cls; n.querySelector('span').textContent = txt; };
|
|
const prog = (done, total, txt) => {
|
|
$('#prog').style.width = total ? Math.round(done / total * 100) + '%' : '0';
|
|
$('#prog-txt').textContent = txt || (total ? `${done}/${total}` : '—');
|
|
};
|
|
|
|
function tiles(obj) {
|
|
const box = $('#tiles'); box.innerHTML = '';
|
|
for (const [k, v] of Object.entries(obj)) {
|
|
const t = el('div', 'tile' + (v && v.bad ? ' bad' : v && v.warn ? ' warn' : ''));
|
|
t.appendChild(el('div', 'k', k));
|
|
const val = (v && typeof v === 'object') ? v.v : v;
|
|
t.appendChild(el('div', 'v' + (String(val).length > 7 ? ' small' : ''), String(val)));
|
|
box.appendChild(t);
|
|
}
|
|
}
|
|
function table(cols, rows) {
|
|
$('#res-head').innerHTML = '<tr>' + cols.map(c => `<th>${c}</th>`).join('') + '</tr>';
|
|
$('#res-body').innerHTML = '';
|
|
rows.forEach(addRow);
|
|
}
|
|
function addRow(cells) {
|
|
const tr = el('tr');
|
|
cells.forEach(c => {
|
|
const td = el('td');
|
|
if (c && typeof c === 'object') { td.className = c.cls || ''; td.textContent = c.t; }
|
|
else td.textContent = c;
|
|
tr.appendChild(td);
|
|
});
|
|
$('#res-body').appendChild(tr);
|
|
return tr;
|
|
}
|
|
|
|
// ─────────────────────────── interfaz ───────────────────────────
|
|
function radio(host, items, current, onPick) {
|
|
host.innerHTML = '';
|
|
items.forEach(([id, title, desc]) => {
|
|
const l = el('label', id === current ? 'sel' : '');
|
|
const i = el('input'); i.type = 'radio'; i.name = host.id; i.checked = id === current;
|
|
i.onchange = () => { onPick(id); radio(host, items, id, onPick); };
|
|
const d = el('div');
|
|
d.appendChild(el('div', 't', title));
|
|
d.appendChild(el('div', 'd', desc));
|
|
l.append(i, d); host.appendChild(l);
|
|
});
|
|
}
|
|
|
|
function ctxUI() {
|
|
const host = $('#ctx-knobs'); host.innerHTML = '';
|
|
for (const [group, knobs] of CTX_KNOBS) {
|
|
host.appendChild(el('div', 'group-title', group));
|
|
const box = el('div', 'knobs');
|
|
for (const [key, type, why] of knobs) {
|
|
const row = el('div', 'knob');
|
|
const cur = key in S.ctx ? S.ctx[key] : DEFAULTS[key];
|
|
const inp = el('input');
|
|
if (type === 'bool') { inp.type = 'checkbox'; inp.checked = !!cur; }
|
|
else { inp.type = 'number'; inp.step = 'any'; inp.value = cur ?? 0; }
|
|
inp.onchange = () => {
|
|
S.ctx[key] = type === 'bool' ? inp.checked : Number(inp.value);
|
|
ctxTag();
|
|
if (S.bench === 'contexto') log('perilla ' + key + ' → ' + S.ctx[key] + ' (relanza para medir)', 'w');
|
|
};
|
|
row.append(inp, el('code', '', key), el('div', 'why', why));
|
|
box.appendChild(row);
|
|
}
|
|
host.appendChild(box);
|
|
}
|
|
ctxTag();
|
|
}
|
|
function ctxTag() {
|
|
const on = CTX_KNOBS.flatMap(([, k]) => k).filter(([key, t]) => t === 'bool' && (key in S.ctx ? S.ctx[key] : DEFAULTS[key])).length;
|
|
$('#ctx-tag').textContent = on + ' activas';
|
|
}
|
|
function algoKnobs() {
|
|
const host = $('#algo-knobs'); host.innerHTML = '';
|
|
const defs = S.algo === 'rlm-hard'
|
|
? [['chunkTokens', 'tamaño de cada trozo'], ['maxChunks', 'techo de sub-llamadas']]
|
|
: S.algo === 'rlm-deep' ? [['rounds', 'rondas de crítica + reescritura']] : [];
|
|
if (!defs.length) { host.appendChild(el('div', 'hint', 'El modo directo no tiene parámetros: una inferencia y ya.')); return; }
|
|
const box = el('div', 'knobs');
|
|
defs.forEach(([k, why]) => {
|
|
const row = el('div', 'knob');
|
|
const i = el('input'); i.type = 'number'; i.value = S.algoOpts[k]; i.min = 0;
|
|
i.onchange = () => { S.algoOpts[k] = Number(i.value); };
|
|
row.append(i, el('code', '', k), el('div', 'why', why));
|
|
box.appendChild(row);
|
|
});
|
|
host.appendChild(box);
|
|
}
|
|
|
|
// ─────────────────────────── cerebro ───────────────────────────
|
|
async function loadBrain() {
|
|
const id = S.brain;
|
|
$('#btn-load').disabled = true;
|
|
setLed('#led-model', 'busy', 'cargando ' + id + '…');
|
|
try {
|
|
let mod;
|
|
if (id === 'rules') mod = await import('./providers/rules.js');
|
|
else if (id === 'onnx') mod = await import('./providers/onnx.js');
|
|
else if (id.startsWith('litert:')) {
|
|
mod = await import('./providers/litert.js');
|
|
mod.configure(id.split(':')[1]);
|
|
}
|
|
if (mod.load) await mod.load(s => { $('#load-state').textContent = String(s).slice(0, 60); });
|
|
S.provider = mod;
|
|
setLed('#led-model', 'on', 'modelo: ' + id);
|
|
$('#load-state').textContent = '';
|
|
log('cerebro listo: ' + id, 'a');
|
|
} catch (e) {
|
|
setLed('#led-model', 'err', 'fallo al cargar');
|
|
log('no se pudo cargar el cerebro: ' + e.message, 'e');
|
|
}
|
|
$('#btn-load').disabled = false;
|
|
}
|
|
|
|
// ═══════════════ BANCO 1 · contexto (determinista, sin GPU) ═══════════════
|
|
// Construye un historial largo con HECHOS plantados y ruido alrededor, lo pasa
|
|
// por el compresor con las perillas elegidas y mide qué evidencia sobrevive y
|
|
// cuántos tokens cuesta. Sin modelo: mide el COMPRESOR, no al que responde.
|
|
// Cada caso trae: el dato BUENO, una versión CADUCADA del mismo dato (dicha
|
|
// antes) y distractores que comparten vocabulario con la pregunta. Así el banco
|
|
// no premia «meter mucho», sino traer lo correcto y dejar fuera lo viejo — que
|
|
// es lo que de verdad hacen SUPERSEDE, PIN_QUERY_TERMS o DEDUP.
|
|
const HECHOS = [
|
|
{ bueno: 'Ahora el servidor de pagos escucha en el puerto 7421.', viejo: 'El servidor de pagos escuchaba en el puerto 3000.',
|
|
q: '¿en qué puerto escucha el servidor de pagos?', clave: '7421', claveVieja: '3000',
|
|
ruido: ['El servidor de correo va por otro puerto distinto.', 'Ese puerto del router no tiene nada que ver con pagos.'] },
|
|
{ bueno: 'La clave de despliegue caduca el 3 de marzo.', viejo: 'La clave de despliegue caducaba el 9 de enero.',
|
|
q: '¿cuándo caduca la clave de despliegue?', clave: '3 de marzo', claveVieja: '9 de enero',
|
|
ruido: ['La clave del wifi la cambiaron en verano.', 'El despliegue de ayer no tocó ninguna clave.'] },
|
|
{ bueno: 'Nordvik factura ahora en coronas.', viejo: 'Nordvik facturaba en euros.',
|
|
q: '¿en qué moneda factura Nordvik?', clave: 'coronas', claveVieja: 'facturaba en euros',
|
|
ruido: ['Nordvik cambió de comercial el trimestre pasado.', 'A otros clientes se les factura en euros.'] },
|
|
{ bueno: 'El informe trimestral lo firma Beatriz.', viejo: 'El informe trimestral lo firmaba Andrés.',
|
|
q: '¿quién firma el informe trimestral?', clave: 'firma Beatriz', claveVieja: 'firmaba Andrés',
|
|
ruido: ['Andrés sigue en la empresa, pero en otro equipo.', 'Beatriz también revisa el informe mensual.'] },
|
|
{ bueno: 'El backup nocturno corre a las 02:30.', viejo: 'El backup nocturno corría a las 23:00.',
|
|
q: '¿a qué hora corre el backup nocturno?', clave: '02:30', claveVieja: '23:00',
|
|
ruido: ['El backup semanal es otra tarea distinta.', 'A las 23:00 lo que hay es el corte de logs.'] },
|
|
{ bueno: 'El almacén de Lugo pasó a Vigo.', viejo: 'El almacén principal estaba en Lugo.',
|
|
q: '¿a dónde se movió el almacén de Lugo?', clave: 'pasó a Vigo', claveVieja: 'estaba en Lugo',
|
|
ruido: ['En Lugo queda solo una oficina comercial.', 'El almacén de Vigo ya existía para otra cosa.'] },
|
|
];
|
|
const RUIDO = [
|
|
'Vale, seguimos mañana con eso.', 'Te paso el enlace en un momento.',
|
|
'Creo que el diseño nuevo se ve mejor.', 'Nos vemos en la reunión de las cinco.',
|
|
'Ese ticket lo cerró soporte la semana pasada.', 'Prefiero no tocar eso hoy.',
|
|
'Lo dejo apuntado para el lunes.', 'Sí, ya lo hemos hablado antes.',
|
|
];
|
|
|
|
// Historial LARGO: el dato bueno queda enterrado y fuera de la ventana reciente.
|
|
function historialCon(c, largo = 420) {
|
|
const h = [];
|
|
const posViejo = Math.floor(largo * 0.10);
|
|
const posBueno = Math.floor(largo * 0.55);
|
|
const posRuido = [Math.floor(largo * 0.30), Math.floor(largo * 0.75)];
|
|
for (let i = 0; i < largo; i++) {
|
|
let txt = RUIDO[i % RUIDO.length] + ' (' + i + ')';
|
|
if (i === posViejo) txt = c.viejo;
|
|
else if (i === posBueno) txt = c.bueno;
|
|
else if (i === posRuido[0]) txt = c.ruido[0];
|
|
else if (i === posRuido[1]) txt = c.ruido[1];
|
|
h.push({ role: i % 2 ? 'assistant' : 'user', content: txt, ts: Date.now() - (largo - i) * 60000 });
|
|
}
|
|
return h;
|
|
}
|
|
|
|
async function benchContexto(onCase) {
|
|
const opts = { ...DEFAULTS, ...S.ctx };
|
|
const budget = 320; // muy apretado: obliga a ELEGIR
|
|
const filas = [];
|
|
for (let i = 0; i < HECHOS.length; i++) {
|
|
if (S.abort) break;
|
|
const c = HECHOS[i];
|
|
const hist = historialCon(c);
|
|
hist.push({ role: 'user', content: c.q, ts: Date.now() });
|
|
const { messages } = packHistoryACER(hist, budget, opts);
|
|
const texto = messages.map(m => m.content).join('\n');
|
|
const sobrevive = texto.includes(c.clave);
|
|
const arrastraViejo = texto.includes(c.claveVieja);
|
|
const usados = estimateTokens(texto);
|
|
filas.push({ pregunta: c.q, sobrevive, arrastraViejo, usados, deN: hist.length, quedan: messages.length });
|
|
onCase(filas[filas.length - 1], i, HECHOS.length);
|
|
await new Promise(r => setTimeout(r, 25)); // deja respirar a la UI
|
|
}
|
|
const rec = filas.filter(f => f.sobrevive).length;
|
|
const conf = filas.filter(f => f.arrastraViejo).length;
|
|
return {
|
|
filas,
|
|
resumen: {
|
|
'evidencia recuperada': `${rec}/${filas.length}`,
|
|
'recall': (filas.length ? (rec / filas.length * 100).toFixed(0) : 0) + '%',
|
|
'arrastra dato viejo': { v: `${conf}/${filas.length}`, bad: conf > 0 },
|
|
'tokens medios': Math.round(filas.reduce((a, f) => a + f.usados, 0) / (filas.length || 1)),
|
|
'presupuesto': budget,
|
|
'mensajes que pasan': Math.round(filas.reduce((a, f) => a + f.quedan, 0) / (filas.length || 1)),
|
|
},
|
|
};
|
|
}
|
|
|
|
// ═══════════════ BANCO 2 · SWE-bench-style (ejecuta los tests) ═══════════════
|
|
async function opfsRoot() { return navigator.storage.getDirectory(); }
|
|
async function limpiarOPFS() {
|
|
const o = await opfsRoot();
|
|
for await (const e of o.values()) await o.removeEntry(e.name, { recursive: true }).catch(() => {});
|
|
}
|
|
async function sembrar(files) {
|
|
const o = await opfsRoot();
|
|
for (const [path, txt] of Object.entries(files)) {
|
|
const parts = path.split('/'); const name = parts.pop(); let d = o;
|
|
for (const x of parts) d = await d.getDirectoryHandle(x, { create: true });
|
|
const w = await (await d.getFileHandle(name, { create: true })).createWritable();
|
|
await w.write(txt); await w.close();
|
|
}
|
|
}
|
|
async function leer(path) {
|
|
const o = await opfsRoot();
|
|
const parts = path.split('/'); const name = parts.pop(); let d = o;
|
|
for (const x of parts) d = await d.getDirectoryHandle(x);
|
|
return (await (await d.getFileHandle(name)).getFile()).text();
|
|
}
|
|
// Ejecuta DE VERDAD el módulo arreglado y le pasa el test de la tarea.
|
|
async function pasaElTest(task) {
|
|
const src = await leer(task.target);
|
|
const url = URL.createObjectURL(new Blob([src], { type: 'text/javascript' }));
|
|
try { return !!task.test(await import(/* @vite-ignore */ url)); }
|
|
catch { return false; }
|
|
finally { URL.revokeObjectURL(url); }
|
|
}
|
|
|
|
async function benchSWE(onCase) {
|
|
const code = await import('./tools/code.js');
|
|
await code.openProject(await opfsRoot());
|
|
const { Agent } = await import('./agent.js');
|
|
const filas = [];
|
|
for (let i = 0; i < TASKS.length; i++) {
|
|
if (S.abort) break;
|
|
const t = TASKS[i];
|
|
await limpiarOPFS(); await sembrar(t.files);
|
|
code.invalidateFileList();
|
|
const t0 = performance.now();
|
|
let pasos = 0, err = null;
|
|
try {
|
|
if (S.brain === 'rules') {
|
|
// Solver de guion: valida el ARNÉS (debe dar N/N) sin gastar GPU.
|
|
await sembrar({ [t.target]: t.solution });
|
|
} else {
|
|
const ag = new Agent(S.provider);
|
|
await ag.handle(`${t.task}\n\nEl fichero a arreglar es ${t.target}. Usa code.read y code.edit.`,
|
|
ev => { if (ev.type === 'tool') pasos++; });
|
|
}
|
|
} catch (e) { err = e.message; }
|
|
const ok = err ? false : await pasaElTest(t);
|
|
filas.push({ id: t.id, ok, pasos, secs: Math.round((performance.now() - t0) / 1000), err });
|
|
onCase(filas[filas.length - 1], i, TASKS.length);
|
|
}
|
|
const res = filas.filter(f => f.ok).length;
|
|
return { filas, resumen: { resolved: `${res}/${filas.length}`, '% resueltas': (filas.length ? (res / filas.length * 100).toFixed(0) : 0) + '%', 'pasos totales': filas.reduce((a, f) => a + f.pasos, 0) } };
|
|
}
|
|
|
|
// ═══════════════ BANCO 3 · juegos (genera, ejecuta y puntúa) ═══════════════
|
|
const BRIEF_JUEGO = 'Un juego tipo Flappy Bird: un pájaro que cae por gravedad, sube al pulsar espacio o click, ' +
|
|
'tuberías que avanzan y hay que esquivar, colisiones, puntuación y pantalla de fin de partida.';
|
|
|
|
async function generarJuego() {
|
|
const rlm = await import('./rlm.js').catch(() => null);
|
|
if (!rlm) throw new Error('rlm.js no disponible en esta build');
|
|
let inferencias = 0;
|
|
const contando = { chat: (...a) => { inferencias++; return S.provider.chat(...a); } };
|
|
const rondas = S.algo === 'rlm-deep' ? Math.max(1, S.algoOpts.rounds) : 0;
|
|
const out = await rlm.deepCreate({ brief: BRIEF_JUEGO, provider: contando, rounds: rondas, onProgress: e => log(' ' + JSON.stringify(e)) });
|
|
return { html: out.html, inferencias, trace: out.trace };
|
|
}
|
|
|
|
// Puntúa el juego EJECUTÁNDOLO en la arena: ¿carga sin errores? ¿arranca?
|
|
// ¿se mueve? ¿aguanta jugando? Cada criterio suma.
|
|
async function puntuarJuego(html) {
|
|
const frame = $('#arena');
|
|
$('#arena-panel').hidden = false;
|
|
const errores = [];
|
|
const onErr = e => errores.push(String(e.message || e).slice(0, 120));
|
|
frame.srcdoc = html;
|
|
await new Promise(r => frame.onload = r);
|
|
const w = frame.contentWindow, d = frame.contentDocument;
|
|
w.addEventListener('error', onErr);
|
|
const hash = () => {
|
|
const c = d.querySelector('canvas'); if (!c) return -1;
|
|
try { const px = c.getContext('2d').getImageData(0, 0, c.width, c.height).data;
|
|
let h = 7; for (let i = 0; i < px.length; i += 997) h = (h * 31 + px[i]) % 999983; return h;
|
|
} catch { return -1; }
|
|
};
|
|
const esperar = ms => new Promise(r => setTimeout(r, ms));
|
|
const canvas = !!d.querySelector('canvas');
|
|
await esperar(600);
|
|
const h0 = hash();
|
|
// arrancar: pulsar el botón si lo hay, y quitarle el foco (si no, ESPACIO
|
|
// re-pulsa el botón en vez de saltar — nos pasó midiéndolo a mano)
|
|
const btn = d.querySelector('button');
|
|
if (btn) { btn.click(); btn.blur?.(); }
|
|
await esperar(400);
|
|
const arranca = hash() !== h0;
|
|
// jugar: aletear con click en el canvas, que es el manejador que sí registran
|
|
const c = d.querySelector('canvas');
|
|
let vivoMs = 0, puntos = 0;
|
|
const t0 = performance.now();
|
|
for (let i = 0; i < 20; i++) {
|
|
c?.dispatchEvent(new w.MouseEvent('click', { bubbles: true }));
|
|
d.dispatchEvent(new w.KeyboardEvent('keydown', { key: ' ', code: 'Space', bubbles: true }));
|
|
await esperar(300);
|
|
const run = (() => { try { return w.gameRunning; } catch { return null; } })();
|
|
puntos = Math.max(puntos, (() => { try { return w.score || 0; } catch { return 0; } })());
|
|
if (run === false) break;
|
|
vivoMs = performance.now() - t0;
|
|
}
|
|
const criterios = {
|
|
'carga sin errores': errores.length === 0,
|
|
'tiene canvas': canvas,
|
|
'arranca': arranca,
|
|
'aguanta ≥3 s jugando': vivoMs >= 3000,
|
|
'llega a puntuar': puntos > 0,
|
|
};
|
|
return { criterios, errores, vivoSegs: +(vivoMs / 1000).toFixed(1), puntos, bytes: html.length };
|
|
}
|
|
|
|
async function benchJuegos(onCase) {
|
|
if (!S.provider || S.brain === 'rules') throw new Error('este banco necesita un modelo de verdad (Reglas no genera juegos)');
|
|
const filas = [];
|
|
log('generando el juego con «' + S.algo + '»… esto tarda', 'w');
|
|
const g = await generarJuego();
|
|
log(`generado: ${g.html.length} bytes en ${g.inferencias} inferencias`, 'a');
|
|
const p = await puntuarJuego(g.html);
|
|
Object.entries(p.criterios).forEach(([k, v], i) => {
|
|
filas.push({ criterio: k, ok: v });
|
|
onCase(filas[filas.length - 1], i, Object.keys(p.criterios).length);
|
|
});
|
|
S.lastGame = { html: g.html, trace: g.trace };
|
|
const pasa = filas.filter(f => f.ok).length;
|
|
return {
|
|
filas,
|
|
resumen: {
|
|
'criterios superados': `${pasa}/${filas.length}`, inferencias: g.inferencias,
|
|
'bytes': p.bytes, 'segundos vivo': p.vivoSegs, 'puntuación': p.puntos,
|
|
},
|
|
};
|
|
}
|
|
|
|
// ─────────────────────────── orquestación ───────────────────────────
|
|
async function run() {
|
|
if (S.running) return;
|
|
S.running = true; S.abort = false;
|
|
$('#btn-run').disabled = true; $('#btn-stop').disabled = false;
|
|
setLed('#led-run', 'busy', 'ejecutando ' + S.bench);
|
|
tiles({}); prog(0, 1, 'preparando…');
|
|
log('▶ benchmark «' + S.bench + '» · algoritmo «' + S.algo + '» · cerebro «' + S.brain + '»', 'a');
|
|
const t0 = Date.now();
|
|
let out = null, error = null;
|
|
try {
|
|
if (S.bench === 'contexto') {
|
|
table(['pregunta', 'dato bueno', 'dato viejo', 'tokens', 'mensajes'], []);
|
|
out = await benchContexto((f, i, n) => {
|
|
addRow([f.pregunta.slice(0, 38),
|
|
f.sobrevive ? { t: 'SOBREVIVE', cls: 'ok' } : { t: 'PERDIDO', cls: 'no' },
|
|
f.arrastraViejo ? { t: 'lo arrastra', cls: 'no' } : { t: 'descartado', cls: 'ok' },
|
|
f.usados, `${f.quedan}/${f.deN}`]);
|
|
prog(i + 1, n, `caso ${i + 1} de ${n}`);
|
|
});
|
|
} else if (S.bench === 'swe') {
|
|
table(['tarea', 'resultado', 'pasos', 'seg'], []);
|
|
out = await benchSWE((f, i, n) => {
|
|
addRow([f.id, f.ok ? { t: 'RESUELTA', cls: 'ok' } : { t: f.err ? 'ERROR' : 'FALLA', cls: 'no' }, f.pasos, f.secs]);
|
|
prog(i + 1, n, `tarea ${i + 1} de ${n}`);
|
|
});
|
|
} else {
|
|
table(['criterio', 'resultado'], []);
|
|
out = await benchJuegos((f, i, n) => {
|
|
addRow([f.criterio, f.ok ? { t: 'SÍ', cls: 'ok' } : { t: 'NO', cls: 'no' }]);
|
|
prog(i + 1, n, `criterio ${i + 1} de ${n}`);
|
|
});
|
|
}
|
|
} catch (e) { error = e.message; log('fallo: ' + e.message, 'e'); }
|
|
|
|
if (out) {
|
|
tiles(out.resumen);
|
|
S.run = {
|
|
bench: S.bench, algo: S.algo, algoOpts: { ...S.algoOpts }, brain: S.brain,
|
|
ctx: { ...DEFAULTS, ...S.ctx }, resumen: out.resumen, casos: out.filas,
|
|
duracionSegs: Math.round((Date.now() - t0) / 1000), cuando: new Date().toISOString(),
|
|
};
|
|
log('✔ terminado: ' + JSON.stringify(out.resumen), 'a');
|
|
}
|
|
setLed('#led-run', error ? 'err' : 'on', error ? 'con fallo' : 'listo');
|
|
prog(1, 1, error ? 'abortado' : 'completado');
|
|
S.running = false; $('#btn-run').disabled = false; $('#btn-stop').disabled = true;
|
|
}
|
|
|
|
// ─────────────────── historial y ranking, en local ───────────────────
|
|
// Todo se queda en esta máquina: ni se envía ni se pide permiso a nadie. La
|
|
// gracia del ranking es comparar CONFIGURACIONES entre sí — qué combinación de
|
|
// algoritmo y perillas gana en cada banco — con su traza al lado para revisarla.
|
|
const STORE = 'elffusscode.lab.runs';
|
|
const MAX_GUARDADAS = 60;
|
|
|
|
// Puntuación comparable (0-100) según el banco, para poder ordenar.
|
|
function puntuar(r) {
|
|
const n = (s) => Number(String(s).split('/')[0]) || 0;
|
|
const d = (s) => Number(String(s).split('/')[1]) || 1;
|
|
if (r.bench === 'contexto') return Math.round(n(r.resumen['evidencia recuperada']) / d(r.resumen['evidencia recuperada']) * 100);
|
|
if (r.bench === 'swe') return Math.round(n(r.resumen.resolved) / d(r.resumen.resolved) * 100);
|
|
return Math.round(n(r.resumen['criterios superados']) / d(r.resumen['criterios superados']) * 100);
|
|
}
|
|
// Huella corta de la configuración: lo que de verdad distingue una corrida.
|
|
function huella(r) {
|
|
const on = Object.entries(r.ctx).filter(([k, v]) => v === true).map(([k]) => k);
|
|
const extra = r.algo === 'rlm-deep' ? `·${r.algoOpts.rounds}r` : '';
|
|
return `${r.algo}${extra} · ${r.brain} · ${on.length} perillas`;
|
|
}
|
|
const leerRuns = () => { try { return JSON.parse(localStorage.getItem(STORE)) || []; } catch { return []; } };
|
|
function guardarCorrida() {
|
|
if (!S.run) return log('todavía no hay ninguna corrida que guardar', 'w');
|
|
const runs = leerRuns();
|
|
runs.push({ ...S.run, score: puntuar(S.run), id: (S.run.cuando || '') + '·' + Math.round(performance.now()) });
|
|
localStorage.setItem(STORE, JSON.stringify(runs.slice(-MAX_GUARDADAS)));
|
|
log('guardada en el historial local (' + runs.length + ' corridas)', 'a');
|
|
pintarRanking();
|
|
}
|
|
function pintarRanking() {
|
|
const host = $('#rank-body'); host.innerHTML = '';
|
|
const runs = leerRuns().filter(r => r.bench === S.bench).sort((a, b) => b.score - a.score);
|
|
$('#rank-tag').textContent = runs.length ? runs.length + ' corridas · ' + S.bench : 'sin corridas de ' + S.bench;
|
|
if (!runs.length) { host.innerHTML = '<tr><td colspan="5" class="pend">Aún no has guardado ninguna corrida de este banco.</td></tr>'; return; }
|
|
runs.forEach((r, i) => {
|
|
const tr = el('tr');
|
|
const medalla = i === 0 ? '🥇' : i === 1 ? '🥈' : i === 2 ? '🥉' : String(i + 1);
|
|
[[medalla], [{ t: r.score + '%', cls: r.score >= 80 ? 'ok' : r.score >= 40 ? '' : 'no' }],
|
|
[huella(r)], [new Date(r.cuando).toLocaleString()]].forEach(([c]) => {
|
|
const td = el('td');
|
|
if (c && typeof c === 'object') { td.className = c.cls || ''; td.textContent = c.t; } else td.textContent = c;
|
|
tr.appendChild(td);
|
|
});
|
|
const acc = el('td');
|
|
const ver = el('button', 'btn ghost', 'traza');
|
|
ver.style.padding = '2px 8px'; ver.style.fontSize = '11px';
|
|
ver.onclick = () => descargarTraza(r);
|
|
acc.appendChild(ver); tr.appendChild(acc);
|
|
host.appendChild(tr);
|
|
});
|
|
}
|
|
// Exportar es un fichero en tu disco, no un envío: sirve para revisar la traza
|
|
// caso a caso o comparar dos corridas fuera del panel.
|
|
function descargarTraza(r) {
|
|
const a = el('a');
|
|
a.href = URL.createObjectURL(new Blob([JSON.stringify(r, null, 2)], { type: 'application/json' }));
|
|
a.download = `elffuss-lab-${r.bench}-${r.score}.json`;
|
|
a.click();
|
|
log('traza exportada a tu disco', 'a');
|
|
}
|
|
function borrarHistorial() {
|
|
if (!confirm('¿Borrar TODAS las corridas guardadas?')) return;
|
|
localStorage.removeItem(STORE); pintarRanking(); log('historial local borrado', 'w');
|
|
}
|
|
|
|
// ─────────────────────────── arranque ───────────────────────────
|
|
radio($('#brains'), BRAINS, S.brain, id => { S.brain = id; $('#brain-tag').textContent = id; });
|
|
radio($('#algos'), ALGOS, S.algo, id => { S.algo = id; $('#algo-tag').textContent = id; algoKnobs(); });
|
|
radio($('#benches'), BENCHES, S.bench, id => { S.bench = id; $('#bench-tag').textContent = id; });
|
|
$('#brain-tag').textContent = S.brain;
|
|
$('#bench-tag').textContent = S.bench;
|
|
algoKnobs(); ctxUI();
|
|
|
|
$('#btn-load').onclick = loadBrain;
|
|
$('#btn-run').onclick = run;
|
|
$('#btn-stop').onclick = () => { S.abort = true; log('parando tras el caso en curso…', 'w'); };
|
|
$('#ctx-all').onclick = () => { CTX_KNOBS.flatMap(([, k]) => k).forEach(([k, t]) => { if (t === 'bool') S.ctx[k] = true; }); ctxUI(); };
|
|
$('#ctx-none').onclick = () => { CTX_KNOBS.flatMap(([, k]) => k).forEach(([k, t]) => { if (t === 'bool') S.ctx[k] = false; }); ctxUI(); };
|
|
$('#ctx-def').onclick = () => { S.ctx = {}; ctxUI(); };
|
|
$('#btn-launch').onclick = () => {
|
|
localStorage.setItem('elffusscode.model', S.brain);
|
|
localStorage.setItem('elffusscode.ctxopts', JSON.stringify(S.ctx));
|
|
localStorage.setItem('elffusscode.algo', JSON.stringify({ algo: S.algo, opts: S.algoOpts }));
|
|
log('configuración guardada; abriendo el IDE', 'a');
|
|
window.open('index.html', '_blank');
|
|
};
|
|
|
|
$('#btn-save').onclick = guardarCorrida;
|
|
$('#btn-clear').onclick = borrarHistorial;
|
|
pintarRanking();
|
|
|
|
log('Elffuss Lab listo. Elige algoritmo, enciende o apaga la compresión y lanza un banco.', 'a');
|
|
log('El banco de contexto no necesita GPU: mide el compresor y responde al instante.');
|
|
log('Los resultados se quedan en esta máquina: historial y ranking local, sin enviar nada.');
|