/* 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='')=>`${esc(text)}`;
const cards=arr=>`
`;
function choose(values,selected){return values.map(([value,text])=>``).join('');}
function highlightText(s){return esc(s).replace(/\n/g,' ');}
function changeAlert(msg){state.alert=msg;const node=el('notice');if(!node)return;node.innerHTML=`i${esc(msg)}`;}
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':'']])+`
${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('')+`
Prediction flip rate uses all ${a.n} held-out items in the split; unchanged synonyms count in the denominator and coverage is displayed separately.
`,'Within the selected model and evaluation split.','', 'EVALUATION SIGNAL')}${card('Paired methodology',`
Paired bootstrap Δ range${signed(ci.low)} to ${signed(ci.high)}
`:''}
${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.'}
`,'Validation claims are scoped to this curated synthetic data.','', 'EVIDENCE & LIMITATIONS')}
`+card('Study design and lineage',`
TRAINING
842 synthetic texts
Model fitting uses only the training split.
EVALUATION
130 + 54 held-out
Controlled and novel-pattern challenges are kept separate.
CONDITIONS
6 reproducible
Five transformations plus clean reference; destructive conditions do not receive accuracy claims.
This is an exploratory robustness audit, not a robustness certification, fairness measurement, human-validated paraphrase benchmark or real-world performance study.
`,'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 `
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.
`,'Original and stress conditions use identical source IDs; labels are assumed stable only for exploratory testing.','', 'REPRODUCIBLE MATCHED SET')+`
${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=>`
`).join('') ,'A lexical change does not certify preservation of sentence meaning.','', 'SEMANTIC REVIEW')}
`;}
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 `
${rows.length} matched
${card('Evidence trail',`
Original text and altered text are displayed together. Predictions are model outputs, not human-verified truth for transformed sentences. Showing first 12 matching rows.
No matching examples. Try another condition or search.
'}`,'Each evidence item can be located in the bundled evaluation data.','span-two','PAIRED EXAMPLE REGISTER')}
`;}
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 ``;}
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)+`
${p?p.n+' of '+all.length+' cases':'No cases'}
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.
`,'Risk is the observed error rate after sorting by the selected model’s margin.','', 'SELECTIVE CLASSIFICATION')+`
`);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']])+`
${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',`
Naive Bayes ECE${fmt(ece)}
Number of bins8
Sample originSynthetic held-out
Inference platformOffline browser
ECE depends on binning, sample composition, and an uncalibrated model posterior. It is not a certification that confidence reflects real-world truth.
`,'Word and character TF-IDF scores are similarities; turning them into probabilities would be misleading.','', 'CALIBRATION CAUTION')}
`;}
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 `
Only changed transformations appear in transformed subsets
`+`
${card('Observed subgroup outcomes',bars+`
${scored?'Accuracy assumes the original gold label remains correct after text modifications.':'Destructive edits are excluded from accuracy scoring; flip rate is shown instead.'}
`,'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=>`
${esc(g.name)}
${g.n}
${g.flips}
${scored?fmt(g.accuracy):fmt(g.flipRate)}
`)),'All rows are synthetic and do not support population fairness claims.','', 'MEASURED SUPPORT')}
`;}
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']])+`
${card('Configure thresholds',`
Changing thresholds only changes the research checklist. It never modifies predictions or data.
`,'Explicit release logic rather than a hidden score.','', 'EXPLORATORY GATE RULES')}${card('Audit results',g.checks.map(c=>`
`)),'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();
})();