← back to Model Arena
add DTD roster and fresh daily challenges
328547ea07cf81f25fb8428d112ccd40a708cab4 · 2026-08-31 10:35:25 -0700 · Steve Abrams
Files touched
M README.mdM public/index.htmlM scripts/daily-challenge.jsM server.jsA verification/daily-novelty.test.jsA verification/e2e-proof.jsonA verification/model-selector.e2e.jsA verification/model-selector.png
Diff
commit 328547ea07cf81f25fb8428d112ccd40a708cab4
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Mon Aug 31 10:35:25 2026 -0700
add DTD roster and fresh daily challenges
---
README.md | 2 ++
public/index.html | 31 ++++++++++++++---
scripts/daily-challenge.js | 68 +++++++++++++++++++++++++++++--------
server.js | 22 ++++++++----
verification/daily-novelty.test.js | 29 ++++++++++++++++
verification/e2e-proof.json | 47 +++++++++++++++++++++++++
verification/model-selector.e2e.js | 43 +++++++++++++++++++++++
verification/model-selector.png | Bin 0 -> 148397 bytes
8 files changed, 218 insertions(+), 24 deletions(-)
diff --git a/README.md b/README.md
index fa32c4c..3beda92 100644
--- a/README.md
+++ b/README.md
@@ -62,6 +62,8 @@ votes.jsonl, costlog.jsonl).
- **Remix mode** — `POST /api/challenges/:id/remix` feeds the best artifact's HTML back to the models as "improve on this," spawning an evolutionary round (`remixOf`/`remixSource` lineage).
- **Source view** — `</>` per pane shows the artifact's raw HTML.
- **Daily generator** — `scripts/daily-challenge.js` runs a `$0` all-local battle on a rotating prompt + AI-judges it, to accrue real data. launchd install is drafted (gated) in `~/.claude/yolo-queue/pending-approval/`.
+- **DTD Six selector** — the complete DTD family roster is visible (Claude, Codex, Qwen, Grok, Kimi, Muse), and one click selects every currently available member; users can select any one model through all available models.
+- **No-repeat daily novelty engine** — 21 categorized challenge foundations combine with seven rotating quality constraints and skip the most recent 14 prompt IDs, replacing the old seven-item weekday loop.
### More analytics (yolo iteration 2)
- **Per-category leaderboards** — challenges auto-tag Games / Real Work / Custom; leaderboard filters ELO by category (`/api/ledger?category=`).
diff --git a/public/index.html b/public/index.html
index 1d8d241..d3d9c87 100644
--- a/public/index.html
+++ b/public/index.html
@@ -91,7 +91,12 @@ input[type=range]{accent-color:var(--neon);width:130px}
.preset:hover{background:rgba(255,46,166,.12)}
.pcat{width:100%;flex-basis:100%;color:var(--dim);font-size:11px;letter-spacing:1px;margin-top:6px}
.models{display:grid;grid-template-columns:repeat(auto-fill,minmax(230px,1fr));gap:8px;margin-bottom:14px}
+.model-tools{display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin:0 0 10px}
+.model-tools .btn{padding:5px 10px}
+.model-tools .count{margin-left:auto;color:var(--dim);font-size:11px}
.mrow{display:flex;align-items:center;gap:8px;border:1px solid var(--line);padding:8px 10px;font-size:12px}
+.mrow.dtd{border-color:#6b4da1;background:rgba(160,107,255,.05)}
+.dtdb{font-size:9px;padding:1px 5px;border:1px solid #a06bff;color:#c8a6ff;border-radius:3px;letter-spacing:1px}
.mrow.off{opacity:.45}
.mrow .cost{margin-left:auto;font-size:11px}
.mrow .cost.free{color:var(--ok)} .mrow .cost.paid{color:var(--gold)}
@@ -216,9 +221,15 @@ tr.clk{cursor:pointer}tr.clk:hover td{background:rgba(0,229,255,.06)}
<input type="text" id="f-title" placeholder="Title (e.g. Neon City Driving)">
<div class="presets" id="presets"></div>
<textarea id="f-prompt" placeholder="The real-world build challenge, e.g. 'Build a playable neon-city driving game in a single HTML file…'"></textarea>
+ <div class="model-tools">
+ <button type="button" class="btn pink" id="pick-dtd">DTD Six</button>
+ <button type="button" class="btn" id="pick-all">Select all available</button>
+ <button type="button" class="btn" id="pick-none">Clear</button>
+ <span class="count" id="pick-count">0 selected</span>
+ </div>
<div class="models" id="model-picks"></div>
<label class="blindtog" title="Models get a graphic-design tool belt: opendesign (palettes, type scales, icons, shadows), hyperframes (motion/easing catalog), figma (live brand tokens when FIGMA_TOKEN is set). Tool-capable models (GPT, Grok, Kimi, Qwen3, Hermes3) call them live; the rest get the same material injected as a design pack. Tool rounds add ~30-60% tokens on metered models."><input type="checkbox" id="f-tools"> 🎨 Design tools — figma · opendesign · hyperframes</label>
- <div class="note">Local models ($0) and Kimi K2.5 (~$0.03) are pre-checked. Other metered models are opt-in per battle — estimated cost shown, actuals logged to the cost ledger.</div>
+ <div class="note">Choose any one model through the entire available roster. “DTD Six” selects every currently available member of Claude, Codex, Qwen, Grok, Kimi, and Muse; unavailable members stay visible and disabled. Metered models remain opt-in and actual usage is logged.</div>
<div class="note" id="est"></div>
<button class="btn" id="f-go">⚔ Fight</button>
</div>
@@ -365,13 +376,16 @@ async function loadModels(){
const cost = m.kind==='local' ? (m.available?'$0 local':(m.hf?'⬇ downloading…':'not pulled'))
: m.kind==='cli' ? (m.available?'$0 Max plan':'no CLI')
: (m.available?('~$'+m.estCost.toFixed(2)):'no key');
- row.className = 'mrow' + (m.available? '' : ' off');
+ row.className = 'mrow' + (m.dtdRef?' dtd':'') + (m.available? '' : ' off');
row.innerHTML = `<input type="checkbox" value="${m.id}" ${(m.kind==='local'&&!m.hf||m.id==='kimi')&&m.available?'checked':''} ${m.available?'':'disabled'}>
- ${m.label}${m.hf?' <span class="hfb">HF</span>':''}
+ ${m.label}${m.dtdRef?' <span class="dtdb">DTD · '+m.dtdRef+'</span>':''}${m.hf?' <span class="hfb">HF</span>':''}
<span class="cost ${free?'free':'paid'}">${cost}</span>`;
el.appendChild(row);
}
- el.addEventListener('change', updateEst); updateEst();
+ el.addEventListener('change', ()=>{ persistPicks(); updateEst(); });
+ const saved = JSON.parse(localStorage.getItem('ma-model-picks') || 'null');
+ if (Array.isArray(saved)) el.querySelectorAll('input:not(:disabled)').forEach(i=>{ i.checked=saved.includes(i.value); });
+ updateEst();
const pr = $('#presets'); pr.innerHTML='';
let lastCat=null;
for (const p of PRESETS){
@@ -382,7 +396,16 @@ async function loadModels(){
}
}
function picked(){ return [...document.querySelectorAll('#model-picks input:checked')].map(i=>i.value); }
+function persistPicks(){ localStorage.setItem('ma-model-picks', JSON.stringify(picked())); }
+function setPicks(test){
+ document.querySelectorAll('#model-picks input').forEach(i=>{ i.checked=!i.disabled && test(models.find(m=>m.id===i.value)); });
+ persistPicks(); updateEst();
+}
+$('#pick-dtd').onclick=()=>setPicks(m=>!!m.dtdRef);
+$('#pick-all').onclick=()=>setPicks(()=>true);
+$('#pick-none').onclick=()=>setPicks(()=>false);
function updateEst(){
+ $('#pick-count').textContent = picked().length + ' selected';
const est = picked().reduce((s,id)=>{ const m=models.find(x=>x.id===id); return s+(m?m.estCost:0); },0);
$('#est').textContent = est
? 'Rough ceiling: ~$' + est.toFixed(2) + ' (flat per-model guess; actual is metered by real tokens and logged after each run)'
diff --git a/scripts/daily-challenge.js b/scripts/daily-challenge.js
index 746c0a1..66ef04e 100644
--- a/scripts/daily-challenge.js
+++ b/scripts/daily-challenge.js
@@ -11,19 +11,54 @@
const http = require('http'), fs = require('fs'), path = require('path');
const BASE = process.env.BASE || 'http://127.0.0.1:9758';
const AUTH = process.env.AUTH || 'admin:DW2024!';
-const LOG = path.join(__dirname, '..', 'yolo', 'daily-log.jsonl');
+const LOG = process.env.DAILY_LOG || path.join(__dirname, '..', 'yolo', 'daily-log.jsonl');
const LOCAL = ['qwen3-14b', 'gemma3-12b', 'hermes3-8b', 'qwen25-7b'];
const POOL = [
- { t: 'Daily: Particle Fireworks', p: 'Interactive fireworks in one HTML file: click to launch rockets that burst into physics particles with trails, gravity, glow; include an auto-show mode.' },
- { t: 'Daily: Sortable Catalog', p: 'Single-file HTML catalog of 8 invented wallcovering SKUs (sku, pattern, colorway, price, stock) as a responsive card grid with a live search box and click-to-sort columns.' },
- { t: 'Daily: Bouncing Balls', p: 'Single-file HTML canvas with 20 balls bouncing under gravity with collisions and trails; a slider controls gravity.' },
- { t: 'Daily: Luxury Product Page', p: 'Single-file HTML luxury wallpaper product page: hero (CSS gradient), serif display type, price, Add to Cart, spec table. High-end interior-design aesthetic.' },
- { t: 'Daily: Starfield Warp', p: 'Single-file HTML animated starfield warp effect on canvas; mouse position steers the warp direction; a slider controls speed.' },
- { t: 'Daily: Sample-Sale Email', p: 'Single-file email-safe HTML marketing email for a wallpaper sample sale: header, 3 featured patterns in a responsive grid (gradient placeholders), CTA button, compliant footer. Inline CSS only.' },
- { t: 'Daily: Conway Life', p: "Single-file HTML Conway's Game of Life on canvas with play/pause, randomize, and a speed slider." },
+ ['fireworks','Games','Particle Fireworks','Interactive fireworks: click-launched rockets, physics particles, trails, gravity, glow, and an auto-show mode.'],
+ ['catalog','Commerce','Sortable Catalog','A responsive catalog of 12 invented wallcovering SKUs with search, useful filters, sort, density control, price, and stock.'],
+ ['balls','Games','Bouncing Ball Lab','A canvas physics lab with collisions, trails, gravity and elasticity controls, plus reset and slow-motion.'],
+ ['pdp','Commerce','Luxury Product Story','A high-end wallpaper product page with strong art direction, price, sample CTA, specs, and an interactive colorway switcher.'],
+ ['starfield','Creative Code','Starfield Warp','An animated steerable starfield with speed, trail, and palette controls plus a cinematic launch sequence.'],
+ ['email','Marketing','Sample-Sale Campaign','An email-safe, inline-CSS wallpaper campaign with three products, a compelling CTA, and compliant footer.'],
+ ['life','Games','Living Cellular Garden',"A visual cellular-automata garden with play/pause, randomize, draw tools, speed, and multiple rule presets."],
+ ['dashboard','Real Work','Operations Dashboard','A responsive operations dashboard with KPI cards, a filterable queue, status changes, keyboard navigation, and empty/error states.'],
+ ['quiz','Education','Adaptive Design Quiz','An accessible five-question interior-style quiz with progress, explanations, scoring, and a tailored result.'],
+ ['timeline','Editorial','Interactive Design Timeline','A scrollable timeline of invented design movements with filters, expandable stories, and a strong editorial system.'],
+ ['moodboard','Creative Tools','Moodboard Composer','A drag-and-drop moodboard with editable swatches, text notes, layout presets, and an export-ready composition.'],
+ ['invoice','Real Work','Quote Builder','A polished quote builder with editable line items, quantity/price math, discount, tax, validation, and print view.'],
+ ['room','Commerce','Room Visualizer','A CSS-drawn room where users switch wall patterns, scale, color, and lighting with smooth visual feedback.'],
+ ['memory','Games','Pattern Memory','A polished memory game using wallpaper-inspired pattern cards, timed rounds, streaks, and difficulty settings.'],
+ ['kanban','Real Work','Production Kanban','An accessible kanban board with drag-and-drop cards, filters, counts, add-card flow, and persistent local state.'],
+ ['calculator','Commerce','Coverage Calculator','A wallcovering roll calculator with room dimensions, doors/windows, repeat allowance, unit switching, and clear assumptions.'],
+ ['music','Creative Code','Generative Music Visualizer','An interactive audiovisual instrument with Web Audio tones, keyboard controls, reactive particles, and safe mute defaults.'],
+ ['compare','Decision Tools','Model Comparison Console','A side-by-side comparison tool with weighted criteria, editable scores, winner explanation, and shareable summary.'],
+ ['booking','Real Work','Showroom Appointment Flow','A friendly multi-step booking flow with service choice, calendar, time slots, contact validation, and confirmation.'],
+ ['story','Editorial','Scrollytelling Brand Story','A cinematic, accessible scrollytelling page with chapter navigation, restrained motion, and reduced-motion support.'],
+ ['inventory','Real Work','Inventory Triage','A dense but readable inventory table with search, multi-filter, sorting, bulk selection, and low-stock actions.'],
+].map(([id,cat,t,p])=>({id,cat,t,p}));
+
+const TWISTS = [
+ 'Make the primary journey fully keyboard accessible.',
+ 'Include a meaningful empty state and one recoverable error state.',
+ 'Use a warm editorial visual direction with restrained motion.',
+ 'Use a high-contrast technical visual direction with excellent information density.',
+ 'Make it work beautifully at both 390px and 1440px widths.',
+ 'Add one delightful interaction that clarifies rather than distracts.',
+ 'Respect prefers-reduced-motion and make focus states unmistakable.',
];
+function recentIds(limit=14){
+ try { return fs.readFileSync(LOG,'utf8').trim().split('\n').filter(Boolean).slice(-limit).map(x=>JSON.parse(x).promptId).filter(Boolean); }
+ catch { return []; }
+}
+function choosePick(day, forced){
+ if (forced != null) return POOL[((+forced % POOL.length)+POOL.length)%POOL.length];
+ const blocked = new Set(recentIds());
+ for (let n=0;n<POOL.length;n++){ const p=POOL[(day+n*7)%POOL.length]; if(!blocked.has(p.id)) return p; }
+ return POOL[day%POOL.length];
+}
+
function req(method, p, body) {
return new Promise((res, rej) => {
const u = new URL(BASE + p);
@@ -35,11 +70,14 @@ function req(method, p, body) {
}
const sleep = ms => new Promise(r => setTimeout(r, ms));
-(async () => {
- const idx = process.env.PICK != null ? (+process.env.PICK % POOL.length)
- : Math.floor((Date.now() / 86400000)) % POOL.length; // day-of-epoch rotation
- const pick = POOL[idx];
- const c = await req('POST', '/api/challenges', { title: pick.t, prompt: pick.p, models: LOCAL });
+if (require.main === module) (async () => {
+ const day = Math.floor(Date.now() / 86400000);
+ const pick = choosePick(day, process.env.PICK);
+ const twist = TWISTS[day % TWISTS.length];
+ const title = `Daily · ${pick.cat}: ${pick.t}`;
+ const prompt = `Build this as one complete, self-contained HTML file: ${pick.p} ${twist}`;
+ if (process.env.PREVIEW === '1') { console.log(JSON.stringify({promptId:pick.id,title,prompt,models:LOCAL},null,2)); return; }
+ const c = await req('POST', '/api/challenges', { title, prompt, models: LOCAL, category: pick.cat });
if (!c.id) { console.error('create failed', c); process.exit(1); }
console.log('created', c.id, '—', pick.t);
// poll until settled + judged (cap ~45 min; reduce sleep to 3s after runs settle)
@@ -57,7 +95,7 @@ const sleep = ms => new Promise(r => setTimeout(r, ms));
if (settled && ((!cur.judging && cur.judged_at) || judgeTimeout)) {
const done = cur.runs.filter(r => r.status === 'done').length;
const isPartial = judgeTimeout && !cur.judged_at;
- const entry = { ts: new Date().toISOString(), id: c.id, title: pick.t, done, aiPick: cur.aiPick || null, partial: isPartial };
+ const entry = { ts: new Date().toISOString(), id: c.id, promptId: pick.id, title, done, aiPick: cur.aiPick || null, partial: isPartial };
try { fs.appendFileSync(LOG, JSON.stringify(entry) + '\n'); } catch (e) { console.error('LOG APPEND FAILED', e); }
if (isPartial) {
// exit non-zero so the wrapper fires its CNCP + email alert for PARTIAL outcomes
@@ -70,3 +108,5 @@ const sleep = ms => new Promise(r => setTimeout(r, ms));
}
console.error('timed out waiting for battle to settle'); process.exit(2);
})().catch(e => { console.error(e); process.exit(1); });
+
+module.exports = { POOL, TWISTS, recentIds, choosePick };
diff --git a/server.js b/server.js
index 9495984..1fc1581 100644
--- a/server.js
+++ b/server.js
@@ -37,7 +37,8 @@ function shootThumb(htmlPath, outPng, cb) {
const PORT = parseInt(process.env.PORT || '9758', 10);
const BASIC_AUTH = process.env.BASIC_AUTH !== undefined ? process.env.BASIC_AUTH : 'admin:DW2024!';
const DIR = __dirname;
-const DATA = path.join(DIR, 'data');
+// DATA_DIR keeps verification and canary servers from touching the live arena ledger.
+const DATA = process.env.DATA_DIR ? path.resolve(process.env.DATA_DIR) : path.join(DIR, 'data');
const ART = path.join(DATA, 'artifacts');
const CH_FILE = path.join(DATA, 'challenges.json');
const VOTES_FILE = path.join(DATA, 'votes.jsonl');
@@ -73,7 +74,7 @@ const MODELS = [
// tool-calling loop — an 8B/14B is unreliable at function-calling and returned 0 chars
// through the tool loop (ALL-MUST-WORK fix, 2026-07-25). Live tool loop stays on the
// reliable API models (kimi/gpt/grok) + claude-code.
- { id: 'qwen3-14b', label: 'Qwen3 14B', kind: 'local', host: 'http://localhost:11434', model: 'qwen3:14b', estCost: 0 },
+ { id: 'qwen3-14b', label: 'Qwen3 14B', kind: 'local', host: 'http://localhost:11434', model: 'qwen3:14b', estCost: 0, dtdRef: 'Qwen' },
{ id: 'gemma3-12b', label: 'Gemma3 12B', kind: 'local', host: 'http://localhost:11434', model: 'gemma3:12b', estCost: 0 },
{ id: 'hermes3-8b', label: 'Hermes3 8B', kind: 'local', host: 'http://localhost:11434', model: 'hermes3:8b', estCost: 0 },
// was Mac1 (192.168.1.133) — that Ollama host wedges (model pinned in VRAM, generations hang);
@@ -82,11 +83,13 @@ const MODELS = [
// HuggingFace BigCode-leaderboard #1 open code model (pulled from HF as GGUF into Ollama). Auto-registers once the download finishes.
{ id: 'hf-qwen-coder-32b', label: 'Qwen2.5-Coder 32B (HF top coder)', kind: 'local', host: 'http://localhost:11434', model: 'hf.co/bartowski/Qwen2.5-Coder-32B-Instruct-GGUF:Q4_K_M', estCost: 0, hf: true },
// Claude on Steve's MAX PLAN via the Claude Code CLI — $0 marginal (subscription-covered, not API credits)
- { id: 'claude-code', label: 'Claude Opus (Max plan)', kind: 'cli', estCost: 0 },
+ { id: 'muse', label: 'Muse Glimmer 30B', kind: 'local', host: process.env.MUSE_HOST || 'http://localhost:11434', model: process.env.MUSE_MODEL || 'muse-glimmer:30b-mlx', estCost: 0, dtdRef: 'Muse' },
+ { id: 'claude-code', label: 'Claude Opus (Max plan)', kind: 'cli', estCost: 0, dtdRef: 'Claude' },
// { id: 'claude', label: 'Claude Fable 5 (API)', kind: 'metered', provider: 'anthropic', model: 'claude-fable-5', envKey: 'ANTHROPIC_API_KEY', estCost: 0.15 }, // needs API credits
- { id: 'kimi', label: 'Kimi K2.5', kind: 'metered', provider: 'moonshot', model: 'kimi-k2.5', envKey: 'MOONSHOT_API_KEY', estCost: 0.03, tools: true },
+ { id: 'kimi', label: 'Kimi K2.5', kind: 'metered', provider: 'moonshot', model: 'kimi-k2.5', envKey: 'MOONSHOT_API_KEY', estCost: 0.03, tools: true, dtdRef: 'Kimi' },
{ id: 'gpt', label: 'GPT-5.1', kind: 'metered', provider: 'openai', model: process.env.OPENAI_MODEL || 'gpt-5.1', envKey: 'OPENAI_API_KEY', estCost: 0.10, tools: true },
- { id: 'grok', label: 'Grok 4.5', kind: 'metered', provider: 'xai', model: process.env.GROK_MODEL || 'grok-4.5', envKey: 'XAI_API_KEY', estCost: 0.10, tools: true },
+ { id: 'codex', label: 'Codex GPT-5.3', kind: 'metered', provider: 'openai', model: process.env.CODEX_MODEL || 'gpt-5.3-codex', envKey: 'OPENAI_API_KEY', estCost: 0.10, responses: true, dtdRef: 'Codex' },
+ { id: 'grok', label: 'Grok 4.5', kind: 'metered', provider: 'xai', model: process.env.GROK_MODEL || 'grok-4.5', envKey: 'XAI_API_KEY', estCost: 0.10, tools: true, dtdRef: 'Grok' },
];
// Claude via the Claude Code CLI — runs on Steve's Max SUBSCRIPTION (OAuth), not
@@ -309,6 +312,13 @@ async function generateMetered(m, prompt) {
if (!r.json || !r.json.content) throw new Error('anthropic error: ' + JSON.stringify(r.json && r.json.error || r.raw).slice(0, 200));
text = r.json.content.map(b => b.text || '').join('');
inTok = (r.json.usage && r.json.usage.input_tokens) || 0; outTok = (r.json.usage && r.json.usage.output_tokens) || 0;
+ } else if (m.responses) {
+ const r = await httpJson('https://api.openai.com/v1/responses',
+ { headers: { Authorization: 'Bearer ' + key } },
+ { model: m.model, input: prompt, max_output_tokens: 16000 }, 300000);
+ if (!r.json || !Array.isArray(r.json.output)) throw new Error('openai responses error: ' + JSON.stringify(r.json && r.json.error || r.raw).slice(0, 200));
+ text = r.json.output_text || r.json.output.flatMap(o => o.content || []).map(c => c.text || '').join('');
+ inTok = (r.json.usage && r.json.usage.input_tokens) || 0; outTok = (r.json.usage && r.json.usage.output_tokens) || 0;
} else {
const base = { moonshot: 'https://api.moonshot.ai/v1', openai: 'https://api.openai.com/v1', xai: 'https://api.x.ai/v1' }[m.provider];
// gpt-5 family only accepts max_completion_tokens (not max_tokens) and only
@@ -628,7 +638,7 @@ const server = http.createServer(async (req, res) => {
const out = await Promise.all(MODELS.map(async m => ({
id: m.id, label: m.label, kind: m.kind, model: m.model,
available: await modelAvailable(m),
- hf: !!m.hf, estCost: m.estCost,
+ hf: !!m.hf, estCost: m.estCost, dtdRef: m.dtdRef || null,
})));
return send(res, 200, out);
}
diff --git a/verification/daily-novelty.test.js b/verification/daily-novelty.test.js
new file mode 100644
index 0000000..92d69d1
--- /dev/null
+++ b/verification/daily-novelty.test.js
@@ -0,0 +1,29 @@
+#!/usr/bin/env node
+const test = require('node:test');
+const assert = require('node:assert/strict');
+const fs = require('fs');
+const os = require('os');
+const path = require('path');
+
+const log = path.join(fs.mkdtempSync(path.join(os.tmpdir(), 'arena-daily-')), 'daily.jsonl');
+process.env.DAILY_LOG = log;
+const { POOL, TWISTS, choosePick } = require('../scripts/daily-challenge');
+
+test('daily pool is broad and categorized', () => {
+ assert.equal(POOL.length, 21);
+ assert.equal(new Set(POOL.map(x => x.id)).size, 21);
+ assert.ok(new Set(POOL.map(x => x.cat)).size >= 6);
+ assert.equal(TWISTS.length, 7);
+});
+
+test('picker avoids the most recent 14 prompt ids', () => {
+ fs.writeFileSync(log, POOL.slice(0, 14).map((x, i) => JSON.stringify({ ts: new Date(i).toISOString(), promptId: x.id })).join('\n') + '\n');
+ const pick = choosePick(0);
+ assert.ok(!POOL.slice(0, 14).some(x => x.id === pick.id), `repeated blocked prompt ${pick.id}`);
+});
+
+test('forced picks support every option and wrap safely', () => {
+ assert.equal(choosePick(0, 0).id, POOL[0].id);
+ assert.equal(choosePick(0, POOL.length).id, POOL[0].id);
+ assert.equal(choosePick(0, -1).id, POOL.at(-1).id);
+});
diff --git a/verification/e2e-proof.json b/verification/e2e-proof.json
new file mode 100644
index 0000000..4efe622
--- /dev/null
+++ b/verification/e2e-proof.json
@@ -0,0 +1,47 @@
+{
+ "intent": "Replace the repetitive seven-day challenge loop and allow selection of any one through all available models, including the six DTD families.",
+ "risk_tier": "R2",
+ "environment": "Isolated local server at 127.0.0.1:19758 with DATA_DIR=/private/tmp/model-arena-e2e; no production data or deployment.",
+ "baseline_commit": "7e06a40",
+ "timestamp": "2026-08-31T17:34:27Z",
+ "checks": [
+ {
+ "boundary": "static and syntax",
+ "commands": ["node --check server.js", "node --check scripts/daily-challenge.js", "git diff --check"],
+ "verdict": "PASS"
+ },
+ {
+ "boundary": "daily novelty logic",
+ "command": "node --test verification/daily-novelty.test.js",
+ "assertions": ["21 unique categorized foundations", "7 rotating constraints", "recent 14 prompt IDs are excluded", "forced selection wraps safely"],
+ "verdict": "PASS"
+ },
+ {
+ "boundary": "HTTP",
+ "tool": "3x M1 plus direct API checks",
+ "assertions": ["GET / returned 200 text/html", "GET /api/healthz returned ok", "GET /api/models returned all six DTD references"],
+ "verdict": "PASS"
+ },
+ {
+ "boundary": "browser interaction",
+ "command": "node verification/model-selector.e2e.js",
+ "assertions": ["six DTD family badges present", "single selection", "DTD available selection", "all available selection", "localStorage reload persistence", "zero console/page errors"],
+ "artifact": "verification/model-selector.png",
+ "verdict": "PASS"
+ },
+ {
+ "boundary": "cross-browser engines",
+ "tool": "3x",
+ "reason": "The shared runner completed HTTP M1 but stalled during headless render on this host; the critical interaction path was independently completed in system Chrome.",
+ "verdict": "SKIP"
+ }
+ ],
+ "negative_checks": [
+ "Unavailable DTD runtimes remain visible but disabled.",
+ "Clear produces zero selected models; server already rejects a battle with zero models.",
+ "Test server used a temporary ledger so startup and browser actions could not mutate live challenges."
+ ],
+ "cleanup": "Isolated server terminated after verification; temporary test ledger retained under /private/tmp for inspection.",
+ "overall": "PASS",
+ "reason": "The critical local user journey and boundaries pass. Cross-browser parity is a recorded non-critical skip because the shared runner stalled after M1. No live deployment was attempted."
+}
diff --git a/verification/model-selector.e2e.js b/verification/model-selector.e2e.js
new file mode 100644
index 0000000..60c1123
--- /dev/null
+++ b/verification/model-selector.e2e.js
@@ -0,0 +1,43 @@
+#!/usr/bin/env node
+const { chromium } = require('/Users/macstudio3/Projects/Designer-Wallcoverings/node_modules/playwright');
+
+(async () => {
+ const browser = await chromium.launch({ executablePath: '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome', headless: true });
+ const page = await browser.newPage({ viewport: { width: 1280, height: 900 } });
+ const errors = [];
+ page.on('pageerror', e => errors.push(String(e)));
+ page.on('console', m => { if (m.type() === 'error') errors.push(m.text()); });
+ await page.goto(process.env.ARENA_URL || 'http://127.0.0.1:19758/', { waitUntil: 'load' });
+ await page.click('#nav-new');
+ await page.waitForSelector('#model-picks .mrow');
+
+ const dtdLabels = await page.locator('#model-picks .dtdb').allTextContents();
+ if (dtdLabels.length !== 6) throw new Error(`expected 6 DTD models, got ${dtdLabels.length}: ${dtdLabels.join(', ')}`);
+ for (const ref of ['Claude','Codex','Qwen','Grok','Kimi','Muse']) {
+ if (!dtdLabels.some(x => x.includes(ref))) throw new Error(`missing DTD reference ${ref}`);
+ }
+
+ await page.click('#pick-none');
+ if (await page.locator('#model-picks input:checked').count()) throw new Error('Clear did not clear selection');
+ const first = page.locator('#model-picks input:not(:disabled)').first();
+ await first.check();
+ if (await page.locator('#model-picks input:checked').count() !== 1) throw new Error('single-model selection failed');
+ if ((await page.locator('#pick-count').textContent()).trim() !== '1 selected') throw new Error('single-model count failed');
+
+ await page.click('#pick-dtd');
+ const availableDtd = await page.locator('#model-picks .mrow.dtd input:not(:disabled)').count();
+ const selectedDtd = await page.locator('#model-picks .mrow.dtd input:checked').count();
+ if (selectedDtd !== availableDtd) throw new Error(`DTD selection ${selectedDtd}/${availableDtd}`);
+ await page.reload({ waitUntil: 'load' });
+ await page.click('#nav-new');
+ if (await page.locator('#model-picks .mrow.dtd input:checked').count() !== availableDtd) throw new Error('DTD selection did not persist');
+
+ await page.click('#pick-all');
+ const availableAll = await page.locator('#model-picks input:not(:disabled)').count();
+ const selectedAll = await page.locator('#model-picks input:checked').count();
+ if (selectedAll !== availableAll) throw new Error(`all selection ${selectedAll}/${availableAll}`);
+ if (errors.length) throw new Error(`browser errors: ${errors.join(' | ')}`);
+ await page.screenshot({ path: 'verification/model-selector.png', fullPage: true });
+ console.log(JSON.stringify({ pass: true, dtdRoster: dtdLabels, availableDtd, availableAll, browserErrors: errors.length }));
+ await browser.close();
+})().catch(e => { console.error(e.stack || e); process.exit(1); });
diff --git a/verification/model-selector.png b/verification/model-selector.png
new file mode 100644
index 0000000..e65d6c7
Binary files /dev/null and b/verification/model-selector.png differ
← 7e06a40 snapshot before DTD roster improvements
·
back to Model Arena
·
record live Model Arena deployment proof 2e08509 →