Skip to content

Commit 65cd458

Browse files
committed
Entropy: add matrix-study insight panel; default to Flagship+Infrastructure
The Entropy page now defaults to the 25 Flagship+Infrastructure servers (drops Emerging/Experimental) via GRID_CONFIG.defaultTiers, and shows an insight panel recomputed live for the current selection: density, distinct behavioural fingerprints, effective rank (Jacobi eigen of the grand-centered matrix), the share of variance a 1-D strictness/Rasch model explains, and a count of residual anomalies. Cells that defy the strictness model (|residual|>=0.75) get a corner marker; the cell hover shows its residual and the test-name hover shows entropy/discrimination/difficulty. For the 25-server MUST matrix: effective rank ~10, strictness explains ~41% — divergence is high-dimensional, not a single strictness gradient.
1 parent e9ae1ce commit 65cd458

3 files changed

Lines changed: 109 additions & 2 deletions

File tree

web/assets/grid.js

Lines changed: 94 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,9 @@
3737
if(CFG.levels) tests=tests.filter(t=>CFG.levels.includes(t.lvl));
3838
const CATS=[...new Set(tests.map(t=>t.cat))];
3939

40-
const state={ sel:new Set(servers.map(s=>s.name)), cats:new Set(CATS), divOnly:false, scoredOnly:false };
40+
const defaultServers = CFG.defaultTiers ? servers.filter(s=>CFG.defaultTiers.includes(s.tier)) : servers;
41+
const state={ sel:new Set(defaultServers.map(s=>s.name)), cats:new Set(CATS), divOnly:false, scoredOnly:false };
42+
let INSIGHT=null; // matrix-study metrics for the Entropy page (recomputed each render)
4143

