prml-playground / index.html
Cuneyt Ozturk
Docs: conformance vector count 20->21 (13 v0.1 + 8 v0.2)
293f4e3
Raw
History Blame Contribute Delete
22.1 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Playground - write, lock, verify a PRML manifest in your browser - falsify.dev</title>
<meta name="description" content="Interactive lab. Edit a PRML manifest, see canonical bytes update, compute SHA-256 in-browser, optionally anchor to the public registry. No server roundtrip." />
<meta name="theme-color" content="#0B0F14" />
<meta property="og:title" content="PRML Playground - interactive manifest lab" />
<meta property="og:description" content="Edit a manifest. Watch canonical bytes update. Compute SHA-256 in-browser. Optionally anchor publicly." />
<meta property="og:type" content="website" />
<meta property="og:url" content="https://falsify.dev/playground" />
<meta property="og:image" content="https://falsify.dev/og.png" />
<link rel="canonical" href="https://falsify.dev/playground" />
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect x='0.75' y='0.75' width='30.5' height='30.5' fill='%230B0F14' stroke='%2339D98A' stroke-width='1.5'/%3E%3Cpolygon points='8,16 24,7.2 20,7.2 13.6,16 20,24.8 24,24.8' fill='%2339D98A'/%3E%3C/svg%3E" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;700&family=DM+Sans:wght@300;400;500&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet" />
<style>
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
:root{
--black:#0B0F14;--surface:#0F1319;--elevated:#141921;
--border:#1A2230;--border-light:#242E3C;
--muted:#4A5568;--gray:#6A7585;--silver:#8A94A0;
--light:#c8c9d4;--white:#E8EEF2;--bright:#f4f4f7;
--signal:#39D98A;--signal-mid:#2BA86E;--signal-dim:#163126;
--warn:#d4a843;--danger:#c45c5c;
--display:'Space Grotesk','DM Sans',system-ui,sans-serif;
--sans:'DM Sans',system-ui,sans-serif;
--mono:'JetBrains Mono','SF Mono',monospace;
}
html{scroll-behavior:smooth;-webkit-text-size-adjust:100%}
body{background:var(--black);color:var(--light);font-family:var(--sans);font-weight:300;line-height:1.6;min-height:100vh}
::selection{background:var(--signal);color:var(--black)}
a{color:var(--signal);text-decoration:underline;text-underline-offset:3px;text-decoration-thickness:1px}
a:hover{color:var(--bright)}
nav#nav{position:fixed;top:0;left:0;right:0;z-index:100;display:flex;align-items:center;justify-content:space-between;gap:16px;padding:0 1.5rem;height:56px;background:rgba(11,15,20,.85);backdrop-filter:blur(12px);border-bottom:1px solid var(--border)}
.nlogo{display:flex;align-items:center;gap:8px;text-decoration:none}
.nlogotext{font-family:var(--display);font-size:18px;font-weight:700;letter-spacing:-.04em;color:var(--signal);line-height:1}
.nlinks{display:flex;align-items:center;gap:22px;flex-wrap:wrap}
.nlinks a{font-family:var(--mono);font-size:9px;letter-spacing:.12em;color:var(--muted);text-decoration:none;text-transform:uppercase;transition:color .3s}
.nlinks a:hover{color:var(--silver)}
.ncta{font-family:var(--mono);font-size:9px;letter-spacing:.12em;color:var(--signal);text-decoration:none;text-transform:uppercase;border:1px solid var(--signal-dim);padding:6px 14px;transition:all .3s}
.ncta:hover{border-color:var(--signal-mid);color:var(--bright);background:rgba(57,217,138,.05)}
main{max-width:84rem;margin:0 auto;padding:6rem 1.5rem 3rem}
@media(min-width:768px){main{padding:7rem 3rem 4rem}}
.tag{font-family:var(--mono);font-size:10px;letter-spacing:.2em;color:var(--muted);text-transform:uppercase;margin-bottom:14px;display:flex;align-items:center;gap:10px}
.tag::before{content:'';width:24px;height:1px;background:var(--signal-mid)}
h1{font-family:var(--display);font-size:clamp(1.6rem,3.6vw,2.25rem);color:var(--white);font-weight:500;line-height:1.15;letter-spacing:-.02em;margin-bottom:8px}
h1 .sig{color:var(--signal)}
.lede{font-size:14.5px;line-height:1.65;color:var(--silver);margin-bottom:28px}
.lab{display:grid;grid-template-columns:1fr;gap:18px}
@media(min-width:1024px){.lab{grid-template-columns:1fr 1fr}}
.panel{border:1px solid var(--border);background:var(--surface)}
.panel-head{display:flex;align-items:center;justify-content:space-between;padding:12px 18px;border-bottom:1px solid var(--border)}
.panel-title{font-family:var(--mono);font-size:11px;letter-spacing:.18em;color:var(--silver);text-transform:uppercase}
.panel-action{font-family:var(--mono);font-size:10px;letter-spacing:.12em;color:var(--muted);text-transform:uppercase;background:transparent;border:1px solid var(--border);padding:5px 10px;cursor:pointer;transition:all .2s}
.panel-action:hover{border-color:var(--signal-mid);color:var(--signal)}
.panel-action.copied{color:var(--signal);border-color:var(--signal-mid)}
textarea, pre{width:100%;min-height:340px;padding:14px 18px;background:var(--black);color:var(--bright);font-family:var(--mono);font-size:13px;line-height:1.6;resize:vertical;border:none;outline:none;display:block}
textarea{caret-color:var(--signal)}
textarea:focus{outline:none}
pre{margin:0;overflow:auto;white-space:pre-wrap;word-break:break-all}
pre.hash{min-height:auto;padding:14px 18px;font-size:12px;color:var(--signal);background:var(--black);word-break:break-all;line-height:1.5}
.status{padding:10px 18px;font-family:var(--mono);font-size:11px;letter-spacing:.05em;color:var(--gray);border-top:1px solid var(--border);background:var(--elevated);display:flex;justify-content:space-between;align-items:center;gap:12px;flex-wrap:wrap}
.status .ok{color:var(--signal)}
.status .err{color:var(--danger)}
.status .warn{color:var(--warn)}
.status .small{font-size:10px;color:var(--muted)}
.controls{margin:24px 0 12px;display:flex;flex-wrap:wrap;gap:10px;align-items:center}
.btn{font-family:var(--mono);font-size:11px;letter-spacing:.14em;color:var(--black);text-decoration:none;text-transform:uppercase;background:var(--signal);padding:11px 18px;border:none;cursor:pointer;transition:background .2s}
.btn:hover{background:var(--signal-mid)}
.btn:disabled{opacity:.5;cursor:not-allowed}
.btn-ghost{background:transparent;color:var(--silver);border:1px solid var(--border)}
.btn-ghost:hover{background:transparent;color:var(--bright);border-color:var(--silver)}
.examples{font-family:var(--mono);font-size:10px;color:var(--muted);letter-spacing:.06em}
.examples a{color:var(--signal-mid);text-decoration:none;border-bottom:1px dotted var(--signal-dim);cursor:pointer;margin-right:14px}
.examples a:hover{color:var(--signal)}
.results{margin-top:18px;padding:18px;border:1px solid var(--border);background:var(--surface);display:none}
.results.show{display:block}
.results .row{display:flex;align-items:center;justify-content:space-between;padding:8px 0;border-bottom:1px solid var(--border);gap:14px;flex-wrap:wrap}
.results .row:last-child{border-bottom:none}
.results .label{font-family:var(--mono);font-size:10px;letter-spacing:.14em;color:var(--muted);text-transform:uppercase}
.results .value{font-family:var(--mono);font-size:12px;color:var(--bright);word-break:break-all}
.results .copy-inline{font-family:var(--mono);font-size:9px;letter-spacing:.12em;color:var(--silver);background:transparent;border:1px solid var(--border);padding:3px 8px;cursor:pointer;text-transform:uppercase;transition:all .2s}
.results .copy-inline:hover{border-color:var(--signal-mid);color:var(--signal)}
.results .copy-inline.copied{color:var(--signal);border-color:var(--signal-mid)}
.disclaimer{margin-top:32px;padding:14px 18px;border-left:2px solid var(--border-light);background:transparent;font-size:12.5px;color:var(--muted);line-height:1.7;font-style:italic}
footer{padding:32px 0 48px;border-top:1px solid var(--border);max-width:84rem;margin:0 auto;padding-left:1.5rem;padding-right:1.5rem;margin-top:48px}
@media(min-width:768px){footer{padding-left:3rem;padding-right:3rem}}
.fnav{display:flex;flex-wrap:wrap;gap:18px;margin-top:14px}
.fnav a{font-family:var(--mono);font-size:10px;letter-spacing:.1em;color:var(--muted);text-decoration:none;text-transform:uppercase}
.fnav a:hover{color:var(--signal)}
</style>
</head>
<body>
<nav id="nav">
<a href="https://falsify.dev/" class="nlogo">
<svg width="22" height="22" viewBox="0 0 32 32" fill="none"><rect x="0.75" y="0.75" width="30.5" height="30.5" fill="#0B0F14" stroke="#39D98A" stroke-width="1.5"/><polygon points="8,16 24,7.2 20,7.2 13.6,16 20,24.8 24,24.8" fill="#39D98A"/></svg>
<span class="nlogotext">falsify</span>
</a>
<div class="nlinks">
<a href="https://spec.falsify.dev/v0.1">Spec</a>
<a href="https://registry.falsify.dev">Registry</a>
<a href="https://falsify.dev/sprint">Sprint</a>
<a href="https://falsify.dev/check">Check</a>
<a href="https://falsify.dev/integrity">Index</a>
<a href="https://falsify.dev/compliance">Compliance</a>
<a href="https://falsify.dev/playground" style="color:var(--signal)">Playground</a>
<a href="https://falsify.dev/notes">Notes</a>
</div>
<a href="https://github.com/studio-11-co/falsify" class="ncta">View Repo</a>
</nav>
<main>
<span class="tag">Playground &middot; In-browser &middot; No server roundtrip</span>
<h1>Edit a manifest. <span class="sig">Watch the hash update in real time.</span></h1>
<p class="lede">Edit the YAML on the left. The right panel shows the canonical bytes (per PRML v0.1 &sect;4) and the SHA-256 in-browser. When you're ready, copy the manifest to a file or anchor the hash publicly at registry.falsify.dev. Examples below load common patterns.</p>
<div class="controls">
<span class="examples">
<a data-ex="minimal">minimal accuracy</a>
<a data-ex="rlhf">RLHF win-rate</a>
<a data-ex="streaming">streaming Elo (v0.2)</a>
<a data-ex="revoked">revoked (v0.2)</a>
</span>
</div>
<div class="lab">
<div class="panel">
<div class="panel-head">
<span class="panel-title">manifest.yaml &mdash; edit me</span>
<button class="panel-action" id="copy-yaml">copy</button>
</div>
<textarea id="src" spellcheck="false" autocomplete="off"></textarea>
<div class="status" id="src-status">
<span class="small">YAML &middot; auto-canonicalises on every keystroke</span>
<span id="src-msg" class="ok">ready</span>
</div>
</div>
<div class="panel">
<div class="panel-head">
<span class="panel-title">canonical bytes &middot; SHA-256</span>
<button class="panel-action" id="copy-canon">copy</button>
</div>
<pre id="canonical">(loading&hellip;)</pre>
<div class="status">
<span class="small">UTF-8 &middot; LF endings &middot; sorted keys &middot; one trailing newline</span>
<span id="byte-count" class="small">0 bytes</span>
</div>
<div class="panel-head" style="border-top:1px solid var(--border);border-bottom:none">
<span class="panel-title">sha-256</span>
<button class="panel-action" id="copy-hash">copy</button>
</div>
<pre id="hash" class="hash">(computing&hellip;)</pre>
</div>
</div>
<div class="controls" style="margin-top:24px">
<button class="btn" id="anchor">Anchor publicly &rarr;</button>
<button class="btn btn-ghost" id="download">Download manifest.yaml</button>
<button class="btn btn-ghost" id="badge">Generate badge</button>
</div>
<div class="results" id="results">
<div class="row">
<span class="label">Permalink</span>
<span><span id="permalink" class="value">-</span> <button class="copy-inline" data-target="permalink">copy</button></span>
</div>
<div class="row">
<span class="label">README badge (Markdown)</span>
<span><span id="badge-md" class="value">-</span> <button class="copy-inline" data-target="badge-md">copy</button></span>
</div>
<div class="row">
<span class="label">CLI verify command</span>
<span><span id="cli-verify" class="value">-</span> <button class="copy-inline" data-target="cli-verify">copy</button></span>
</div>
</div>
<div class="disclaimer">
Pure browser computation. The SHA-256 is computed via the Web Crypto API (<code>crypto.subtle.digest</code>) on your device. Canonicalisation here uses <code>js-yaml</code>'s deterministic emit (sorted keys, block style, LF endings, single trailing newline, UTF-8) - close to the v0.1 reference but <strong>may differ in edge cases</strong> (string quoting, float formatting). For audit-grade hashes, run <code>falsify lock</code> from the Python reference implementation. The 13 v0.1 conformance vectors at <a href="https://github.com/studio-11-co/falsify/tree/main/spec/test-vectors/v0.1">github.com/studio-11-co/falsify</a> document the byte-exact expected output for canonical inputs. No data leaves your browser unless you click <em>Anchor publicly</em>. <a href="https://falsify.dev/check">/check</a> runs heuristic claim analysis - <a href="https://falsify.dev/integrity">/integrity</a> shows scored real-world claims - <a href="https://spec.falsify.dev/v0.1">spec.falsify.dev/v0.1</a> is the canonical specification.
</div>
</main>
<footer>
<p style="font-family:var(--mono);font-size:10px;letter-spacing:.1em;color:var(--muted);text-transform:uppercase">C&uuml;neyt &Ouml;zt&uuml;rk &middot; CC BY 4.0 &middot; In-browser SHA-256 via <code>crypto.subtle</code></p>
<nav class="fnav">
<a href="/">Home</a>
<a href="https://spec.falsify.dev/v0.1">Spec</a>
<a href="https://spec.falsify.dev/schema/">Schema</a>
<a href="https://registry.falsify.dev">Registry</a>
<a href="https://falsify.dev/check">Check</a>
<a href="https://falsify.dev/integrity">Index</a>
<a href="https://falsify.dev/compliance">Compliance</a>
<a href="https://github.com/studio-11-co/falsify">GitHub</a>
</nav>
</footer>
<!-- js-yaml for parsing user YAML -->
<script src="https://cdn.jsdelivr.net/npm/js-yaml@4.1.0/dist/js-yaml.min.js" crossorigin="anonymous"></script>
<script>
const EXAMPLES = {
minimal: `version: prml/0.1
claim_id: 01900000-0000-7000-8000-000000000000
created_at: "2026-05-09T20:00:00Z"
metric: accuracy
comparator: ">="
threshold: 0.92
dataset:
id: imagenet-val-2012
hash: sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
seed: 42
producer:
id: falsify.dev
`,
rlhf: `version: prml/0.1
claim_id: 01900000-0000-7000-8000-000000000020
created_at: "2026-05-09T20:00:00Z"
metric: length_controlled_win_rate
comparator: ">="
threshold: 0.55
dataset:
id: alpaca-eval-2-2024-08
hash: huggingface:revision-d8f3e1a2
uri: https://huggingface.co/datasets/tatsu-lab/alpaca_eval
seed: 42
producer:
id: falsify.dev
model:
id: experimental-rlhf-2026-05-09
notes: |
AlpacaEval 2.0, judge: gpt-4-1106-preview, baseline: gpt-4-0613.
805 prompts, temperature 0.0, single completion per prompt.
`,
streaming: `version: prml/0.2
claim_id: 01900000-0000-7000-8000-000000000014
prml_mode: streaming
metric: elo_rating
value_method: lmsys_anonymous_chat_arena_v1
comparator: ">="
threshold: 1300
dataset:
id: lmsys-arena-live
hash: n/a-streaming
seed: null
producer:
id: falsify.dev
model:
id: claude-3.5-sonnet@2025-10-01
sample_size: 1000
pre_registered_from: "2026-05-01T00:00:00Z"
pre_registered_to: "2026-06-01T00:00:00Z"
`,
revoked: `version: prml/0.2
claim_id: 01900000-0000-7000-8000-000000000016
created_at: "2026-04-01T12:00:00Z"
metric: accuracy
comparator: ">="
threshold: 0.92
dataset:
id: imagenet-val-2012
hash: sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
seed: 42
producer:
id: falsify.dev
revoked_at: "2026-05-15T10:00:00Z"
revocation_reason: dataset_compromised
`,
};
const $ = (id) => document.getElementById(id);
const src = $('src');
const canonicalEl = $('canonical');
const hashEl = $('hash');
const srcMsg = $('src-msg');
const byteCountEl = $('byte-count');
// ---- Canonicalisation: deterministic YAML emit matching v0.1 reference ----
function sortObject(o) {
if (Array.isArray(o)) return o.map(sortObject);
if (o !== null && typeof o === 'object') {
const out = {};
Object.keys(o).sort().forEach(k => { out[k] = sortObject(o[k]); });
return out;
}
return o;
}
// Approximate js-yaml canonical dump matching PyYAML safe_dump(default_flow_style=false, sort_keys=true, allow_unicode=true)
function canonicalise(obj) {
if (typeof jsyaml === 'undefined') throw new Error('YAML lib not loaded');
let s = jsyaml.dump(sortObject(obj), {
indent: 2,
sortKeys: true,
lineWidth: -1,
noRefs: true,
noCompatMode: false,
quotingType: "'",
forceQuotes: false,
flowLevel: -1,
});
s = s.replace(/\r\n/g, '\n').replace(/\s+$/g, '') + '\n';
return s;
}
async function sha256(text) {
const buf = new TextEncoder().encode(text);
const digest = await crypto.subtle.digest('SHA-256', buf);
return Array.from(new Uint8Array(digest)).map(b => b.toString(16).padStart(2, '0')).join('');
}
async function update() {
const text = src.value;
let parsed;
try {
parsed = jsyaml.load(text);
} catch (e) {
canonicalEl.textContent = '(YAML parse error: ' + e.message + ')';
hashEl.textContent = '-';
srcMsg.className = 'err';
srcMsg.textContent = 'parse error';
byteCountEl.textContent = '- bytes';
return;
}
if (parsed === undefined || parsed === null) {
canonicalEl.textContent = '(empty manifest)';
hashEl.textContent = '-';
srcMsg.className = 'warn';
srcMsg.textContent = 'empty';
byteCountEl.textContent = '0 bytes';
return;
}
let canon;
try {
canon = canonicalise(parsed);
} catch (e) {
canonicalEl.textContent = '(canonicalisation error: ' + e.message + ')';
hashEl.textContent = '-';
srcMsg.className = 'err';
srcMsg.textContent = 'canon error';
return;
}
canonicalEl.textContent = canon;
byteCountEl.textContent = new Blob([canon]).size + ' bytes';
const h = await sha256(canon);
hashEl.textContent = 'sha256:' + h;
srcMsg.className = 'ok';
srcMsg.textContent = 'canonical';
}
let updateTimer;
src.addEventListener('input', () => {
clearTimeout(updateTimer);
updateTimer = setTimeout(update, 80);
});
document.querySelectorAll('[data-ex]').forEach(a => {
a.addEventListener('click', (e) => {
e.preventDefault();
src.value = EXAMPLES[a.dataset.ex];
update();
});
});
// Copy buttons
function bindCopy(btnId, sourceFn, label = 'copy') {
const btn = $(btnId);
if (!btn) return;
btn.addEventListener('click', async () => {
try {
await navigator.clipboard.writeText(sourceFn());
btn.textContent = 'copied';
btn.classList.add('copied');
setTimeout(() => { btn.textContent = label; btn.classList.remove('copied'); }, 1300);
} catch (e) { btn.textContent = 'fail'; }
});
}
bindCopy('copy-yaml', () => src.value);
bindCopy('copy-canon', () => canonicalEl.textContent);
bindCopy('copy-hash', () => hashEl.textContent);
// Download manifest.yaml
$('download').addEventListener('click', () => {
const blob = new Blob([src.value], { type: 'text/yaml' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url; a.download = 'manifest.yaml';
document.body.appendChild(a); a.click();
document.body.removeChild(a); URL.revokeObjectURL(url);
});
// Anchor button - POST to registry
$('anchor').addEventListener('click', async () => {
const yaml = src.value;
const btn = $('anchor');
btn.disabled = true; btn.textContent = 'anchoring\u2026';
try {
const res = await fetch('https://registry.falsify.dev/commit', {
method: 'POST',
headers: { 'Content-Type': 'text/yaml' },
body: yaml,
});
if (!res.ok) {
throw new Error('registry returned ' + res.status);
}
const data = await res.json();
showResults(data);
} catch (e) {
btn.textContent = 'anchor failed: ' + e.message;
setTimeout(() => { btn.textContent = 'Anchor publicly \u2192'; btn.disabled = false; }, 4000);
return;
}
btn.textContent = '\u2713 anchored';
setTimeout(() => { btn.textContent = 'Anchor publicly \u2192'; btn.disabled = false; }, 4000);
});
function showResults(data) {
const hash = (data.hash || '').replace(/^sha256:/, '');
const permalink = data.permalink || ('https://registry.falsify.dev/' + hash);
const badgeUrl = 'https://registry.falsify.dev/badge/' + hash + '.svg';
const badgeMd = `[![PRML locked](${badgeUrl})](${permalink})`;
const cli = `falsify verify manifest.yaml --hash sha256:${hash}`;
$('permalink').textContent = permalink;
$('badge-md').textContent = badgeMd;
$('cli-verify').textContent = cli;
$('results').classList.add('show');
}
// Inline copy buttons in results
document.querySelectorAll('.copy-inline').forEach(btn => {
btn.addEventListener('click', async () => {
const target = $(btn.dataset.target);
if (!target) return;
try {
await navigator.clipboard.writeText(target.textContent);
btn.textContent = 'copied';
btn.classList.add('copied');
setTimeout(() => { btn.textContent = 'copy'; btn.classList.remove('copied'); }, 1300);
} catch (e) {}
});
});
// Badge button - short-circuit anchor for hash-only viewers
$('badge').addEventListener('click', () => {
const h = (hashEl.textContent || '').replace(/^sha256:/, '');
if (!h || h === '-' || h.length !== 64) {
alert('Compute a valid hash first (edit the manifest).');
return;
}
const badgeUrl = 'https://registry.falsify.dev/badge/custom.svg?label=PRML&value=' + encodeURIComponent('sha256:' + h.slice(0, 7)) + '&color=signal';
const badgeMd = `[![PRML](${badgeUrl})](https://falsify.dev/playground)`;
$('permalink').textContent = '(local - not anchored to public registry)';
$('badge-md').textContent = badgeMd;
$('cli-verify').textContent = `falsify verify manifest.yaml --hash sha256:${h}`;
$('results').classList.add('show');
});
// Init
src.value = EXAMPLES.minimal;
update();
</script>
</body>
</html>