Spaces:
Running
Running
File size: 30,474 Bytes
f4c6961 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 | /* RobustnessLab AI — dashboard, local-only interaction and exports. MIT. */
(function(){'use strict';
const R=window.RobustnessEngine,M=window.ModelOpsEngine;
const VIEW_INFO={overview:['01 / COMMAND CENTER','Robustness overview','Matched baseline vs. transformed evaluation, with honest coverage accounting.'],matrix:['02 / STRESS MATRIX','Paired perturbation matrix','Compare changed examples, model flips and score changes across stress families.'],cases:['03 / PAIRED EXAMPLES','Trace individual examples','Inspect original wording, deterministic transformation, ground-truth inheritance and predicted labels.'],selective:['04 / RISK & ABSTENTION','Selective prediction research','Sort predictions by each model’s own score margin to study the trade-off between coverage and observed error.'],reliability:['05 / RELIABILITY','Confidence diagnostics','Inspect Naive Bayes posterior probabilities; other methods expose margins but not calibrated probabilities.'],slices:['06 / LANGUAGE SLICES','Subgroup robustness','Compare transformation behavior across English, Spanish, Portuguese and research domains.'],gates:['07 / RESEARCH GATES','Exploratory release checks','Assess research thresholds transparently. These gates are NOT production safety certification.'],sandbox:['08 / SANDBOX & EXPORT','Custom stress-test sandbox','Try your own text locally and export methods, evidence snapshots or audit reports.']};
const state={view:'overview',method:'hybrid',split:'challenge',attack:'typo',records:[],manifest:null,modelBundle:null,train:[],coverage:80,cases:'all',query:'',slice:'language',gateFlip:15,gateDrop:10,gateMin:40,sandboxText:'The findings indicate that online education may increase participation, although evidence remains limited.',sandboxLang:'en',sandboxAttack:'typo',sandboxResult:null,alert:''};
const el=id=>document.getElementById(id);
const esc=s=>String(s??'').replace(/[&<>"']/g,ch=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[ch]));
const fmt=(v,dp=1)=>v==null||!Number.isFinite(v)?'N/A':(v*100).toFixed(dp)+'%';
const signed=v=>v==null?'N/A':(v>=0?'+':'')+(v*100).toFixed(1)+' pp';
const num=x=>Number(x).toLocaleString('en-US');
const attackInfo=id=>R.ATTACKS.find(x=>x.id===id);
const current=(attack=state.attack,method=state.method,split=state.split)=>state.records.filter(r=>r.method===method&&r.split===split&&r.attack===attack);
const summary=(attack=state.attack,method=state.method,split=state.split)=>R.summarize(state.records,method,split,attack);
const original=()=>current('original');
const pill=(text,cls='')=>`<span class="pill ${cls}">${esc(text)}</span>`;
const cards=arr=>`<div class="stats">${arr.map(x=>`<div class="stat"><span class="stat-label">${esc(x[0])}</span><b class="${x[3]||''}">${esc(x[1])}</b><small>${esc(x[2]||'')}</small></div>`).join('')}</div>`;
const card=(title,content,desc='',cls='',kicker='RESEARCH NOTE')=>`<section class="card ${cls}"><div class="card-kicker">${esc(kicker)}</div><h3>${esc(title)}</h3>${desc?`<p class="card-sub">${esc(desc)}</p>`:''}${content}</section>`;
const bar=(name,val,display,cls='')=>`<div class="metric-row"><span class="metric-name">${esc(name)}</span><div class="bar-track"><div class="bar-fill ${cls}" style="width:${Math.max(0,Math.min(100,100*(val||0)))}%"></div></div><span class="metric-value">${esc(display||fmt(val))}</span></div>`;
const table=(head,rows)=>`<div class="table-scroll"><table class="data-table"><thead><tr>${head.map(h=>`<th>${esc(h)}</th>`).join('')}</tr></thead><tbody>${rows.join('')}</tbody></table></div>`;
function choose(values,selected){return values.map(([value,text])=>`<option value="${esc(value)}" ${value===selected?'selected':''}>${esc(text)}</option>`).join('');}
function highlightText(s){return esc(s).replace(/\n/g,'<br>');}
function changeAlert(msg){state.alert=msg;const node=el('notice');if(!node)return;node.innerHTML=`<span class="notice-icon">i</span><span>${esc(msg)}</span>`;}
function renderOverview(){const a=summary(),base=original(),clean=R.accuracy(base),changed=current().filter(x=>x.changed),bdelta=a.scored?a.accuracyDelta:null,ci=a.scored&&a.changed>=2?R.pairedBootstrap(state.records,state.method,state.split,state.attack):null;const conditions=R.ATTACKS.filter(x=>x.id!=='original').map(at=>summary(at.id));return cards([['Original accuracy',fmt(clean),`${base.length} synthetic hold-out rows`],['Changed inputs',num(a.changed),`${a.n} total · ${fmt(a.changedShare)} coverage`],['Prediction flips',fmt(a.flipRate),`${a.flips} / ${a.n} paired IDs`],['Matched accuracy Δ',a.scored?signed(bdelta):'Not scored',a.scored?'Inherited labels · modified subset':'Destructive condition',bdelta<-.08?'bad':'']])+`<div class="grid-two">${card('Robustness by stress family',conditions.map((r,i)=>bar(attackInfo(r.attack).name,r.flipRate,fmt(r.flipRate),r.attack==='context'?'purple':r.attack==='truncate'?'red':'' )).join('')+`<p class="fine-print">Prediction flip rate uses all ${a.n} held-out items in the split; unchanged synonyms count in the denominator and coverage is displayed separately.</p>`,'Within the selected model and evaluation split.','', 'EVALUATION SIGNAL')}${card('Paired methodology',`<div class="kv"><span>Selected model</span><strong>${esc(R.METHOD_NAMES[state.method])}</strong></div><div class="kv"><span>Evaluation split</span><strong>${state.split==='challenge'?'Novel-pattern challenge':'Controlled IID'}</strong></div><div class="kv"><span>Stress condition</span><strong>${esc(attackInfo(state.attack).name)}</strong></div><div class="kv"><span>Changed / attempted</span><strong>${a.changed} / ${a.n}</strong></div><div class="kv"><span>Matched baseline accuracy</span><strong>${a.scored?fmt(a.baselineMatchedAccuracy):'Not valid'}</strong></div><div class="kv"><span>Transformed subset accuracy</span><strong>${a.scored?fmt(a.accuracy):'Not valid'}</strong></div>${ci?`<div class="kv"><span>Paired bootstrap Δ range</span><strong>${signed(ci.low)} to ${signed(ci.high)}</strong></div>`:''}<div class="notice-block">${esc(attackInfo(state.attack).meaning)}. ${a.scored?'Labels are inherited for diagnostic use, not verified after editing.':'Information removal can change the correct label; do not interpret an accuracy value.'}</div>`,'Validation claims are scoped to this curated synthetic data.','', 'EVIDENCE & LIMITATIONS')}</div>`+card('Study design and lineage',`<div class="grid-three"><div><span class="mini-heading muted">TRAINING</span><h3>842 synthetic texts</h3><p class="fine-print">Model fitting uses only the training split.</p></div><div><span class="mini-heading muted">EVALUATION</span><h3>130 + 54 held-out</h3><p class="fine-print">Controlled and novel-pattern challenges are kept separate.</p></div><div><span class="mini-heading muted">CONDITIONS</span><h3>6 reproducible</h3><p class="fine-print">Five transformations plus clean reference; destructive conditions do not receive accuracy claims.</p></div></div><div class="notice-block">This is an exploratory robustness audit, not a robustness certification, fairness measurement, human-validated paraphrase benchmark or real-world performance study.</div>`,'Every observed outcome is traceable to an original example ID and script.','', 'SOURCE PROVENANCE');}
function renderMatrix(){const data=R.ATTACKS.map(at=>summary(at.id));const rows=data.map(s=>{const a=attackInfo(s.attack);return `<tr><td><strong>${esc(a.name)}</strong><br><span class="tiny">${esc(a.level)}</span></td><td class="align-right">${s.changed}/${s.n}</td><td class="align-right">${fmt(s.changedShare)}</td><td class="align-right">${fmt(s.flipRate)}</td><td class="align-right">${s.scored?fmt(s.accuracy):pill('Not valid','amber')}</td><td class="align-right">${s.scored?signed(s.accuracyDelta):'N/A'}</td></tr>`;});return cards([['Clean baseline',fmt(summary('original').accuracy),'Unchanged reference score'],['Stress families','5','4 label-inheritance assumptions'],['Perturbed records',num(state.records.filter(x=>x.method===state.method&&x.split===state.split&&x.attack!=='original').length),'In selected split'],['Meaning-preserving?','Not proven','Dictionary & context require human review']])+card('Stress comparison table',table(['Stress family','Changed','Coverage','Prediction flips','Matched accuracy','Δ vs matched baseline'],rows)+`<div class="notice-block">Each modified-subset accuracy uses ONLY records that actually changed; matched baseline accuracy is calculated on those same IDs. Destructive truncation is tracked for prediction flips only.</div>`,'Original and stress conditions use identical source IDs; labels are assumed stable only for exploratory testing.','', 'REPRODUCIBLE MATCHED SET')+`<div class="grid-two">${card('Compare classifiers',R.METHODS.map(m=>{const x=summary(state.attack,m);return bar(R.METHOD_NAMES[m],x.flipRate,fmt(x.flipRate),m===state.method?'':'purple');}).join(''),'Flip rate on the same selected attack and split.','', 'FOUR BASELINES')}${card('Test condition documentation',R.ATTACKS.map(a=>`<div class="kv"><span>${esc(a.name)}</span>${pill(a.level,a.invariant?'neutral':'amber')}</div><p class="fine-print">${esc(a.meaning)}</p>`).join('') ,'A lexical change does not certify preservation of sentence meaning.','', 'SEMANTIC REVIEW')}</div>`;}
function renderCases(){const rows=current().filter(r=>(state.cases==='flips'?r.pred!==r.originalPred:state.cases==='changed'?r.changed:state.cases==='errors'?r.pred!==r.truth:true)&&(!state.query||[r.text,r.originalText,r.truth,r.id,r.language].join(' ').toLowerCase().includes(state.query.toLowerCase())));return `<div class="controls-line"><label for="caseFilter">Filter</label><select id="caseFilter">${choose([['all','All paired rows'],['flips','Prediction flips'],['changed','Text changed'],['errors','Model errors (exploratory)']],state.cases)}</select><label for="caseSearch">Search text or ID</label><input type="search" id="caseSearch" placeholder="e.g. ari-0649" value="${esc(state.query)}"><span class="tiny">${rows.length} matched</span></div><div class="grid-two">${card('Evidence trail',`<p class="fine-print">Original text and altered text are displayed together. Predictions are model outputs, not human-verified truth for transformed sentences. Showing first 12 matching rows.</p>${rows.slice(0,12).map(r=>`<div class="case"><div class="case-head"><span class="case-id">${esc(r.id)} · ${esc(r.language.toUpperCase())} · ${esc(r.domain)}</span>${r.pred===r.originalPred?pill('stable'):pill('FLIPPED','danger')}</div><div class="contrast"><div><h4>ORIGINAL</h4><blockquote>${highlightText(r.originalText)}</blockquote><span class="tag">${esc(r.originalPred)}</span></div><div><h4>MODIFIED · ${esc(r.attack)}</h4><blockquote>${highlightText(r.text)}</blockquote><span class="tag ${r.pred!==r.truth?'bad':''}">${esc(r.pred)}</span></div></div><p class="tiny">Inherited gold label: ${esc(r.truth)} · Score gap: ${r.margin.toFixed(4)} · ${r.changed?'Text changed':'No substitution matched'}</p></div>`).join('')||'<p class="muted">No matching examples. Try another condition or search.</p>'}`,'Each evidence item can be located in the bundled evaluation data.','span-two','PAIRED EXAMPLE REGISTER')}</div>`;}
function riskSvg(series){const W=690,H=260,pad={l:48,r:20,t:20,b:42};const X=x=>pad.l+(W-pad.l-pad.r)*x,Y=y=>pad.t+(H-pad.t-pad.b)*(1-Math.max(0,Math.min(1,y)));const points=series.filter((_,i)=>i%Math.max(1,Math.floor(series.length/55))===0||i===series.length-1).map(p=>`${X(p.coverage).toFixed(1)},${Y(p.risk).toFixed(1)}`).join(' ');return `<svg class="svg-chart" viewBox="0 0 ${W} ${H}" role="img" aria-label="Observed error risk versus model coverage"><line x1="${pad.l}" y1="${Y(0)}" x2="${W-pad.r}" y2="${Y(0)}" stroke="#557080"/><line x1="${pad.l}" y1="${pad.t}" x2="${pad.l}" y2="${Y(0)}" stroke="#557080"/>${[0,.25,.5,.75,1].map(t=>`<line x1="${pad.l}" x2="${W-pad.r}" y1="${Y(t)}" y2="${Y(t)}" stroke="#264356" stroke-dasharray="3,7"/><text x="${pad.l-8}" y="${Y(t)+4}" text-anchor="end" fill="#9bb5c9" font-size="11">${Math.round(t*100)}%</text>`).join('')}${[.25,.5,.75,1].map(t=>`<text x="${X(t)}" y="${H-17}" text-anchor="middle" fill="#9bb5c9" font-size="11">${Math.round(t*100)}%</text>`).join('')}<polyline fill="none" stroke="#61e1bb" stroke-width="3.5" points="${points}"/><text x="${W/2}" y="${H-3}" text-anchor="middle" fill="#b2c8d8" font-size="11">Prediction coverage (top score margins retained)</text></svg>`;}
function renderSelective(){const all=original(),p=R.coverageAt(all,state.coverage/100),risk=R.riskCoverage(all),a=R.aurc(all);return cards([['Total evaluated',num(all.length),'Unchanged source examples'],['Coverage selected',state.coverage+'%',`${p?.n??0} top-margin examples retained`],['Observed risk',p?fmt(p.risk):'N/A','Errors among retained examples'],['Risk AUC (AURC)',a!=null?a.toFixed(3):'N/A','Lower is better on same dataset']])+card('Risk-coverage curve',riskSvg(risk)+`<div class="controls-line"><label for="coverageRange">Retain ${state.coverage}% of predictions</label><input id="coverageRange" type="range" min="10" max="100" step="5" value="${state.coverage}" style="width:min(330px,100%);accent-color:#6de7bd"><span class="pill">${p?p.n+' of '+all.length+' cases':'No cases'}</span></div><div class="notice-block">These are rankings based on each model’s own top-two score margin. Margin values are NOT calibrated probabilities, cannot be compared directly between methods, and do not imply safe automated abstention.</div>`,'Risk is the observed error rate after sorting by the selected model’s margin.','', 'SELECTIVE CLASSIFICATION')+`<div class="grid-two">${card('Coverage checkpoints',table(['Retained share','Kept cases','Observed error'],[.25,.50,.75,1].map(c=>{const x=R.coverageAt(all,c);return `<tr><td>${fmt(c,0)}</td><td>${x.n}</td><td>${fmt(x.risk)}</td></tr>`;})),'Same held-out synthetic split; small sample sizes matter.','', 'DIAGNOSTIC TABLE')}${card('What this tells us',`<p class="fine-print">If the model is often wrong even among high-margin predictions, margin-based rejection will not reliably isolate correct decisions.</p><div class="kv"><span>Scoring method</span><strong>Top-two difference</strong></div><div class="kv"><span>Selection mechanism</span><strong>Within-method ranking</strong></div><div class="kv"><span>Validated calibration</span><strong class="bad">No</strong></div><div class="kv"><span>External test set</span><strong class="bad">Not included</strong></div>`,'AURC is a descriptive statistic on a fixed evaluation sample.','', 'INTERPRETATION')}</div>`;}
function renderReliability(){const row=current('original','nb'),bins=R.reliability(row,8),ece=R.ece(row,8),lines=bins.map(b=>`<tr><td>${fmt(b.low,0)}–${fmt(b.high,0)}</td><td>${b.n}</td><td>${fmt(b.meanConfidence)}</td><td>${fmt(b.empiricalAccuracy)}</td><td>${b.n?fmt(Math.abs(b.meanConfidence-b.empiricalAccuracy)):'N/A'}</td></tr>`);return cards([['NB confidence ECE',fmt(ece),'Descriptive, uncalibrated posterior'],['Evaluation examples',row.length,'Naive Bayes original conditions'],['Nonempty bins',bins.filter(x=>x.n>0).length,'Eight equal-width probability bins'],['Other baselines','Margins only','Not probabilities · not ECE']])+`<div class="grid-two">${card('Naive Bayes reliability bins',table(['Posterior bin','N','Mean posterior','Observed accuracy','Absolute gap'],lines),'The Naive Bayes output is a normalized posterior from the lexical model, not a calibrated guarantee.','', 'PROBABILITY DIAGNOSTIC')}${card('Confidence is not truth',`<div class="kv"><span>Naive Bayes ECE</span><strong>${fmt(ece)}</strong></div><div class="kv"><span>Number of bins</span><strong>8</strong></div><div class="kv"><span>Sample origin</span><strong>Synthetic held-out</strong></div><div class="kv"><span>Inference platform</span><strong>Offline browser</strong></div><div class="notice-block">ECE depends on binning, sample composition, and an uncalibrated model posterior. It is not a certification that confidence reflects real-world truth.</div>`,'Word and character TF-IDF scores are similarities; turning them into probabilities would be misleading.','', 'CALIBRATION CAUTION')}</div>`;}
function renderSlices(){const rows=current().filter(r=>r.changed||state.attack==='original'),scored=attackInfo(state.attack).invariant,groups=R.slices(rows,state.slice);const bars=groups.map(g=>bar(g.name,scored?g.accuracy:g.flipRate,scored?fmt(g.accuracy):fmt(g.flipRate),g.n<8?'amber':'' )).join('');return `<div class="controls-line"><label for="sliceBy">Group by</label><select id="sliceBy">${choose([['language','Language'],['domain','Research domain'],['truth','Original rhetorical label']],state.slice)}</select><span class="tiny">Only changed transformations appear in transformed subsets</span></div>`+`<div class="grid-two">${card('Observed subgroup outcomes',bars+`<div class="notice-block">${scored?'Accuracy assumes the original gold label remains correct after text modifications.':'Destructive edits are excluded from accuracy scoring; flip rate is shown instead.'}</div>`,'Slices with small support are unstable.','', 'SUBGROUP MEASUREMENT')}${card('Subgroup evidence table',table(['Subgroup','N','Prediction flips',scored?'Inherited accuracy':'Flip rate'],groups.map(g=>`<tr><td>${esc(g.name)}</td><td>${g.n}</td><td>${g.flips}</td><td>${scored?fmt(g.accuracy):fmt(g.flipRate)}</td></tr>`)),'All rows are synthetic and do not support population fairness claims.','', 'MEASURED SUPPORT')}</div>`;}
function renderGates(){const thresholds={minN:state.gateMin,maxFlipRate:state.gateFlip/100,maxAccuracyDrop:state.gateDrop/100};const g=R.releaseGate(state.records,state.method,state.split,thresholds);return cards([['Research verdict',g.pass?'CHECKS PASS':'CHECKS FAIL',g.pass?'Thresholds met; not safety certification':'One or more configured thresholds missed',g.pass?'good':'bad'],['Checks passing',g.checks.filter(x=>x.pass).length+'/'+g.checks.length,'Multiple independent rules'],['Maximum flip rate',state.gateFlip+'%','Applies to four non-destructive stress families'],['Max matched accuracy drop',state.gateDrop+' pp','Compared on the same transformed IDs']])+`<div class="grid-two">${card('Configure thresholds',`<div class="control-card"><div><label for="gateFlip">Maximum flip rate (%)</label><input id="gateFlip" type="number" min="0" max="100" step="1" value="${state.gateFlip}"></div><div><label for="gateDrop">Maximum accuracy drop (pp)</label><input id="gateDrop" type="number" min="0" max="100" step="1" value="${state.gateDrop}"></div><div><label for="gateMin">Minimum original test N</label><input id="gateMin" type="number" min="1" max="130" step="1" value="${state.gateMin}"></div></div><div class="report-actions"><button class="btn primary" id="applyGates">Apply research thresholds</button><button class="btn ghost" id="resetGates">Reset</button></div><p class="fine-print">Changing thresholds only changes the research checklist. It never modifies predictions or data.</p>`,'Explicit release logic rather than a hidden score.','', 'EXPLORATORY GATE RULES')}${card('Audit results',g.checks.map(c=>`<div class="kv"><span>${pill(c.pass?'PASS':'FAIL',c.pass?'':'danger')} ${esc(c.rule)}</span><strong>${typeof c.actual==='number'?(c.rule.includes('rate')||c.rule.includes('drop')?fmt(c.actual):c.actual):esc(c.actual)}</strong></div>`).join('')+`<div class="notice-block">Even if all checks pass, that is not proof of robustness, fairness, compliance, safety, or production readiness.</div>`,'Rules cover perturbation flip rates and matched-subset changes.','', 'OBSERVABLE VERDICTS')}</div>`;}
function renderSandbox(){const x=state.sandboxResult;return `<div class="grid-two">${card('Run your own statement',`<label for="sandboxText" class="mini-heading muted">Enter your own sentence (local only)</label><textarea id="sandboxText" rows="5" spellcheck="false" style="resize:vertical;margin:8px 0 11px">${esc(state.sandboxText)}</textarea><div class="controls-line"><label for="sandboxLang">Language</label><select id="sandboxLang">${choose([['en','English'],['es','Español'],['pt','Português']],state.sandboxLang)}</select><label for="sandboxAttack">Condition</label><select id="sandboxAttack">${choose(R.ATTACKS.filter(a=>a.id!=='original').map(a=>[a.id,a.name]),state.sandboxAttack)}</select></div><div class="report-actions"><button class="btn primary" id="sandboxRun">Compare locally</button><button class="btn ghost" id="sandboxExample">Load example</button></div>${x?`<div class="result-inline"><span class="card-kicker">ORIGINAL</span><p>${esc(x.baseText)}</p>${pill(x.before.label)}<hr class="section-divider"><span class="card-kicker">TRANSFORMED</span><p>${esc(x.modified)}</p>${pill(x.after.label,x.before.label===x.after.label?'':'danger')} ${x.before.label!==x.after.label?pill('Prediction changed','danger'):pill('Prediction stable')}</div><p class="fine-print">No original gold label is available for custom text. This tool does not calculate accuracy for personal examples.</p>`:''}`,'Text remains inside the browser. No network inference.','', 'LOCAL COUNTERFACTUAL')}${card('Data downloads and provenance',`<p class="card-sub">Reproduce every reported number, inspect documentation, and cite the source data license separately.</p><div class="report-actions"><button class="btn primary" data-export="report">Research report .md</button><button class="btn" data-export="table">Summary .csv</button><button class="btn" data-export="session">Audit JSON</button><button class="btn" data-export="predictions">All records .csv</button></div><div class="kv"><span>Prediction records</span><strong>${num(state.records.length)}</strong></div><div class="kv"><span>Provenance</span><strong>SHA-256 manifest</strong></div><div class="kv"><span>Code license</span><strong>MIT</strong></div><div class="kv"><span>Dataset license</span><strong>CC BY 4.0</strong></div><div class="notice-block">Source examples are synthetic-curated. Publication does not turn these tests into validated real-world performance measurements.</div>`,'Downloads are generated locally, without uploads to a server.','', 'REPRODUCIBILITY')}</div>`+card('Integrity registry',table(['Asset','SHA-256 digest'],Object.entries(state.manifest?.files||{}).map(([name,hash])=>`<tr><td>${esc(name)}</td><td style="font-family:ui-monospace,SFMono-Regular,monospace;font-size:10px">${esc(hash.slice(0,24))}…</td></tr>`)),'Complete digests are in benchmark_manifest.json and downloadable JSON.','', 'SOURCE LINEAGE');}
const RENDER={overview:renderOverview,matrix:renderMatrix,cases:renderCases,selective:renderSelective,reliability:renderReliability,slices:renderSlices,gates:renderGates,sandbox:renderSandbox};
function render(){if(!state.records.length)return;const info=VIEW_INFO[state.view];el('viewId').textContent=info[0];el('viewTitle').textContent=info[1];el('viewSubtitle').textContent=info[2];el('panel').innerHTML=RENDER[state.view]();document.querySelectorAll('.nav-btn').forEach(b=>b.classList.toggle('active',b.dataset.view===state.view));el('methodSelect').value=state.method;el('splitSelect').value=state.split;el('attackSelect').value=state.attack;}
function report(){const r=summary(),base=summary('original'),ci=r.scored&&r.changed>=2?R.pairedBootstrap(state.records,state.method,state.split,state.attack):null;return `# RobustnessLab AI — auditable stress-test report\n\n- Source: Academic Rhetorical Intent Mini v1.1 (synthetic-curated).\n- Baseline: ${R.METHOD_NAMES[state.method]}\n- Split: ${state.split} (${r.n} examples)\n- Transformation: ${attackInfo(state.attack).name}\n- Original accuracy: ${fmt(base.accuracy)}\n- Actually changed records: ${r.changed}/${r.n}\n- Prediction flips: ${r.flips}/${r.n} (${fmt(r.flipRate)})\n- Matched-subset original accuracy: ${r.scored?fmt(r.baselineMatchedAccuracy):'Not applicable; destructive transformation'}\n- Matched-subset transformed accuracy: ${r.scored?fmt(r.accuracy):'Not applicable; destructive transformation'}\n- Matched accuracy difference: ${r.scored?signed(r.accuracyDelta):'Not applicable'}\n${ci?`- Paired bootstrap interval, 400 iterations (descriptive only): [${signed(ci.low)}, ${signed(ci.high)}]\n`:''}\n## All stress conditions\n\n| Condition | Modified N | Flip rate | Matched accuracy | Delta |\n|---|---:|---:|---:|---:|\n${R.ATTACKS.map(a=>{const s=summary(a.id);return `| ${a.name} | ${s.changed}/${s.n} | ${fmt(s.flipRate)} | ${s.scored?fmt(s.accuracy):'N/A'} | ${s.scored?signed(s.accuracyDelta):'N/A'} |`;}).join('\n')}\n\n## Methodological qualifications\n- Synthetic-curated data only; not real-world accuracy, safety or fairness.\n- Gold labels are inherited after stress edits for exploratory diagnostics. Some transformations may change meaning.\n- Information removal is destructive and receives NO transformed accuracy claim.\n- The synonym perturbation changes only rows with a matching dictionary item; always consider changed-row coverage.\n- Models other than Naive Bayes produce uncalibrated score margins, NOT probabilities.\n- Dataset code: MIT; separate original source data license: CC BY 4.0.\n- Reproduce with node generate_artifacts.js and node test_engine.js.\n`;}
function download(name,content,mime){const blob=new Blob([content],{type:mime});const url=URL.createObjectURL(blob);const a=document.createElement('a');a.href=url;a.download=name;document.body.appendChild(a);a.click();a.remove();setTimeout(()=>URL.revokeObjectURL(url),1500);}
function exportData(kind){if(kind==='report'){download('RobustnessLab_research_report.md',report(),'text/markdown;charset=utf-8');return;}if(kind==='session'){download('RobustnessLab_audit.json',JSON.stringify({schemaVersion:'1.0',selected:{method:state.method,split:state.split,attack:state.attack},manifest:state.manifest,allSummaries:R.METHODS.flatMap(m=>['iid','challenge'].flatMap(split=>R.ATTACKS.map(a=>R.summarize(state.records,m,split,a.id)))),warning:'Synthetic data: do not infer deployment readiness.'},null,2),'application/json');return;}if(kind==='table'){const rows=R.METHODS.flatMap(m=>['iid','challenge'].flatMap(split=>R.ATTACKS.map(a=>R.summarize(state.records,m,split,a.id))));download('RobustnessLab_metrics.csv',R.csv(rows,['method','split','attack','n','changed','changedShare','flips','flipRate','accuracy','baselineMatchedAccuracy','accuracyDelta','scored']),'text/csv;charset=utf-8');return;}if(kind==='predictions'){download('RobustnessLab_predictions.csv',R.csv(state.records,['id','method','split','attack','language','domain','truth','originalPred','pred','changed','margin','originalText','text']),'text/csv;charset=utf-8');return;}}
function sandboxRun(){if(!state.modelBundle)throw Error('Models have not loaded.');const text=state.sandboxText.trim();if(text.length<12||text.length>4000)throw Error('Please enter 12–4,000 characters.');const before=M.predict(state.modelBundle,text,state.method),modified=R.perturb(text,state.sandboxAttack,state.sandboxLang),after=M.predict(state.modelBundle,modified,state.method);state.sandboxResult={baseText:text,modified,before,after};render();}
function setStateAndRender(k,v){state[k]=v;render();}
document.addEventListener('click',e=>{const nav=e.target.closest('[data-view]');if(nav){state.view=nav.dataset.view;render();return;}const exportButton=e.target.closest('[data-export]');if(exportButton){exportData(exportButton.dataset.export);return;}if(e.target.closest('#exportSummary')){exportData('report');return;}if(e.target.closest('#applyGates')){const flip=Number(el('gateFlip').value),drop=Number(el('gateDrop').value),min=Number(el('gateMin').value);if(!Number.isFinite(flip)||!Number.isFinite(drop)||!Number.isInteger(min)||flip<0||flip>100||drop<0||drop>100||min<1||min>130){changeAlert('Invalid thresholds. Use rates between 0 and 100, and a valid minimum sample size.');return;}Object.assign(state,{gateFlip:flip,gateDrop:drop,gateMin:min});render();return;}if(e.target.closest('#resetGates')){Object.assign(state,{gateFlip:15,gateDrop:10,gateMin:40});render();return;}if(e.target.closest('#sandboxExample')){state.sandboxText={en:'Results reveal that the proposed method improved outcomes in a small sample.',es:'Los resultados muestran que la estrategia mejoró el aprendizaje en una muestra limitada.',pt:'Os resultados mostram que a metodologia melhorou a aprendizagem em uma amostra pequena.'}[state.sandboxLang];state.sandboxResult=null;render();return;}if(e.target.closest('#sandboxRun')){state.sandboxText=el('sandboxText').value;state.sandboxLang=el('sandboxLang').value;state.sandboxAttack=el('sandboxAttack').value;try{sandboxRun();}catch(err){changeAlert(err.message);}return;}});
document.addEventListener('change',e=>{if(e.target.id==='methodSelect')setStateAndRender('method',e.target.value);if(e.target.id==='splitSelect')setStateAndRender('split',e.target.value);if(e.target.id==='attackSelect')setStateAndRender('attack',e.target.value);if(e.target.id==='caseFilter')setStateAndRender('cases',e.target.value);if(e.target.id==='sliceBy')setStateAndRender('slice',e.target.value);if(e.target.id==='coverageRange')setStateAndRender('coverage',Number(e.target.value));if(e.target.id==='sandboxLang'){state.sandboxLang=e.target.value;state.sandboxResult=null;render();}if(e.target.id==='sandboxAttack'){state.sandboxAttack=e.target.value;state.sandboxResult=null;render();}});
document.addEventListener('input',e=>{if(e.target.id==='caseSearch'){state.query=e.target.value;const cursor=e.target.selectionStart;render();const b=el('caseSearch');if(b){b.focus();b.setSelectionRange(cursor,cursor);}}if(e.target.id==='coverageRange')setStateAndRender('coverage',Number(e.target.value));});
async function load(){try{if(!R||!M)throw Error('A required script is missing.');const [r,m,t]=await Promise.all([fetch('robustness_snapshots.json'),fetch('benchmark_manifest.json'),fetch('train.json')]);if(!r.ok||!m.ok||!t.ok)throw Error('Cannot load bundled research assets. Open the published Static Space, not the raw HTML file.');const obj=await r.json();state.records=R.parse(obj.rows);state.manifest=await m.json();state.train=await t.json();const a=R.audit(state.records);if(!a.ok||state.records.length!==4416)throw Error('Benchmark records are incomplete.');state.modelBundle=M.train(state.train);el('snapN').textContent=num(state.records.length);el('status').textContent='SNAPSHOT VERIFIED';render();}catch(err){el('status').textContent='LOAD ERROR';el('panel').textContent='Error loading local research assets: '+String(err.message);}}
load();
})();
|