4244
// ----- helpers -----
4345
const el=(t,c,h)=>{const e=document.createElement(t);if(c)e.className=c;if(h!=null)e.innerHTML=h;return e;};
@@ -75,6 +77,88 @@
7577
return h;
7678
}
7779
const ENT_MAX=Math.log2(3); // max for {Pass,Warn,Fail} — bar normalisation
80+
const ANOM_T=0.75; // |residual| above which a cell is a genuine anomaly (confident model, contradicted)
81+
82+
// ---- matrix-study helpers (Entropy page insight panel) ----
83+
function pearson(a,b){
84+
const N=a.length; let sa=0,sb=0; for(let i=0;i<N;i++){sa+=a[i];sb+=b[i];}
85+
const ma=sa/N,mb=sb/N; let num=0,da=0,db=0;
86+
for(let i=0;i<N;i++){const x=a[i]-ma,y=b[i]-mb; num+=x*y; da+=x*x; db+=y*y;}
87+
return (da>1e-12&&db>1e-12)?num/Math.sqrt(da*db):0;
88+
}
89+
function jacobiEigenvalues(A){ // eigenvalues of a symmetric matrix, descending
90+
const N=A.length, a=A.map(r=>r.slice());
91+
for(let sweep=0;sweep<50;sweep++){
92+
let off=0; for(let p=0;p<N;p++)for(let q=p+1;q<N;q++)off+=a[p][q]*a[p][q];
93+
if(off<1e-11) break;
94+
for(let p=0;p<N;p++)for(let q=p+1;q<N;q++){
95+
if(Math.abs(a[p][q])<1e-13) continue;
96+
const phi=0.5*Math.atan2(2*a[p][q],a[q][q]-a[p][p]), c=Math.cos(phi), s=Math.sin(phi);
97+
for(let k=0;k<N;k++){const kp=a[k][p],kq=a[k][q]; a[k][p]=c*kp-s*kq; a[k][q]=s*kp+c*kq;}
98+
for(let k=0;k<N;k++){const pk=a[p][k],qk=a[q][k]; a[p][k]=c*pk-s*qk; a[q][k]=s*pk+c*qk;}
99+
}
100+
}
101+
return a.map((r,i)=>r[i]).sort((x,y)=>y-x);
102+
}
103+
function effectiveRank(V){ // participation ratio of singular values of grand-centered V
104+
const rows=V.length, cols=V[0].length; let g=0; for(const r of V)for(const v of r)g+=v; g/=rows*cols;
105+
const useCols=cols<=rows, d=useCols?cols:rows;
106+
const G=Array.from({length:d},()=>new Array(d).fill(0));
107+
for(let i=0;i<d;i++)for(let j=i;j<d;j++){ let s=0;
108+
if(useCols){ for(let k=0;k<rows;k++) s+=(V[k][i]-g)*(V[k][j]-g); }
109+
else { for(let k=0;k<cols;k++) s+=(V[i][k]-g)*(V[j][k]-g); }
110+
G[i][j]=G[j][i]=s;
111+
}
112+
const ev=jacobiEigenvalues(G).filter(x=>x>1e-8);
113+
const tot=ev.reduce((a,b)=>a+b,0); if(tot<=0) return {rank:0,var2:0};
114+
let h=0; for(const l of ev){const p=l/tot; if(p>0) h-=p*Math.log(p);}
115+
return {rank:Math.exp(h), var2:(ev[0]+(ev[1]||0))/tot};
116+
}
117+
function raschResiduals(V){ // additive-logit fit logit p = theta_col - beta_row; residual = obs - pred
118+
const m=V.length, n=V[0].length;
119+
const rowsum=V.map(r=>r.reduce((a,b)=>a+b,0));
120+
const colsum=new Array(n).fill(0); for(let i=0;i<m;i++)for(let j=0;j<n;j++)colsum[j]+=V[i][j];
121+
const theta=new Array(n).fill(0), beta=new Array(m).fill(0);
122+
const lg=x=>1/(1+Math.exp(-Math.max(-30,Math.min(30,x))));
123+
for(let it=0;it<200;it++){
124+
for(let i=0;i<m;i++){ let pr=0,dd=0; for(let j=0;j<n;j++){const p=lg(theta[j]-beta[i]); pr+=p; dd+=p*(1-p);}
125+
beta[i]=Math.max(-8,Math.min(8, beta[i]+(pr-rowsum[i])/Math.max(dd,1e-6))); }
126+
for(let j=0;j<n;j++){ let pc=0,dd=0; for(let i=0;i<m;i++){const p=lg(theta[j]-beta[i]); pc+=p; dd+=p*(1-p);}
127+
theta[j]=Math.max(-8,Math.min(8, theta[j]-(pc-colsum[j])/Math.max(dd,1e-6))); }
128+
}
129+
const gm=rowsum.reduce((a,b)=>a+b,0)/(m*n); let ssTot=0,ssRes=0;
130+
const resid=V.map((r,i)=>r.map((v,j)=>{const e=v-lg(theta[j]-beta[i]); ssTot+=(v-gm)**2; ssRes+=e*e; return e;}));
131+
return {resid, explained:ssTot>0?1-ssRes/ssTot:0};
132+
}
133+
function computeInsight(vts,srvs){
134+
const m=vts.length,n=srvs.length,enc={Pass:1,Warn:0.5,Fail:0};
135+
const V=vts.map(t=>srvs.map(s=>{const r=s.byId[t.id]; return r?(enc[r.verdict]??0):0;}));
136+
const density=V.reduce((a,r)=>a+r.reduce((x,y)=>x+y,0),0)/(m*n);
137+
const colpat=new Set(srvs.map(s=>vts.map(t=>{const r=s.byId[t.id];return r?r.verdict:"NA";}).join("|"))).size;
138+
const er=effectiveRank(V), rf=raschResiduals(V);
139+
const colsum=srvs.map((s,j)=>{let x=0;for(let i=0;i<m;i++)x+=V[i][j];return x;});
140+
const byId={}, residKey={}; let anom=0;
141+
for(let i=0;i<m;i++){
142+
const disc=pearson(V[i],colsum);
143+
const passN=srvs.reduce((a,s)=>a+(((s.byId[vts[i].id]||{}).verdict==="Pass")?1:0),0);
144+
byId[vts[i].id]={disc,passN,H:entropy(vts[i],srvs)};
145+
for(let j=0;j<n;j++){const r=rf.resid[i][j]; residKey[vts[i].id+"|"+srvs[j].name]=r; if(Math.abs(r)>=ANOM_T)anom++;}
146+
}
147+
return {density,colpat,n,effRank:er.rank,var2:er.var2,explained:rf.explained,anom,byId,residKey};
148+
}
149+
function renderInsight(INS){
150+
const box=document.getElementById("insight"); if(!box) return;
151+
if(!INS){ box.innerHTML=""; return; }
152+
const pct=Math.round(INS.explained*100);
153+
const tiles=[
154+
["Density",(INS.density*100).toFixed(0)+"%","MUST verdicts that pass (warn = ½)"],
155+
["Distinct behaviours",INS.colpat+" / "+INS.n,"unique verdict fingerprints among shown servers"],
156+
["Effective rank",INS.effRank.toFixed(1),"independent behavioural axes · 1 = pure strictness"],
157+
["Strictness explains",pct+"%","of the pattern — the other "+(100-pct)+"% is residual divergence"],
158+
["Anomalies",String(INS.anom),"cells defying the strictness model (|residual| ≥ 0.75)"],
159+
];
160+
box.innerHTML=tiles.map(([k,v,d])=>`<div class="itile"><div class="iv">${v}</div><div class="ik">${k}</div><div class="idesc">${d}</div></div>`).join("");
161+
}
78162
function visibleTests(srvs){
79163
let ts=tests.filter(t=>state.cats.has(t.cat)&&(!state.scoredOnly||t.scored));
80164
ts=ts.map(t=>({t,d:disagreement(t,srvs),h:entropy(t,srvs)}));
@@ -182,6 +266,8 @@
182266
th.innerHTML=`<div class="rot">${s.name}</div><div class="sc">${s.score}</div>`;tr.appendChild(th);});
183267
head.appendChild(tr);
184268
const vts=visibleTests(srvs);
269+
INSIGHT=(CFG.showEntropy && vts.length>=3 && srvs.length>=3)?computeInsight(vts,srvs):null;
270+
renderInsight(INSIGHT);
185271
const frag=document.createDocumentFragment();
186272
vts.forEach(t=>{
187273
const row=el("tr",t.scored?null:"unscored");
@@ -200,6 +286,7 @@
200286
srvs.forEach(s=>{
201287
const r=s.byId[t.id]; const v=r?r.verdict:"NA";
202288
const td=el("td","cell "+v);
289+
if(INSIGHT){const rk=INSIGHT.residKey[t.id+"|"+s.name]; if(rk!==undefined&&Math.abs(rk)>=ANOM_T) td.classList.add("anom");}
203290
const code=esc(statusText(r));
204291
td.innerHTML=t.url?`<a href="${t.url}" tabindex="-1">${code}</a>`:`<span>${code}</span>`;
205292
td.dataset.s=s.name;td.dataset.t=t.id;td.dataset.v=v;
@@ -223,10 +310,12 @@
223310
const rid=e.target.closest("td.rid");
224311
if(rid){
225312
const t=tById[rid.dataset.t]; if(!t) return;
313+
const st=INSIGHT&&INSIGHT.byId[t.id];
226314
tip.innerHTML=
227315
`<div class="tip-h"><b>${esc(t.id)}</b></div>`+
228316
`<div class="tip-sub">${esc(t.cat)} · ${esc(t.lvl==="Must"?"MUST":t.lvl)}${t.rfc?" · "+esc(t.rfc):""} · expected ${esc(t.exp||"?")}</div>`+
229317
`<div class="tip-desc">${esc(t.desc||"No description available.")}</div>`+
318+
(st?`<div class="tip-stat">entropy <b>${st.H.toFixed(2)}</b> bits · discrimination <b>${st.disc>=0?"+":""}${st.disc.toFixed(2)}</b> · <b>${st.passN}/${INSIGHT.n}</b> pass${st.disc<0.2?` · <span class="warn-tag">low-discrimination</span>`:""}</div>`:"")+
230319
(t.url?`<div class="tip-foot">Click the name to open the full test page →</div>`:"");
231320
positionTip(rid,tip);
232321
document.querySelectorAll("td.cell.hl").forEach(x=>x.classList.remove("hl"));
@@ -237,9 +326,13 @@
237326
const req=(r&&r.rawRequest)?trunc(r.rawRequest,700):"(request unavailable)";
238327
const res=(r&&r.rawResponse)?trunc(r.rawResponse,700)
239328
:((r&&r.connectionState==="ClosedByServer")?"(connection closed by server — no response)":"(no response captured)");
329+
let residHtml="";
330+
if(INSIGHT){ const rk=INSIGHT.residKey[c.dataset.t+"|"+c.dataset.s];
331+
if(rk!==undefined) residHtml=`<div class="tip-stat">residual <b>${rk>=0?"+":""}${rk.toFixed(2)}</b>${Math.abs(rk)>=ANOM_T?` · <span class="warn-tag">anomaly — ${rk>0?"passes where its strictness predicts a fail":"fails where its strictness predicts a pass"}</span>`:""}</div>`; }
240332
tip.innerHTML=
241333
`<div class="tip-h"><b>${esc(c.dataset.s)}</b> · <span class="v-${c.dataset.v}">${(c.dataset.v||"n/a").toUpperCase()}</span> → ${esc(statusText(r))}</div>`+
242334
`<div class="tip-sub">${esc(t.id)} · ${esc(t.cat)} · ${esc(t.lvl)} · expected ${esc(t.exp||"")}</div>`+
335+
residHtml+
243336
`<span class="lbl">request sent</span><pre>${esc(req)}</pre>`+
244337
`<span class="lbl">response</span><pre>${esc(res)}</pre>`;
245338
positionTip(c,tip);

web/assets/styles.css

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,18 @@ td.ent .ent-bar{display:block;height:3px;background:var(--surface-2);margin-top:
249249
td.ent .ent-bar i{display:block;height:100%;background:var(--accent)}
250250
tr:hover td.ent{background:var(--surface-2)}
251251
tr.unscored td.ent{opacity:.5}
252+
/* Entropy insight panel */
253+
.insight{display:flex;flex-wrap:wrap;gap:12px;margin:18px 0 4px}
254+
.itile{flex:1;min-width:158px;background:var(--surface);border:1px solid var(--line);box-shadow:var(--shadow);padding:14px 16px}
255+
.itile .iv{font-family:var(--mono);font-size:24px;font-weight:800;letter-spacing:-.02em;color:var(--ink);line-height:1;font-variant-numeric:tabular-nums}
256+
.itile .ik{font-size:12px;font-weight:650;color:var(--ink-2);margin-top:7px}
257+
.itile .idesc{font-size:10.5px;color:var(--muted);margin-top:3px;line-height:1.35}
258+
/* anomaly marker on residual-defying cells */
259+
td.cell.anom{position:relative}
260+
td.cell.anom::after{content:"";position:absolute;top:0;right:0;border:5px solid transparent;border-top-color:var(--ink);border-right-color:var(--ink);opacity:.9}
261+
#tip .tip-stat{padding:6px 12px;font-family:var(--mono);font-size:11px;color:var(--ink-2);border-top:1px solid var(--line)}
262+
#tip .tip-stat b{color:var(--ink)}
263+
#tip .warn-tag{color:var(--fail);font-weight:700}
252264
td.cell a{color:inherit;display:flex;align-items:center;justify-content:center;width:100%;height:100%}
253265
td.cell a:hover{text-decoration:none}
254266
td.cell>span{display:flex;align-items:center;justify-content:center;width:100%;height:100%}

web/build.mjs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -214,7 +214,8 @@ function landing(navHtml){
214214

215215
function entropyPage(navHtml){
216216
const cfg={ cats:["Compliance","Smuggling","MalformedInput"], levels:["Must"],
217-
showCatChips:false, showEntropy:true, sort:"entropy" };
217+
showCatChips:false, showEntropy:true, sort:"entropy",
218+
defaultTiers:["Flagship","Infrastructure"] };
218219
const body=`<div class="hero-lite">
219220
<h1>Entropy</h1>
220221
<p>A matrix study of the <strong>MUST / MUST&nbsp;NOT</strong> tests in Compliance, Smuggling and
@@ -223,6 +224,7 @@ function entropyPage(navHtml){
223224
higher means the field is split on a hard requirement. Rows are ranked most-contested first.</p>
224225
<div class="provstrip" id="provstrip"></div>
225226
</div>
227+
<div class="insight" id="insight"></div>
226228
<div class="toolbar">
227229
<div class="fw-dd" id="fw-dd">
228230
<button class="fw-trigger" id="fw-trigger" aria-expanded="false" aria-haspopup="true">

0 commit comments

Comments
 (0)