[--port 4601] [--log posts.log] (--port 0 picks a free port)'
+if (args.includes('--help') || args.includes('-h')) { console.log(usage); process.exit(0) }
+const root = args.find((a, i) => !a.startsWith('--') && !['--port', '--log'].includes(args[i - 1]))
+if (!root) { console.error(usage); process.exit(2) }
+const base = resolve(root)
+try { if (!(await stat(base)).isDirectory()) throw 0 } catch { console.error(`not a folder: ${root}`); process.exit(2) }
+const port = Number(opt('--port', 4601))
+if (!Number.isInteger(port) || port < 0 || port > 65535) { console.error(`bad port: ${opt('--port')}`); process.exit(2) }
+const log = resolve(opt('--log', join(base, 'posts.log')))
+const types = { '.html': 'text/html; charset=utf-8', '.css': 'text/css', '.js': 'text/javascript', '.mjs': 'text/javascript',
+ '.json': 'application/json', '.svg': 'image/svg+xml', '.png': 'image/png', '.jpg': 'image/jpeg', '.webp': 'image/webp',
+ '.ico': 'image/x-icon', '.woff2': 'font/woff2', '.txt': 'text/plain; charset=utf-8' }
+
+const tryFile = async p => { try { const s = await stat(p); return s.isFile() ? p : null } catch { return null } }
+async function find(path) {
+ let rel
+ try { rel = decodeURIComponent(path) } catch { return null }
+ const p = normalize(join(base, rel))
+ // Inside the folder only. A sibling whose name starts the same way (target-old/) is outside too.
+ if (p !== base && !p.startsWith(base + sep)) return null
+ if (path.endsWith('/')) return tryFile(join(p, 'index.html'))
+ return (await tryFile(p)) || (!extname(p) && ((await tryFile(p + '.html')) || (await tryFile(join(p, 'index.html')))))
+}
+
+async function handle(req, res) {
+ const { pathname } = new URL(req.url, 'http://x')
+ if (req.method !== 'GET' && req.method !== 'HEAD') {
+ let body = ''
+ for await (const c of req) { body += c; if (body.length > 4096) break }
+ await appendFile(log, `${new Date().toISOString()} ${req.method} ${req.url}${body ? ' ' + body.slice(0, 4096).replace(/\s+/g, ' ') : ''}\n`)
+ res.writeHead(405, { 'content-type': 'text/plain' }); return res.end('Not part of the demo.')
+ }
+ const file = await find(pathname)
+ // Never serve the log, the key or dotfiles, even when they sit inside the target folder.
+ if (!file || file === log || /(^|\/)(\.[^/]*|key\.json)$/.test(file)) { res.writeHead(404, { 'content-type': 'text/html; charset=utf-8' }); return res.end('Not builtThis screen is not part of the demo.
') }
+ res.writeHead(200, { 'content-type': types[extname(file)] || 'application/octet-stream', 'cache-control': 'no-store' })
+ res.end(req.method === 'HEAD' ? undefined : await readFile(file))
+}
+
+// A request that fails answers 500 and the server keeps running: one odd request must not end a run's target.
+const server = createServer((req, res) => handle(req, res).catch(e => {
+ console.error(`${req.method} ${req.url}: ${e.message}`)
+ if (!res.headersSent) res.writeHead(500, { 'content-type': 'text/plain' })
+ res.end()
+}))
+server
+ .on('error', e => { if (e.code === 'EADDRINUSE') { console.error(`port ${port} is busy; pass --port `); process.exit(1) } throw e })
+ .listen(port, '127.0.0.1', () => console.log(`target ${base} on http://127.0.0.1:${server.address().port} · log ${log}`))
diff --git a/skills/blind-test/templates/key.template.json b/skills/blind-test/templates/key.template.json
new file mode 100644
index 000000000..44c1d07dc
--- /dev/null
+++ b/skills/blind-test/templates/key.template.json
@@ -0,0 +1,37 @@
+{
+ "target": "",
+ "request": "",
+ "claims": [
+ {
+ "id": "C1",
+ "text": ""
+ }
+ ],
+ "items": [
+ {
+ "id": "K1",
+ "type": "K",
+ "claim": "C1",
+ "class": "",
+ "role": "",
+ "what": "",
+ "evidence": "",
+ "found": "",
+ "partial": ""
+ },
+ {
+ "id": "T1",
+ "type": "T",
+ "claim": "C1",
+ "class": "",
+ "what": "",
+ "why_fine": ""
+ },
+ {
+ "id": "F1",
+ "type": "F",
+ "what": "",
+ "correct": ""
+ }
+ ]
+}
diff --git a/skills/blind-test/templates/report.template.md b/skills/blind-test/templates/report.template.md
new file mode 100644
index 000000000..73ea9f697
--- /dev/null
+++ b/skills/blind-test/templates/report.template.md
@@ -0,0 +1,43 @@
+# Blind test:
+
+ · · · runs per arm
+
+
+
+## What was tested
+
+- **Claims:** C1 …; C2 …
+- **Target:** ,
+- **Request:** ""
+- **Arms:** baseline (no skill), treatment ( )[, competitor ( )]
+
+## Calibration
+
+…. Quick check: skipped, so the roles are guesses>
+
+## Result
+
+
+
+## By class
+
+- **Only with the skill:** —
+- **Only without it:** —
+- **Neither:** —
+- **Traps:**
+
+## Beyond the key
+
+
+
+## What changed in the skill
+
+ → rerun:
+
+## Grading
+
+
+
+## Limits
+
+