Spaces:
Running
Running
| <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 · In-browser · 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 §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 — 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 · 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 · SHA-256</span> | |
| <button class="panel-action" id="copy-canon">copy</button> | |
| </div> | |
| <pre id="canonical">(loading…)</pre> | |
| <div class="status"> | |
| <span class="small">UTF-8 · LF endings · sorted keys · 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…)</pre> | |
| </div> | |
| </div> | |
| <div class="controls" style="margin-top:24px"> | |
| <button class="btn" id="anchor">Anchor publicly →</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üneyt Öztürk · CC BY 4.0 · 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 = `[](${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 = `[](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> | |