[object Object]

← back to Model Arena

add DTD roster and fresh daily challenges

328547ea07cf81f25fb8428d112ccd40a708cab4 · 2026-08-31 10:35:25 -0700 · Steve Abrams

Files touched

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 →