/* ModelGuard Observatory: auditable snapshot comparison engine. MIT. No network or dependencies. */ (function(root,factory){const api=factory();if(typeof module==='object'&&module.exports)module.exports=api;root.ModelGuard=api;})(typeof globalThis!=='undefined'?globalThis:this,function(){'use strict'; const METHODS=['nb','word','char','hybrid']; const LABELS=['background','objective','method','result','limitation','implication']; const SPLITS=['iid','challenge']; const ratio=(a,b)=>b?a/b:null; const round=(x,n=3)=>x==null?null:Number(x.toFixed(n)); const safe=(x)=>String(x??'').trim(); function parseRows(input){let rows=Array.isArray(input)?input:input&&input.rows;if(!Array.isArray(rows)||rows.length===0||rows.length>30000)throw Error('Expected an array of 1–30,000 evaluation rows.');const clean=[];const keys=new Set();for(let i=0;i150||language.length>30||domain.length>70)throw Error('Invalid model, split, label or row id at row '+(i+1)+'.');const key=[split,method,id].join('|');if(keys.has(key))throw Error('Duplicate split/method/id at row '+(i+1));keys.add(key);const tc=r.tokenCount===undefined||r.tokenCount===''?null:Number(r.tokenCount);if(tc!==null&&(!Number.isFinite(tc)||tc<0||tc>100000))throw Error('Invalid tokenCount at row '+(i+1));clean.push({split,method,id,language,domain,truth,pred,tokenCount:tc,chars:r.chars===undefined?null:Number(r.chars),text:safe(r.text).slice(0,800)});}return clean;} function select(rows,method,split){return rows.filter(r=>r.method===method&&r.split===split);} function matrix(rows){const m=Object.fromEntries(LABELS.map(a=>[a,Object.fromEntries(LABELS.map(b=>[b,0]))]));for(const r of rows)m[r.truth][r.pred]++;return m;} function metrics(rows){if(!rows.length)return {n:0,accuracy:null,macroF1:null,perLabel:[],confusion:matrix([]),errors:0};const conf=matrix(rows),n=rows.length,correct=rows.filter(r=>r.truth===r.pred).length;const perLabel=LABELS.map(l=>{let tp=conf[l][l],fp=LABELS.reduce((s,a)=>s+conf[a][l],0)-tp,fn=LABELS.reduce((s,b)=>s+conf[l][b],0)-tp;const precision=ratio(tp,tp+fp)??0,recall=ratio(tp,tp+fn)??0,f1=ratio(2*precision*recall,precision+recall)??0;return {label:l,support:tp+fn,precision,recall,f1};});const supported=perLabel.filter(r=>r.support>0);return {n,correct,errors:n-correct,accuracy:correct/n,macroF1:supported.length?supported.reduce((s,x)=>s+x.f1,0)/supported.length:null,perLabel,confusion:conf};} function segments(rows,key){if(!['language','domain','truth'].includes(key))throw Error('Invalid segment key');const groups=new Map();for(const r of rows){let k=r[key]||'unspecified';if(!groups.has(k))groups.set(k,[]);groups.get(k).push(r);}return [...groups].map(([name,items])=>({name,...metrics(items)})).sort((a,b)=>a.name.localeCompare(b.name));} function histogram(rows,key,bins){const out=Array(bins.length-1).fill(0);for(const r of rows){let x=r[key];if(typeof x!=='number'||!Number.isFinite(x))continue;for(let i=0;i=bins[i]&&(i===out.length-1||x[u,0]));for(const r of rows){if(Object.hasOwn(h,r[key]))h[r[key]]++;}return universe.map(x=>h[x]);} function prob(counts){const z=counts.reduce((a,b)=>a+b,0);return z?counts.map(v=>v/z):counts.map(()=>0);} function jsd(a,b){if(a.length!==b.length||a.length===0)throw Error('Mismatched distributions');const p=prob(a),q=prob(b),m=p.map((v,i)=>(v+q[i])/2);const kl=(x,y)=>x.reduce((s,v,i)=>s+(v>0?v*Math.log2(v/y[i]):0),0);return (kl(p,m)+kl(q,m))/2;} function psi(a,b){if(a.length!==b.length||a.length===0)throw Error('Mismatched distributions');const p=prob(a),q=prob(b),eps=1e-4;return p.reduce((s,v,i)=>{const x=Math.max(v,eps),y=Math.max(q[i],eps);return s+(y-x)*Math.log(y/x)},0);} function drift(base,candidate){const languages=[...new Set([...base,...candidate].map(r=>r.language))].sort();if(!languages.length)languages.push('und');const labels=LABELS, bins=[0,5,10,15,20,30,50,100001];return {languageJSD:jsd(distribution(base,'language',languages),distribution(candidate,'language',languages)),truthJSD:jsd(distribution(base,'truth',labels),distribution(candidate,'truth',labels)),lengthPSI:psi(histogram(base,'tokenCount',bins),histogram(candidate,'tokenCount',bins)),languageCounts:languages.map((name,i)=>({name,baseline:distribution(base,'language',languages)[i],candidate:distribution(candidate,'language',languages)[i]})),lengthBins:bins,baselineLength:histogram(base,'tokenCount',bins),candidateLength:histogram(candidate,'tokenCount',bins)};} function compare(rows,method){const a=select(rows,method,'iid'),b=select(rows,method,'challenge'),am=metrics(a),bm=metrics(b);return {method,iid:am,challenge:bm,gap:am.accuracy!=null&&bm.accuracy!=null?am.accuracy-bm.accuracy:null,drift:drift(a,b)};} function seeded(seed=14159){let x=(Number(seed)||1)>>>0;return()=>{x+=0x6D2B79F5;let y=x;y=Math.imul(y^(y>>>15),y|1);y^=y+Math.imul(y^(y>>>7),y|61);return ((y^(y>>>14))>>>0)/4294967296;};} function ciAccuracy(rows,iterations=500,seed=14159){if(!rows.length)throw Error('Empty observations');if(iterations<20||iterations>5000)throw Error('Invalid iterations');const rnd=seeded(seed),values=[];for(let it=0;ita-b);return {low:values[Math.floor((values.length-1)*.025)],high:values[Math.ceil((values.length-1)*.975)],iterations,seed,method:'naive nonparametric bootstrap, descriptive only'};} function pairedComparison(rows,methodA,methodB,split='challenge',iterations=500,seed=102){const a=select(rows,methodA,split),b=new Map(select(rows,methodB,split).map(r=>[r.id,r]));const paired=a.filter(r=>b.has(r.id)).map(r=>({a:Number(r.pred===r.truth),b:Number(b.get(r.id).pred===b.get(r.id).truth)}));if(!paired.length)throw Error('No paired examples');let bWins=0,aWins=0;for(const p of paired){if(!p.a&&p.b)bWins++;if(p.a&&!p.b)aWins++;}let delta=paired.reduce((s,p)=>s+p.b-p.a,0)/paired.length;const rnd=seeded(seed),vals=[];for(let i=0;ix-y);return {n:paired.length,delta,bWins,aWins,ciLow:vals[Math.floor((iterations-1)*.025)],ciHigh:vals[Math.ceil((iterations-1)*.975)],warning:'Paired bootstrap is descriptive on a small synthetic curated set; not a population confidence guarantee.'};} function gate(rows,method,config={}){const cfg={minChallengeN:50,minChallengeAccuracy:.75,maxGeneralizationGap:.22,minSliceN:10,minSliceAccuracy:.58,maxLengthPSI:.5,...config};const c=compare(rows,method);const slices=segments(select(rows,method,'challenge'),'language');const checks=[{label:'Challenge dataset size',pass:c.challenge.n>=cfg.minChallengeN,detail:`${c.challenge.n} vs minimum ${cfg.minChallengeN}`},{label:'Challenge accuracy',pass:c.challenge.accuracy!==null&&c.challenge.accuracy>=cfg.minChallengeAccuracy,detail:`${round(c.challenge.accuracy*100,1)}% vs minimum ${cfg.minChallengeAccuracy*100}%`},{label:'Generalization gap',pass:c.gap!==null&&c.gap<=cfg.maxGeneralizationGap,detail:`${round(c.gap*100,1)} pp vs maximum ${cfg.maxGeneralizationGap*100} pp`},{label:'Minimum language slice',pass:slices.every(s=>s.n>=cfg.minSliceN),detail:slices.map(s=>`${s.name}: ${s.n}`).join(' · ')},{label:'Minimum language accuracy',pass:slices.every(s=>s.accuracy>=cfg.minSliceAccuracy),detail:slices.map(s=>`${s.name}: ${round(s.accuracy*100,1)}%`).join(' · ')},{label:'Text-length distribution PSI',pass:c.drift.lengthPSI<=cfg.maxLengthPSI,detail:`${round(c.drift.lengthPSI,3)} vs maximum ${cfg.maxLengthPSI}`}];return {method,pass:checks.every(x=>x.pass),checks,config:cfg,notice:'Exploratory release checklist, NOT a production safety approval or certification.'};} function audit(rows){const duplicate=[];let nMissingTokens=0;const sets={iid:new Set(),challenge:new Set()};const ids=new Set();for(const r of rows){const k=r.split+'|'+r.method+'|'+r.id;if(ids.has(k))duplicate.push(k);ids.add(k);if(r.tokenCount===null)nMissingTokens++;sets[r.split].add(r.id);}const overlap=[...sets.iid].filter(id=>sets.challenge.has(id));const incomplete=[];for(const split of SPLITS){const byMethod=METHODS.map(method=>new Set(select(rows,method,split).map(r=>r.id)));for(let i=1;i!byMethod[i].has(id)))incomplete.push(split+': '+METHODS[i]+' mismatched examples');}return {total:rows.length,duplicates:duplicate.length,duplicateSamples:duplicate.slice(0,10),splitIdOverlap:overlap.length,missingTokenCounts:nMissingTokens,incomplete,clean:duplicate.length===0&&overlap.length===0&&incomplete.length===0};} function cell(value){const s=String(value??'');const safe=/^[=+@\-\t\r]/.test(s)?"'"+s:s;return /[",\n\r]/.test(safe)?'"'+safe.replace(/"/g,'""')+'"':safe;} function csv(rows,headers){return [headers.join(','),...rows.map(r=>headers.map(h=>cell(r[h])).join(','))].join('\r\n');} function parseCsv(text){const lines=[];let row=[],buffer='',quote=false;for(let i=0;ix.trim().replace(/^\uFEFF/,''));if(new Set(h).size!==h.length)throw Error('Duplicate column names');return lines.filter(x=>x.some(z=>z.trim())).map(line=>Object.fromEntries(h.map((key,i)=>[key,line[i]??''])));} return {LABELS,METHODS,SPLITS,parseRows,select,matrix,metrics,segments,distribution,jsd,psi,drift,compare,seeded,ciAccuracy,pairedComparison,gate,audit,csv,parseCsv,round}; });