/* 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=>`
${arr.map(x=>`
${esc(x[0])}${esc(x[1])}${esc(x[2]||'')}
`).join('')}
`; const card=(title,content,desc='',cls='',kicker='RESEARCH NOTE')=>`
${esc(kicker)}

${esc(title)}

${desc?`

${esc(desc)}

`:''}${content}
`; const bar=(name,val,display,cls='')=>`
${esc(name)}
${esc(display||fmt(val))}
`; const table=(head,rows)=>`
${head.map(h=>``).join('')}${rows.join('')}
${esc(h)}
`; 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',`
Selected model${esc(R.METHOD_NAMES[state.method])}
Evaluation split${state.split==='challenge'?'Novel-pattern challenge':'Controlled IID'}
Stress condition${esc(attackInfo(state.attack).name)}
Changed / attempted${a.changed} / ${a.n}
Matched baseline accuracy${a.scored?fmt(a.baselineMatchedAccuracy):'Not valid'}
Transformed subset accuracy${a.scored?fmt(a.accuracy):'Not valid'}
${ci?`
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 `${esc(a.name)}
${esc(a.level)}${s.changed}/${s.n}${fmt(s.changedShare)}${fmt(s.flipRate)}${s.scored?fmt(s.accuracy):pill('Not valid','amber')}${s.scored?signed(s.accuracyDelta):'N/A'}`;});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)+`
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=>`
${esc(a.name)}${pill(a.level,a.invariant?'neutral':'amber')}

${esc(a.meaning)}

`).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.

${rows.slice(0,12).map(r=>`
${esc(r.id)} · ${esc(r.language.toUpperCase())} · ${esc(r.domain)}${r.pred===r.originalPred?pill('stable'):pill('FLIPPED','danger')}

ORIGINAL

${highlightText(r.originalText)}
${esc(r.originalPred)}

MODIFIED · ${esc(r.attack)}

${highlightText(r.text)}
${esc(r.pred)}

Inherited gold label: ${esc(r.truth)} · Score gap: ${r.margin.toFixed(4)} · ${r.changed?'Text changed':'No substitution matched'}

`).join('')||'

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 `${[0,.25,.5,.75,1].map(t=>`${Math.round(t*100)}%`).join('')}${[.25,.5,.75,1].map(t=>`${Math.round(t*100)}%`).join('')}Prediction coverage (top score margins retained)`;} 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')+`
${card('Coverage checkpoints',table(['Retained share','Kept cases','Observed error'],[.25,.50,.75,1].map(c=>{const x=R.coverageAt(all,c);return `${fmt(c,0)}${x.n}${fmt(x.risk)}`;})),'Same held-out synthetic split; small sample sizes matter.','', 'DIAGNOSTIC TABLE')}${card('What this tells us',`

If the model is often wrong even among high-margin predictions, margin-based rejection will not reliably isolate correct decisions.

Scoring methodTop-two difference
Selection mechanismWithin-method ranking
Validated calibrationNo
External test setNot included
`,'AURC is a descriptive statistic on a fixed evaluation sample.','', 'INTERPRETATION')}
`;} function renderReliability(){const row=current('original','nb'),bins=R.reliability(row,8),ece=R.ece(row,8),lines=bins.map(b=>`${fmt(b.low,0)}–${fmt(b.high,0)}${b.n}${fmt(b.meanConfidence)}${fmt(b.empiricalAccuracy)}${b.n?fmt(Math.abs(b.meanConfidence-b.empiricalAccuracy)):'N/A'}`);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=>`
${pill(c.pass?'PASS':'FAIL',c.pass?'':'danger')}  ${esc(c.rule)}${typeof c.actual==='number'?(c.rule.includes('rate')||c.rule.includes('drop')?fmt(c.actual):c.actual):esc(c.actual)}
`).join('')+`
Even if all checks pass, that is not proof of robustness, fairness, compliance, safety, or production readiness.
`,'Rules cover perturbation flip rates and matched-subset changes.','', 'OBSERVABLE VERDICTS')}
`;} function renderSandbox(){const x=state.sandboxResult;return `
${card('Run your own statement',`
${x?`
ORIGINAL

${esc(x.baseText)}

${pill(x.before.label)}
TRANSFORMED

${esc(x.modified)}

${pill(x.after.label,x.before.label===x.after.label?'':'danger')}  ${x.before.label!==x.after.label?pill('Prediction changed','danger'):pill('Prediction stable')}

No original gold label is available for custom text. This tool does not calculate accuracy for personal examples.

`:''}`,'Text remains inside the browser. No network inference.','', 'LOCAL COUNTERFACTUAL')}${card('Data downloads and provenance',`

Reproduce every reported number, inspect documentation, and cite the source data license separately.

Prediction records${num(state.records.length)}
ProvenanceSHA-256 manifest
Code licenseMIT
Dataset licenseCC BY 4.0
Source examples are synthetic-curated. Publication does not turn these tests into validated real-world performance measurements.
`,'Downloads are generated locally, without uploads to a server.','', 'REPRODUCIBILITY')}
`+card('Integrity registry',table(['Asset','SHA-256 digest'],Object.entries(state.manifest?.files||{}).map(([name,hash])=>`${esc(name)}${esc(hash.slice(0,24))}…`)),'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(); })();