`;
function notify(msg,type='info'){const node=$('notice');node.hidden=false;node.className='notice'+(type==='error'?' error':'');node.textContent=msg;}
function clearNotice(){const n=$('notice');n.hidden=true;n.textContent='';}
function bar(name,value,alternate=false){const x=Math.max(0,Math.min(100,(value||0)*100));return `
${pc(value)}
`;}
function stat(label,value,hint='',cls=''){return `
${esc(label)}${esc(value)}${esc(hint)}
`;}
function rowTable(headers,rows){return `
${headers.map(h=>`
${esc(h)}
`).join('')}
${rows.join('')}
`;}
const txt=(row)=>esc((row.text||'No text supplied').slice(0,240));
function renderOverview(){const all=state.rows,s=E.select(all,state.model,state.split),base=E.select(all,state.model,'iid'),chal=E.select(all,state.model,'challenge'),now=E.metrics(s),b=E.metrics(base),c=E.metrics(chal),comparison=E.compare(all,state.model),g=E.gate(all,state.model,state.threshold),slices=E.segments(s,'language'),audit=E.audit(all);
const summary=describe('Evaluation overview','Snapshot-based monitoring of an academic rhetorical-intent classifier. Controlled IID and novel-pattern challenge are not chronological production runs.');
let vals=`
${esc(state.source)}${audit.clean?'No duplicate IDs across splits':'Data integrity warning'}
The challenging examples intentionally change wording patterns. These benchmark scores are diagnostic, not guaranteed estimates of real-world performance.
${g.pass?'All configured exploratory gates passed. This is not a production certificate.':`${g.checks.filter(x=>!x.pass).length} configured release check(s) failed. Open Release gates to investigate. No overrides are applied automatically.`}
The length-distribution PSI compares text-length histograms; the language and label comparisons use Jensen–Shannon divergence. None identifies the cause of a change by itself.
${slices.map(x=>bar(`${x.name} (n=${x.n})`,x.accuracy)).join('')}`;
}
function renderDrift(){const a=rowSet(state.model,'iid'),b=rowSet(state.model,'challenge'),d=E.drift(a,b);const lenText=['0–4','5–9','10–14','15–19','20–29','30–49','50+'];const max=Math.max(1,...d.baselineLength,...d.candidateLength);return describe('Drift detection','Compare observed properties between controlled and challenge evaluation splits. This is dataset shift, not evidence of live production drift.', 'DATA SHIFT & DISTRIBUTION DIAGNOSTICS')+`
${stat('Text-length PSI',n3(d.lengthPSI),'Bins: word types, not tokens',d.lengthPSI>.25?'warn':'good')}${stat('Language JSD',n3(d.languageJSD),'Range 0–1 bits')}${stat('True-label JSD',n3(d.truthJSD),'Range 0–1 bits')}${stat('Sample volume',`${a.length} → ${b.length}`,'Controlled → challenge')}
WORD-TYPE DISTRIBUTION
Length-bin comparison
ControlledChallenge
${lenText.map((l,i)=>`
${l}
`).join('')}
A high PSI indicates a shift in this particular distribution. Thresholds are user-defined heuristics; PSI is sensitive to binning and smoothing, especially on small samples.
JSD is symmetric divergence measured in bits (0 means equal distributions). PSI compares binned frequencies with a numerical smoothing term. Neither is a formal significance test here.
The challenge set is small and curated. Unequal split sizes alone should not be interpreted as an alarming production change.
`;}
function renderSlices(){const splits=E.segments(rowSet(),state.segment);const select=``;return describe('Slice analysis','Expose weak subgroups rather than relying only on aggregate accuracy. Slice results are descriptive and small samples are clearly marked.')+select+`
Language and academic domain are task categories here, not protected demographic groups. This view identifies variation but does not validate fairness, equity or performance in real deployment.
Any slice with fewer than 10 examples should be treated as highly unstable. Compare supporting counts before ranking models.
`))}`;}
function renderErrors(){const rows=rowSet(),mt=E.metrics(rows),errorRows=rows.filter(x=>x.pred!==x.truth&&(state.errorFilter==='all'||x.truth===state.errorFilter)).slice(0,50),max=Math.max(1,...Object.values(mt.confusion).flatMap(x=>Object.values(x)));const legend=E.LABELS.map(l=>`
`).join('');return describe('Error explorer','Inspect exact misclassifications with original text, true labels and predicted labels. Full text comes only from the bundled synthetic dataset.')+`
'}`;}
function renderRegression(){const selected=rowSet(state.model,'challenge');const other=state.candidate===state.model?'nb':state.candidate;const available=E.select(state.rows,state.model,'challenge').length&&E.select(state.rows,other,'challenge').length;if(!available)return describe('Model regression laboratory','Paired comparisons require challenge predictions for two models.')+'
Load compatible predictions for at least two models with matching example IDs before comparing them. You can restore the bundled benchmark under Export & governance.
';const p=E.pairedComparison(state.rows,state.model,other,'challenge',500,2026);const a=E.metrics(selected),b=E.metrics(rowSet(other,'challenge'));return describe('Model regression laboratory','Compare two methods on identical challenge examples. A paired bootstrap describes uncertainty for this small curated set, not general-purpose statistical significance.')+`
${stat('Reference model',pc(a.accuracy),label(state.model))}${stat('Comparator',pc(b.accuracy),label(other))}${stat('Paired difference',gap(p.delta), 'Comparator − reference')}${stat('Paired n',p.n,'Matched example IDs')}
${p.bWins} examples corrected by comparator, ${p.aWins} examples that the reference got right but comparator missed.
`;}
function renderGates(){const g=E.gate(state.rows,state.model,state.threshold);return describe('Research release gates','Adjust explicit quality thresholds and inspect every pass/fail result. This is an exploratory engineering checklist, not a production approval or safety certification.')+`
GOVERNANCE POLICY
Threshold configuration
Thresholds do not change measured results. Relaxing thresholds may produce a PASS badge without improving the model.
AUDIT RESULT
${g.pass?'CONFIGURED CHECKS PASS':'CONFIGURED CHECKS FAIL'} · ${g.checks.filter(c=>c.pass).length} of ${g.checks.length} passed
A passing research gate is neither an ethical, legal, fairness, robustness nor operational safety certification. External domain validation is required.
`;}
function renderRegistry(){const manifest=state.manifest||{},audit=E.audit(state.rows),records=E.METHODS.flatMap(m=>['iid','challenge'].map(s=>{const mt=E.metrics(rowSet(m,s));return `
${esc(label(m))}
${s==='iid'?'Controlled IID':'Challenge'}
${mt.n}
${pc(mt.accuracy)}
${pc(mt.macroF1)}
deterministic
`}));return describe('Run registry & lineage','Inspect the underlying evaluation runs, their provenance, source licenses and dataset-integrity checks. A snapshot registry, not real-time production telemetry.')+`
${stat('Prediction rows',audit.total,'Four methods × two splits')}${stat('Duplicate keys',audit.duplicates,'split + method + sample ID')}${stat('Cross-split ID overlap',audit.splitIdOverlap,'Should be zero')}${stat('Integrity review',audit.clean?'PASS':'CHECK',audit.clean?'No structural anomalies':'See issues',audit.clean?'good':'warn')}
ARTIFACT LINEAGE
Experiment manifest
${esc(JSON.stringify(manifest,null,2))}
REPRODUCIBILITY & LIMITS
What is and is not being measured
Predictions come from retraining four browser-compatible baselines with the original 842-row synthetic training split. No third-party model APIs, personal records, real-time inference logs or remote monitoring are used.
Data license: CC BY 4.0. Application code license: MIT. The dataset and baseline methods are deliberately small-scale research resources.
Risk: synthetic template-family data may exaggerate performance on easy examples. Do not use the app's numbers for high-stakes deployment decisions.
RECORDED RUNS
Evaluation ledger
${rowTable(['Method','Split','Samples','Accuracy','Macro F1','Reproduction'],records)}`;}
function buildReport(){const a=E.compare(state.rows,state.model),d=a.drift,gate=E.gate(state.rows,state.model,state.threshold),slices=E.segments(rowSet(),'language');let lines=['# ModelGuard Observatory — Research report','','**Provenance:** '+state.source,'**Dataset:** Academic Rhetorical Intent Mini v1.1, synthetic-curated','**Evaluation type:** Static IID versus novel-pattern challenge snapshots (NOT temporal production monitoring)','**Method:** '+label(state.model),'**Visible split:** '+state.split,'','## Evaluation','',`- Controlled accuracy: ${pc(a.iid.accuracy)} (n=${a.iid.n})`,`- Challenge accuracy: ${pc(a.challenge.accuracy)} (n=${a.challenge.n})`,`- Generalization gap: ${gap(a.gap)}`,'','## Shift diagnostics','',`- Token-type count PSI: ${n3(d.lengthPSI)}`,`- Language JSD: ${n3(d.languageJSD)} bits`,`- True-label JSD: ${n3(d.truthJSD)} bits`,'','## Language slices',''];for(const s of slices)lines.push(`- ${s.name}: n=${s.n}; accuracy=${pc(s.accuracy)}; macro-F1=${pc(s.macroF1)}`);lines.push('','## Exploratory release gates','',...gate.checks.map(c=>`- ${c.pass?'PASS':'FAIL'}: ${c.label}. ${c.detail}`),'','## Limitations','', '- Dataset synthetic-curated; narrow academic rhetorical-intent task.','- Comparison across intentionally different dataset splits, not a timestamped stream.','- PSI and JSD measure descriptive shift, not causes or statistical significance.','- Thresholds are researcher configurable and cannot certify safety, bias mitigation or production readiness.','- Model performance on this dataset does not establish external reliability.','',`SHA256: ${state.manifest?.evaluationSha256||'unknown'}`);return lines.join('\n');}
function download(name,contents,mime){const blob=new Blob([contents],{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),1000);notify(name+' downloaded.');}
function renderExport(){return describe('Export & governance','Download underlying predictions, metrics and methodology. You may also import a compatible CSV or JSON snapshot for entirely local auditing.')+`
REPRODUCIBLE DELIVERABLES
Download an audit package
Exported data includes model names, split labels and source provenance. CSV cells are escaped to reduce spreadsheet-formula injection risk.
LOCAL DATA ONLY
Import your evaluation snapshot
Load a JSON array, {'{ "rows": [...] }'} object, or CSV with the required fields: id, split, method, truth, pred. Optional: language, domain, tokenCount, text.
Imported files stay in your browser session, are not uploaded to this app, and are not saved across page reloads. Do not import sensitive personal records.
RESPONSIBLE EVALUATION
Guardrails and citations
Methodological boundaries. Drift, class-level disparity and release gates are diagnostics. A scientist must review external tests, documentation, sampling assumptions, and the consequences of errors.
Governance boundaries. No automatic retraining, decisions affecting individuals, fairness certification, background telemetry or credentials. The Space runs on the client and fetches only its own local research artifacts.
Dataset CC BY 4.0Code MITLocal-only custom importDeterministic baseline replay
`;}
function render(){clearNotice();const content=({overview:renderOverview,drift:renderDrift,slices:renderSlices,errors:renderErrors,regression:renderRegression,gates:renderGates,registry:renderRegistry,export:renderExport}[state.view]||renderOverview)();$('panel').innerHTML=content;$('crumb').textContent=VIEWS[state.view]||'Overview';document.querySelectorAll('[data-view]').forEach(btn=>{btn.classList.toggle('active',btn.dataset.view===state.view);btn.setAttribute('aria-current',btn.dataset.view===state.view?'page':'false');});$('sourceLabel').textContent=state.source.startsWith('Bundled')?'SYNTHETIC-CURATED DATA':'USER IMPORT · LOCAL ONLY';$('heroN').textContent=state.rows.length.toLocaleString('en-US');bindView();}
function performDownload(type){const m=state.model,s=state.split,comp=E.compare(state.rows,m),g=E.gate(state.rows,m,state.threshold);if(type==='report')return download('modelguard_research_report.md',buildReport(),'text/markdown;charset=utf-8');if(type==='metrics')return download('modelguard_metrics.json',JSON.stringify({generatedUTC:new Date().toISOString(),provenance:state.source,method:m,split:s,metrics:comp,gate:g,manifest:state.manifest},null,2),'application/json');if(type==='session')return download('modelguard_session.json',JSON.stringify({schemaVersion:'1.0',provenance:state.source,model:m,split:s,thresholds:state.threshold,rows:state.rows},null,2),'application/json');if(type==='rows')return download('modelguard_predictions.csv',E.csv(state.rows,['split','method','id','language','domain','truth','pred','tokenCount','text']),'text/csv;charset=utf-8');if(type==='template')return download('modelguard_import_template.csv',E.csv([{id:'sample-001',split:'iid',method:'hybrid',truth:'objective',pred:'objective',language:'en',domain:'academic',tokenCount:14,text:'Fictional example only'}],['id','split','method','truth','pred','language','domain','tokenCount','text']),'text/csv;charset=utf-8');}
async function importLocal(){const f=$('inputFile')?.files?.[0];if(!f)return notify('Choose a JSON or CSV file first.','error');if(f.size>12*1024*1024)return notify('File exceeds the 12 MB local import limit.','error');try{const text=await f.text();const candidate=f.name.toLowerCase().endsWith('.csv')?E.parseCsv(text):JSON.parse(text);const rows=E.parseRows(candidate);const inspection=E.audit(rows);if(inspection.duplicates)throw Error('Duplicate row identifiers are not allowed.');state.rows=rows;state.manifest={source:'User-provided local snapshot',recordCount:rows.length,importedFile:f.name,limitations:'User-supplied records are unverified; this is local-only evaluation.'};state.source='Imported '+f.name;state.view='overview';render();notify('Imported '+rows.length+' rows. Provenance is user supplied; results are not independently verified.');}catch(e){notify('Import failed: '+e.message,'error');}}
async function restoreBundled(){try{const [d,m]=await Promise.all([fetch('evaluation_snapshots.json').then(x=>{if(!x.ok)throw Error('Missing data file');return x.json();}),fetch('benchmark_manifest.json').then(x=>{if(!x.ok)throw Error('Missing manifest');return x.json();})]);state.rows=E.parseRows(d);state.manifest=m;state.source='Bundled synthetic-curated evaluation artifacts';state.view='overview';render();notify('Bundled benchmark restored.');}catch(e){notify('Could not restore: '+e.message,'error');}}
function bindView(){const q=(id,ev,fn)=>{const x=$(id);if(x)x.addEventListener(ev,fn);};q('sliceBy','change',e=>{state.segment=e.target.value;render();});q('errorBy','change',e=>{state.errorFilter=e.target.value;render();});q('compareMethod','change',e=>{state.candidate=e.target.value;render();});q('applyGates','click',()=>{const int=(id)=>Number($(id).value),th={minChallengeAccuracy:int('tAccuracy')/100,maxGeneralizationGap:int('tGap')/100,maxLengthPSI:int('tPSI'),minSliceAccuracy:int('tSlice')/100,minChallengeN:int('tCount'),minSliceN:int('tSliceN')};if(Object.entries(th).some(([k,v])=>!Number.isFinite(v)||v<0||((k==='minChallengeAccuracy'||k==='maxGeneralizationGap'||k==='minSliceAccuracy')&&v>1)||((k==='minChallengeN'||k==='minSliceN')&&(!Number.isInteger(v)||v<1))))return notify('Use valid positive thresholds (percent values from 0 to 100).','error');state.threshold=th;render();notify('Thresholds updated. Measurements were not changed.');});q('resetGates','click',()=>{state.threshold={minChallengeN:50,minChallengeAccuracy:.75,maxGeneralizationGap:.22,minSliceN:10,minSliceAccuracy:.58,maxLengthPSI:.5};render();});q('importBtn','click',importLocal);q('restoreBtn','click',restoreBundled);document.querySelectorAll('[data-download]').forEach(x=>x.addEventListener('click',()=>performDownload(x.dataset.download)));}
function bindGlobal(){document.querySelectorAll('[data-view]').forEach(btn=>btn.addEventListener('click',()=>{state.view=btn.dataset.view;render();document.getElementById('panel').scrollIntoView({behavior:'smooth',block:'start'});}));$('methodSelect').addEventListener('change',e=>{state.model=e.target.value;if(state.candidate===state.model)state.candidate=E.METHODS.find(x=>x!==state.model);render();});$('splitSelect').addEventListener('change',e=>{state.split=e.target.value;render();});$('reportBtn').addEventListener('click',()=>performDownload('report'));}
async function init(){bindGlobal();await restoreBundled();clearNotice();}
init().catch(e=>{console.error(e);$('panel').innerHTML=`
Could not load the evaluation artifacts: ${esc(e.message)}. Check that evaluation_snapshots.json and benchmark_manifest.json are in the Space root.