Model_Drift / index.html
eaglelandsonce's picture
Update index.html
db615a8 verified
Raw
History Blame Contribute Delete
27.5 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>DriftWatch — LLM Model Drift &amp; MLOps Simulation</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Archivo:wght@500;600;700;800&family=IBM+Plex+Mono:wght@400;500;600&display=swap" rel="stylesheet">
<style>
:root{
--bg:#0F1318; --panel:#171C24; --panel2:#1D242F; --line:#262E3A;
--text:#E8ECF1; --muted:#8B95A5; --faint:#5A6475;
--teal:#45D6B5; --amber:#F5A83C; --red:#F05D5D; --blue:#5B8DEF;
--radius:10px;
}
*{box-sizing:border-box; margin:0; padding:0;}
body{
background:var(--bg); color:var(--text);
font-family:'Archivo',system-ui,sans-serif;
padding:20px; min-height:100vh;
}
.mono{font-family:'IBM Plex Mono',monospace;}
/* ---------- header ---------- */
header{
display:flex; align-items:center; gap:18px; flex-wrap:wrap;
padding-bottom:16px; border-bottom:1px solid var(--line); margin-bottom:16px;
}
.brand h1{font-size:20px; font-weight:800; letter-spacing:.04em;}
.brand h1 span{color:var(--teal);}
.brand p{font-size:12px; color:var(--muted); margin-top:2px;}
.clock{
margin-left:auto; display:flex; align-items:center; gap:14px;
}
.week{font-family:'IBM Plex Mono',monospace; font-size:13px; color:var(--muted);}
.week b{color:var(--text); font-size:16px;}
button{
font-family:'Archivo',sans-serif; font-weight:600; font-size:13px;
background:var(--panel2); color:var(--text);
border:1px solid var(--line); border-radius:8px;
padding:8px 14px; cursor:pointer; transition:border-color .15s, background .15s;
}
button:hover{border-color:var(--muted);}
button:focus-visible{outline:2px solid var(--teal); outline-offset:2px;}
button.primary{background:var(--teal); color:#08110E; border-color:var(--teal);}
button.warn{border-color:var(--amber); color:var(--amber);}
button.warn:hover{background:rgba(245,168,60,.12);}
button:disabled{opacity:.4; cursor:not-allowed;}
.speed{display:flex; gap:4px;}
.speed button{padding:8px 10px;}
.speed button.on{border-color:var(--teal); color:var(--teal);}
/* ---------- layout ---------- */
.grid{display:grid; grid-template-columns: 1.6fr 1fr; gap:16px;}
.col{display:flex; flex-direction:column; gap:16px; min-width:0;}
.card{
background:var(--panel); border:1px solid var(--line);
border-radius:var(--radius); padding:16px;
}
.card h2{
font-size:11px; font-weight:700; letter-spacing:.14em; text-transform:uppercase;
color:var(--muted); margin-bottom:12px;
display:flex; align-items:center; gap:8px;
}
.card h2 .hint{margin-left:auto; font-weight:500; letter-spacing:0; text-transform:none; color:var(--faint); font-size:11px;}
canvas{width:100%; display:block;}
/* ---------- metric tiles ---------- */
.tiles{display:grid; grid-template-columns:repeat(2,1fr); gap:10px;}
.tile{background:var(--panel2); border:1px solid var(--line); border-radius:8px; padding:10px 12px;}
.tile .label{font-size:10px; letter-spacing:.12em; text-transform:uppercase; color:var(--muted);}
.tile .value{font-family:'IBM Plex Mono',monospace; font-size:22px; font-weight:600; margin-top:4px;}
.tile .sub{font-size:11px; color:var(--faint); margin-top:2px;}
.ok{color:var(--teal);} .warnc{color:var(--amber);} .bad{color:var(--red);} .bluec{color:var(--blue);}
/* ---------- controls ---------- */
.ctl{display:flex; flex-direction:column; gap:14px;}
.ctl-row{display:flex; align-items:center; gap:10px;}
.ctl-row label{font-size:12.5px; color:var(--muted); width:150px; flex-shrink:0;}
input[type=range]{flex:1; accent-color:var(--teal);}
.ctl-row .val{font-family:'IBM Plex Mono',monospace; font-size:12px; width:52px; text-align:right; color:var(--text);}
.toggle{display:flex; align-items:center; gap:10px; cursor:pointer; font-size:12.5px; color:var(--muted);}
.toggle input{accent-color:var(--teal); width:16px; height:16px;}
.btnrow{display:flex; gap:8px; flex-wrap:wrap;}
/* ---------- pipeline ---------- */
.pipe-wrap{display:flex; justify-content:center;}
.stage-note{font-size:12px; color:var(--muted); text-align:center; min-height:18px; margin-top:6px;}
/* ---------- feeds ---------- */
.feeds{display:grid; grid-template-columns:1.6fr 1fr; gap:16px; margin-top:16px;}
.feed{max-height:230px; overflow-y:auto; display:flex; flex-direction:column; gap:6px;}
.q{
display:flex; gap:10px; align-items:flex-start;
font-size:12.5px; padding:7px 10px; border-radius:6px;
background:var(--panel2); border:1px solid var(--line);
}
.q .mark{font-family:'IBM Plex Mono',monospace; font-weight:600; flex-shrink:0;}
.q .why{color:var(--red); font-size:11.5px; display:block; margin-top:2px;}
.log-line{font-family:'IBM Plex Mono',monospace; font-size:11.5px; color:var(--muted); padding:3px 0; border-bottom:1px dashed var(--line);}
.log-line b{color:var(--text); font-weight:500;}
.log-line.alert b{color:var(--amber);}
.log-line.deploy b{color:var(--blue);}
.log-line.shock b{color:var(--red);}
/* ---------- explainer ---------- */
.lesson{display:grid; grid-template-columns:repeat(3,1fr); gap:16px; margin-top:16px;}
.lesson .card p{font-size:12.5px; color:var(--muted); line-height:1.55;}
.lesson .card h3{font-size:13px; margin-bottom:6px;}
.lesson .card h3 span{margin-right:6px;}
/* legend */
.legend{display:flex; gap:16px; font-size:11px; color:var(--muted); margin-top:8px; flex-wrap:wrap;}
.legend i{display:inline-block; width:18px; height:3px; border-radius:2px; margin-right:6px; vertical-align:middle;}
@media (max-width: 980px){
.grid,.feeds,.lesson{grid-template-columns:1fr;}
}
@media (prefers-reduced-motion: reduce){
*{transition:none !important; animation:none !important;}
}
</style>
</head>
<body>
<header>
<div class="brand">
<h1>DRIFT<span>WATCH</span></h1>
<p>LLM production console — a model drift &amp; MLOps teaching simulation</p>
</div>
<div class="clock">
<span class="week">WEEK <b id="week">0</b></span>
<div class="speed" role="group" aria-label="Simulation speed">
<button data-speed="1" class="on"></button>
<button data-speed="2"></button>
<button data-speed="4"></button>
</div>
<button id="playBtn" class="primary">▶ Run</button>
<button id="resetBtn">Reset</button>
</div>
</header>
<div class="grid">
<!-- LEFT: charts -->
<div class="col">
<div class="card">
<h2>Live eval accuracy <span class="hint">weekly automated eval against fresh, human-labeled traffic</span></h2>
<canvas id="accChart" height="190"></canvas>
<div class="legend">
<span><i style="background:var(--teal)"></i>Eval accuracy</span>
<span><i style="background:var(--amber)"></i>Degraded (&lt;88%)</span>
<span><i style="background:var(--red)"></i>SLA breach (&lt;75%)</span>
<span><i style="background:var(--blue)"></i>Deployment</span>
</div>
</div>
<div class="card">
<h2>Input distribution: training snapshot vs. live traffic <span class="hint">the gap between the curves is the drift</span></h2>
<canvas id="distChart" height="180"></canvas>
<div class="legend">
<span><i style="background:var(--blue)"></i>What the model was trained on</span>
<span><i style="background:var(--teal)"></i>What users ask about now</span>
<span><i style="background:rgba(245,168,60,.6)"></i>Divergence (unfamiliar territory)</span>
</div>
</div>
</div>
<!-- RIGHT: metrics, pipeline, controls -->
<div class="col">
<div class="card">
<h2>Service health</h2>
<div class="tiles">
<div class="tile"><div class="label">Eval accuracy</div><div class="value" id="mAcc"></div><div class="sub" id="mSla"></div></div>
<div class="tile"><div class="label">Drift score (PSI)</div><div class="value" id="mPsi"></div><div class="sub" id="mPsiNote">threshold 0.25</div></div>
<div class="tile"><div class="label">Model age</div><div class="value" id="mAge"></div><div class="sub">weeks since deploy</div></div>
<div class="tile"><div class="label">Retrain spend</div><div class="value" id="mCost">$0</div><div class="sub" id="mRuns">0 pipeline runs</div></div>
</div>
</div>
<div class="card">
<h2>MLOps cycle</h2>
<div class="pipe-wrap">
<svg id="pipeSvg" viewBox="0 0 300 210" width="300" height="210" aria-label="MLOps cycle diagram"></svg>
</div>
<div class="stage-note" id="stageNote">Monitoring production…</div>
</div>
<div class="card">
<h2>Ops controls</h2>
<div class="ctl">
<div class="ctl-row">
<label for="driftRate">Rate of world change</label>
<input type="range" id="driftRate" min="0" max="0.10" step="0.005" value="0.03">
<span class="val" id="driftRateVal">0.030</span>
</div>
<div class="ctl-row">
<label for="psiThresh">Drift alert threshold</label>
<input type="range" id="psiThresh" min="0.10" max="0.60" step="0.05" value="0.25">
<span class="val" id="psiThreshVal">0.25</span>
</div>
<label class="toggle">
<input type="checkbox" id="autoRetrain">
Auto-retrain when drift alert fires (continuous training)
</label>
<div class="btnrow">
<button id="shockBtn" class="warn">⚡ Inject world event</button>
<button id="retrainBtn">⟳ Trigger retrain pipeline</button>
</div>
</div>
</div>
</div>
</div>
<div class="feeds">
<div class="card">
<h2>Live traffic sample <span class="hint">✓ answered correctly · ✗ stale answer</span></h2>
<div class="feed" id="queryFeed"></div>
</div>
<div class="card">
<h2>Ops event log</h2>
<div class="feed" id="eventLog"></div>
</div>
</div>
<div class="lesson">
<div class="card">
<h3><span></span>Why accuracy decays</h3>
<p>The model's weights are frozen at deployment, but the world keeps moving: new products, new APIs, new slang, new regulations. The blue curve (training data) stays put while the teal curve (live traffic) slides away. The bigger the gap, the more often the model answers from a world that no longer exists.</p>
</div>
<div class="card">
<h3><span></span>Why we monitor drift, not just accuracy</h3>
<p>Accuracy needs labeled data, which is slow and expensive to collect. The drift score (PSI) compares input distributions only — no labels needed — so it acts as an early-warning smoke detector. Watch how the PSI alert usually fires <em>before</em> accuracy visibly craters.</p>
</div>
<div class="card">
<h3><span></span>Why retraining never fully catches up</h3>
<p>Notice that a fresh deployment starts slightly behind: the pipeline trains on data collected when it <em>started</em>, and the world moved during training. That's the MLOps treadmill — monitor → collect → train → evaluate → deploy → monitor — and why teams automate it instead of retraining by hand.</p>
</div>
</div>
<script>
"use strict";
/* =====================================================================
DriftWatch — a teaching simulation of LLM model drift + the MLOps cycle
---------------------------------------------------------------------
Mental model:
- The "world" is a 1-D topic distribution: a gaussian centered at muWorld.
- The model's training data is a frozen gaussian centered at muTrain.
- Each simulated week, muWorld drifts (gradual drift) and can jump
(sudden drift via "world events").
- Accuracy is a function of the distance between the two centers.
- PSI (Population Stability Index) is computed between the two
distributions — the label-free drift metric ops teams actually use.
- The retrain pipeline takes several weeks: it snapshots the world at
collection time, so even a fresh model deploys slightly stale.
===================================================================== */
// ----------------------------- state ---------------------------------
const SIGMA = 1.0;
const COST_PER_RUN = 2400; // $ per pipeline run (compute + labeling)
const STAGES = ["Monitor","Collect","Train","Evaluate","Deploy"];
const STAGE_WEEKS = {Collect:2, Train:3, Evaluate:1};
let S; // simulation state
function freshState(){
return {
week:0, muWorld:0, muTrain:0, lastDeployWeek:0,
accHist:[], psiHist:[], deployWeeks:[0],
pipeline:null, // {stage, weeksLeft, snapshotMu}
runs:0, cost:0,
alertLatched:false, // avoid re-firing alert every week while above threshold
running:false, speed:1, timer:null,
};
}
// --------------------------- math helpers ----------------------------
function gauss(x, mu){ return Math.exp(-0.5*((x-mu)/SIGMA)**2) / (SIGMA*Math.sqrt(2*Math.PI)); }
// PSI over binned gaussians — same computation ops teams run on real features
function computePSI(muA, muB){
const lo = Math.min(muA,muB)-4, hi = Math.max(muA,muB)+4, BINS=20;
let psi=0;
for(let i=0;i<BINS;i++){
const x0=lo+(hi-lo)*i/BINS, x1=lo+(hi-lo)*(i+1)/BINS, xm=(x0+x1)/2, w=x1-x0;
const a=Math.max(gauss(xm,muA)*w,1e-6), b=Math.max(gauss(xm,muB)*w,1e-6);
psi += (a-b)*Math.log(a/b);
}
return psi;
}
function accuracy(){
const d = Math.abs(S.muWorld - S.muTrain);
const base = 0.55 + 0.42*Math.exp(-0.55*d*d); // 97% fresh → 55% floor
return Math.min(0.99, Math.max(0.5, base + (Math.random()-0.5)*0.016));
}
// --------------------------- content pools ----------------------------
const SHOCK_EVENTS = [
"Framework releases breaking v4 API",
"New data-privacy regulation takes effect",
"Competitor launches rival product — support traffic shifts",
"Company rebrands product line & pricing tiers",
"Viral trend floods traffic with new terminology",
];
const EVERGREEN_Q = [
"Summarize this incident report for the exec channel",
"Write a SQL query for weekly active users",
"Explain the difference between L1 and L2 regularization",
"Draft a polite follow-up email to a client",
"Convert this JSON payload to a CSV",
];
const DRIFTY_Q = [
["How do I migrate to the new v4 SDK?", "recommends the deprecated v2 API"],
["Does the new privacy addendum apply to our EU users?", "cites the superseded regulation text"],
["What's included in the Pro tier now?", "quotes last year's pricing"],
["What does everyone mean by the new acronym in tickets?", "unfamiliar with post-training terminology"],
["Compare us against the competitor's new offering", "unaware the product exists"],
["Why does the build fail after the framework update?", "suggests config removed in v4"],
];
// ------------------------------ DOM ------------------------------------
const $ = id => document.getElementById(id);
const accCanvas=$("accChart"), distCanvas=$("distChart");
function fitCanvas(c){
const dpr = window.devicePixelRatio||1, w=c.clientWidth, h=parseInt(c.getAttribute("height"));
c.width=w*dpr; c.height=h*dpr; c.style.height=h+"px";
const ctx=c.getContext("2d"); ctx.setTransform(dpr,0,0,dpr,0,0);
return ctx;
}
// --------------------------- pipeline SVG ------------------------------
function buildPipeSvg(){
const svg=$("pipeSvg"); svg.innerHTML="";
const cx=150, cy=105, r=72;
const pts = STAGES.map((_,i)=>{
const a = -Math.PI/2 + i*2*Math.PI/STAGES.length;
return [cx+r*Math.cos(a), cy+r*Math.sin(a)];
});
const ns="http://www.w3.org/2000/svg";
// arrows between stages
for(let i=0;i<pts.length;i++){
const [x1,y1]=pts[i], [x2,y2]=pts[(i+1)%pts.length];
const mx=(x1+x2)/2 + (cx-(x1+x2)/2)*-0.18, my=(y1+y2)/2 + (cy-(y1+y2)/2)*-0.18;
const p=document.createElementNS(ns,"path");
p.setAttribute("d",`M ${x1} ${y1} Q ${mx} ${my} ${x2} ${y2}`);
p.setAttribute("fill","none"); p.setAttribute("stroke","#38424F");
p.setAttribute("stroke-width","1.5"); p.setAttribute("stroke-dasharray","4 4");
svg.appendChild(p);
}
// nodes
STAGES.forEach((name,i)=>{
const [x,y]=pts[i];
const g=document.createElementNS(ns,"g"); g.setAttribute("id","node-"+name);
const c=document.createElementNS(ns,"circle");
c.setAttribute("cx",x); c.setAttribute("cy",y); c.setAttribute("r",21);
c.setAttribute("fill","#1D242F"); c.setAttribute("stroke","#38424F"); c.setAttribute("stroke-width","2");
const t=document.createElementNS(ns,"text");
t.setAttribute("x",x); t.setAttribute("y",y+3.5);
t.setAttribute("text-anchor","middle");
t.setAttribute("font-size","8.5"); t.setAttribute("font-weight","700");
t.setAttribute("letter-spacing","0.5"); t.setAttribute("fill","#8B95A5");
t.setAttribute("font-family","Archivo,sans-serif");
t.textContent=name.toUpperCase();
g.appendChild(c); g.appendChild(t); svg.appendChild(g);
});
}
function highlightStage(active){
STAGES.forEach(name=>{
const g=$("node-"+name), c=g.querySelector("circle"), t=g.querySelector("text");
const on = name===active;
c.setAttribute("stroke", on ? (name==="Monitor"?"#45D6B5":"#F5A83C") : "#38424F");
c.setAttribute("fill", on ? "#242D3A" : "#1D242F");
t.setAttribute("fill", on ? "#E8ECF1" : "#8B95A5");
});
}
// ------------------------------ logging --------------------------------
function log(msg, cls=""){
const el=document.createElement("div");
el.className="log-line "+cls;
el.innerHTML=`W${String(S.week).padStart(3,"0")} <b>${msg}</b>`;
const box=$("eventLog"); box.prepend(el);
while(box.children.length>40) box.removeChild(box.lastChild);
}
function pushQuery(){
const d=Math.abs(S.muWorld-S.muTrain);
const pDrifty=Math.min(0.85, d*0.45); // stale topics dominate as drift grows
const drifty=Math.random()<pDrifty;
const acc=S.accHist.length?S.accHist[S.accHist.length-1]:0.97;
let text, ok, why="";
if(drifty){
const [q,w]=DRIFTY_Q[Math.floor(Math.random()*DRIFTY_Q.length)];
text=q; ok=Math.random()<Math.max(0.05, acc-0.25); why=w;
}else{
text=EVERGREEN_Q[Math.floor(Math.random()*EVERGREEN_Q.length)];
ok=Math.random()<Math.min(0.98, acc+0.06);
}
const el=document.createElement("div"); el.className="q";
el.innerHTML=`<span class="mark ${ok?'ok':'bad'}">${ok?'✓':'✗'}</span>
<span>“${text}${ok?'':`<span class="why">↳ ${why}</span>`}</span>`;
const box=$("queryFeed"); box.prepend(el);
while(box.children.length>30) box.removeChild(box.lastChild);
}
// ------------------------------ charts ---------------------------------
function drawAccChart(){
const ctx=fitCanvas(accCanvas), W=accCanvas.clientWidth, H=parseInt(accCanvas.getAttribute("height"));
ctx.clearRect(0,0,W,H);
const padL=36, padR=8, padT=8, padB=18;
const yMin=0.5, yMax=1.0;
const y=v=>padT+(H-padT-padB)*(1-(v-yMin)/(yMax-yMin));
// threshold bands
ctx.fillStyle="rgba(245,168,60,.06)"; ctx.fillRect(padL,y(0.88),W-padL-padR,y(0.75)-y(0.88));
ctx.fillStyle="rgba(240,93,93,.08)"; ctx.fillRect(padL,y(0.75),W-padL-padR,y(yMin)-y(0.75));
// gridlines + labels
ctx.font="10px 'IBM Plex Mono',monospace"; ctx.fillStyle="#5A6475";
[0.6,0.7,0.8,0.9,1.0].forEach(v=>{
ctx.strokeStyle="#232B36"; ctx.beginPath(); ctx.moveTo(padL,y(v)); ctx.lineTo(W-padR,y(v)); ctx.stroke();
ctx.fillText(Math.round(v*100)+"%",4,y(v)+3);
});
const hist=S.accHist; if(!hist.length) return;
const view=80, start=Math.max(0,hist.length-view), n=hist.length-start;
const x=i=>padL+(W-padL-padR)*(n<=1?1:(i/(Math.max(view-1,n-1))));
// deploy markers
ctx.strokeStyle="#5B8DEF"; ctx.setLineDash([4,4]);
S.deployWeeks.forEach(w=>{
const i=w-1-start; if(i<0||i>=n) return;
ctx.beginPath(); ctx.moveTo(x(i),padT); ctx.lineTo(x(i),H-padB); ctx.stroke();
});
ctx.setLineDash([]);
// accuracy line
ctx.strokeStyle="#45D6B5"; ctx.lineWidth=2; ctx.beginPath();
for(let i=0;i<n;i++){ const v=hist[start+i]; i?ctx.lineTo(x(i),y(v)):ctx.moveTo(x(i),y(v)); }
ctx.stroke();
// current point
const last=hist[hist.length-1];
ctx.fillStyle= last<0.75?"#F05D5D": last<0.88?"#F5A83C":"#45D6B5";
ctx.beginPath(); ctx.arc(x(n-1),y(last),3.5,0,7); ctx.fill();
}
function drawDistChart(){
const ctx=fitCanvas(distCanvas), W=distCanvas.clientWidth, H=parseInt(distCanvas.getAttribute("height"));
ctx.clearRect(0,0,W,H);
const padB=22, top=12;
// window follows midpoint of the two distributions
const mid=(S.muWorld+S.muTrain)/2, span=Math.max(6, Math.abs(S.muWorld-S.muTrain)+5);
const lo=mid-span/2, hi=mid+span/2;
const X=v=>(v-lo)/(hi-lo)*W;
const peak=gauss(0,0);
const Y=p=>H-padB-(H-padB-top)*(p/peak);
const N=160, step=(hi-lo)/N;
// divergence fill: where live exceeds training (unfamiliar territory)
ctx.beginPath(); let started=false;
for(let i=0;i<=N;i++){
const xv=lo+i*step, a=gauss(xv,S.muTrain), b=gauss(xv,S.muWorld);
const yTop=Y(Math.max(a,b)), yBot=Y(a);
if(b>a){
if(!started){ ctx.moveTo(X(xv),yBot); started=true; }
ctx.lineTo(X(xv),yTop);
}
}
if(started){
for(let i=N;i>=0;i--){
const xv=lo+i*step, a=gauss(xv,S.muTrain), b=gauss(xv,S.muWorld);
if(b>a) ctx.lineTo(X(xv),Y(a));
}
ctx.closePath(); ctx.fillStyle="rgba(245,168,60,.22)"; ctx.fill();
}
// curves
const curve=(mu,color)=>{
ctx.strokeStyle=color; ctx.lineWidth=2; ctx.beginPath();
for(let i=0;i<=N;i++){ const xv=lo+i*step, p=gauss(xv,mu);
i?ctx.lineTo(X(xv),Y(p)):ctx.moveTo(X(xv),Y(p)); }
ctx.stroke();
};
curve(S.muTrain,"#5B8DEF");
curve(S.muWorld,"#45D6B5");
// baseline + axis label
ctx.strokeStyle="#262E3A"; ctx.beginPath(); ctx.moveTo(0,H-padB); ctx.lineTo(W,H-padB); ctx.stroke();
ctx.font="10px 'IBM Plex Mono',monospace"; ctx.fillStyle="#5A6475";
ctx.fillText("← older topics · topic space · newer topics →", W/2-110, H-7);
}
// ------------------------------ metrics --------------------------------
function renderMetrics(psi, acc){
const pct=(acc*100).toFixed(1)+"%";
const accEl=$("mAcc"); accEl.textContent=pct;
accEl.className="value "+(acc<0.75?"bad":acc<0.88?"warnc":"ok");
$("mSla").textContent = acc<0.75?"SLA BREACH":acc<0.88?"degraded":"healthy";
const psiEl=$("mPsi"); psiEl.textContent=psi.toFixed(2);
const th=parseFloat($("psiThresh").value);
psiEl.className="value "+(psi>=th?"warnc":"ok");
$("mPsiNote").textContent="threshold "+th.toFixed(2);
$("mAge").textContent=S.week-S.lastDeployWeek;
$("mCost").textContent="$"+S.cost.toLocaleString();
$("mRuns").textContent=S.runs+" pipeline run"+(S.runs===1?"":"s");
}
// ------------------------------ pipeline -------------------------------
function startPipeline(reason){
if(S.pipeline) return;
S.pipeline={stage:"Collect", weeksLeft:STAGE_WEEKS.Collect, snapshotMu:S.muWorld};
S.runs++; S.cost+=COST_PER_RUN;
log(`Retrain pipeline started (${reason}) — collecting fresh data`);
highlightStage("Collect");
$("stageNote").textContent="Collecting & labeling fresh production data…";
$("retrainBtn").disabled=true;
}
function stepPipeline(){
const p=S.pipeline; if(!p) return;
p.weeksLeft--;
if(p.weeksLeft>0) return;
if(p.stage==="Collect"){ p.stage="Train"; p.weeksLeft=STAGE_WEEKS.Train;
log("Dataset frozen — fine-tuning candidate model");
$("stageNote").textContent="Training on the collected snapshot…";
} else if(p.stage==="Train"){ p.stage="Evaluate"; p.weeksLeft=STAGE_WEEKS.Evaluate;
log("Training complete — running eval suite & safety gates");
$("stageNote").textContent="Evaluating candidate against golden set…";
} else if(p.stage==="Evaluate"){
// Deploy: model knows the world AS OF collection time — already stale!
S.muTrain=p.snapshotMu; S.lastDeployWeek=S.week; S.deployWeeks.push(S.week);
S.pipeline=null; S.alertLatched=false;
log("Candidate passed — deployed to production ✦ (trained on a snapshot that is already "+(STAGE_WEEKS.Train+STAGE_WEEKS.Evaluate)+" weeks old)","deploy");
$("stageNote").textContent="Monitoring production…";
$("retrainBtn").disabled=false;
highlightStage("Monitor");
return;
}
highlightStage(p.stage);
}
// ------------------------------ tick -----------------------------------
function tick(){
S.week++;
// gradual drift, with a little randomness so runs differ
const rate=parseFloat($("driftRate").value);
S.muWorld += rate*(0.6+0.8*Math.random());
stepPipeline();
const acc=accuracy(), psi=computePSI(S.muTrain,S.muWorld);
S.accHist.push(acc); S.psiHist.push(psi);
if(S.accHist.length>200){S.accHist.shift(); S.psiHist.shift();}
// drift alerting (label-free early warning)
const th=parseFloat($("psiThresh").value);
if(psi>=th && !S.alertLatched){
S.alertLatched=true;
log(`DRIFT ALERT — PSI ${psi.toFixed(2)}${th.toFixed(2)}`,"alert");
if($("autoRetrain").checked) startPipeline("auto-trigger");
}
if(psi<th*0.8) S.alertLatched=false;
$("week").textContent=S.week;
renderMetrics(psi,acc);
drawAccChart(); drawDistChart();
pushQuery(); if(Math.random()<0.5) pushQuery();
}
// ------------------------------ controls -------------------------------
function setRunning(run){
S.running=run;
$("playBtn").textContent=run?"❚❚ Pause":"▶ Run";
$("playBtn").classList.toggle("primary",!run);
clearInterval(S.timer);
if(run) S.timer=setInterval(tick, 900/S.speed);
}
$("playBtn").addEventListener("click",()=>setRunning(!S.running));
$("resetBtn").addEventListener("click",()=>{ init(); });
document.querySelectorAll(".speed button").forEach(b=>{
b.addEventListener("click",()=>{
document.querySelectorAll(".speed button").forEach(x=>x.classList.remove("on"));
b.classList.add("on"); S.speed=parseInt(b.dataset.speed);
if(S.running) setRunning(true);
});
});
$("shockBtn").addEventListener("click",()=>{
const ev=SHOCK_EVENTS[Math.floor(Math.random()*SHOCK_EVENTS.length)];
S.muWorld += 0.9+Math.random()*0.5;
log("WORLD EVENT — "+ev,"shock");
if(!S.running){ renderMetrics(computePSI(S.muTrain,S.muWorld), S.accHist.at(-1)??0.97); drawDistChart(); }
});
$("retrainBtn").addEventListener("click",()=>startPipeline("manual trigger"));
$("driftRate").addEventListener("input",e=>$("driftRateVal").textContent=parseFloat(e.target.value).toFixed(3));
$("psiThresh").addEventListener("input",e=>{
$("psiThreshVal").textContent=parseFloat(e.target.value).toFixed(2);
$("mPsiNote").textContent="threshold "+parseFloat(e.target.value).toFixed(2);
});
window.addEventListener("resize",()=>{ drawAccChart(); drawDistChart(); });
// ------------------------------ init -----------------------------------
function init(){
if(S&&S.timer) clearInterval(S.timer);
S=freshState();
$("week").textContent=0;
$("queryFeed").innerHTML=""; $("eventLog").innerHTML="";
$("retrainBtn").disabled=false;
buildPipeSvg(); highlightStage("Monitor");
$("stageNote").textContent="Monitoring production…";
S.accHist.push(0.97); S.psiHist.push(0);
renderMetrics(0,0.97); drawAccChart(); drawDistChart();
log("Model v1 deployed to production — eval accuracy 97.0%","deploy");
$("playBtn").textContent="▶ Run"; $("playBtn").classList.add("primary");
const sp=document.querySelector(".speed button.on"); S.speed=sp?parseInt(sp.dataset.speed):1;
}
init();
</script>
</body>
</html>