Skip to content

Commit 0381862

Browse files
committed
Entropy: show the behavioural families
Switched family clustering from single-linkage (which chained 14 servers into one blob) to complete-linkage (every pair in a family agrees >=85%). Added a Families section listing each family's members as chips, plus an F1..F6 badge under each server's column header tying the grid to the families. The 6 families are the parser lineages: Go net/http (Caddy/Gin/Traefik), strict parsers (Node/Express/Uvicorn/Actix/Deno), Java/Apache (Spring Boot/Jetty/Apache), the HAProxy/Envoy proxies, H2O/Puma, Flask/Lighttpd; 8 servers stay distinct.
1 parent 04945ab commit 0381862

3 files changed

Lines changed: 45 additions & 12 deletions

File tree

web/assets/grid.js

Lines changed: 31 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -106,19 +106,24 @@
106106
for(let i=0;i<m;i++){ let fc=0; for(let j=0;j<n;j++) if(!B[i][j]) fc++;
107107
totalFails+=fc; if(fc>=3) systematic+=fc; }
108108
const systematicPct = totalFails>0 ? systematic/totalFails : 1;
109-
// behavioural families: single-linkage clusters of servers agreeing >=85% of the time
110-
const parent=srvs.map((_,i)=>i);
111-
const find=x=>{while(parent[x]!==x){parent[x]=parent[parent[x]];x=parent[x];}return x;};
112-
for(let i=0;i<n;i++)for(let j=i+1;j<n;j++){
113-
let same=0; for(let k=0;k<m;k++) if(B[k][i]===B[k][j]) same++;
114-
if(same/m>=0.85) parent[find(i)]=find(j);
109+
// behavioural families: complete-linkage clusters (EVERY pair in a family agrees >=85%)
110+
const A=Array.from({length:n},()=>new Array(n).fill(1));
111+
for(let i=0;i<n;i++)for(let j=i+1;j<n;j++){let s=0;for(let k=0;k<m;k++)if(B[k][i]===B[k][j])s++; A[i][j]=A[j][i]=s/m;}
112+
let clusters=srvs.map((_,i)=>[i]);
113+
const cdist=(a,b)=>{let mn=1;for(const x of a)for(const y of b)if(A[x][y]<mn)mn=A[x][y];return mn;};
114+
for(;;){ let best=-1,bi=-1,bj=-1;
115+
for(let a=0;a<clusters.length;a++)for(let b=a+1;b<clusters.length;b++){const d=cdist(clusters[a],clusters[b]); if(d>=0.85&&d>best){best=d;bi=a;bj=b;}}
116+
if(bi<0)break; clusters[bi]=clusters[bi].concat(clusters[bj]); clusters.splice(bj,1);
115117
}
116-
const families=new Set(srvs.map((_,i)=>find(i))).size;
118+
clusters.sort((a,b)=>b.length-a.length);
119+
const famGroups=clusters.filter(c=>c.length>=2).map(c=>c.map(i=>srvs[i].name));
120+
const loners=clusters.filter(c=>c.length===1).map(c=>srvs[c[0]].name);
121+
const famOf={}; famGroups.forEach((g,idx)=>g.forEach(nm=>famOf[nm]=idx+1));
117122
// per-test stats for the test-name hover
118123
const byId={};
119124
for(let i=0;i<m;i++){ let passN=0; for(let j=0;j<n;j++) if(B[i][j]) passN++;
120125
byId[vts[i].id]={passN,H:entropy(vts[i],srvs)}; }
121-
return {density,explained,systematicPct,families,n,byId};
126+
return {density,explained,systematicPct,families:famGroups.length,famGroups,loners,famOf,n,byId};
122127
}
123128
function renderInsight(INS){
124129
const box=document.getElementById("insight"); if(!box) return;
@@ -131,6 +136,18 @@
131136
];
132137
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("");
133138
}
139+
function renderFamilies(INS){
140+
const box=document.getElementById("families"); if(!box) return;
141+
if(!INS||!INS.famGroups){ box.innerHTML=""; return; }
142+
let html=`<div class="fam-title">Behavioural families <span>· every pair in a family gives the same verdict ≥85% of the time</span></div><div class="fam-grid">`;
143+
INS.famGroups.forEach((g,i)=>{
144+
html+=`<div class="fam-card"><div class="fam-h"><span class="fam-tag">F${i+1}</span> ${g.length} servers</div>`+
145+
`<div class="fam-chips">${g.map(nm=>`<span class="fam-chip">${esc(nm)}</span>`).join("")}</div></div>`;
146+
});
147+
if(INS.loners.length) html+=`<div class="fam-card loners"><div class="fam-h">Distinct · ${INS.loners.length}</div>`+
148+
`<div class="fam-chips">${INS.loners.map(nm=>`<span class="fam-chip">${esc(nm)}</span>`).join("")}</div></div>`;
149+
box.innerHTML=html+`</div>`;
150+
}
134151
function visibleTests(srvs){
135152
let ts=tests.filter(t=>state.cats.has(t.cat)&&(!state.scoredOnly||t.scored));
136153
ts=ts.map(t=>({t,d:disagreement(t,srvs),h:entropy(t,srvs)}));
@@ -231,15 +248,17 @@
231248
const head=document.getElementById("mhead"), body=document.getElementById("mbody");
232249
head.innerHTML="";body.innerHTML="";
233250
if(!srvs.length){ document.getElementById("rowcount").textContent="No frameworks selected."; return; }
251+
const vts=visibleTests(srvs);
252+
INSIGHT=(CFG.showEntropy && vts.length>=3 && srvs.length>=3)?computeInsight(vts,srvs):null;
253+
renderInsight(INSIGHT); renderFamilies(INSIGHT);
234254
const tr=el("tr");
235255
tr.appendChild(el("th","corner",`test · ${srvs.length} shown →`));
236256
if(CFG.showEntropy) tr.appendChild(el("th","ent-h","entropy<br>bits"));
237257
srvs.forEach(s=>{const th=el("th","srv");th.title=`${s.name}${s.score}/${s.scored} (${s.tier})`;
238-
th.innerHTML=`<div class="rot">${s.name}</div><div class="sc">${s.score}</div>`;tr.appendChild(th);});
258+
const fam=INSIGHT&&INSIGHT.famOf?INSIGHT.famOf[s.name]:null;
259+
th.innerHTML=`<div class="rot">${s.name}</div><div class="sc">${s.score}</div>`+(fam?`<div class="fam-badge">F${fam}</div>`:"");
260+
tr.appendChild(th);});
239261
head.appendChild(tr);
240-
const vts=visibleTests(srvs);
241-
INSIGHT=(CFG.showEntropy && vts.length>=3 && srvs.length>=3)?computeInsight(vts,srvs):null;
242-
renderInsight(INSIGHT);
243262
const frag=document.createDocumentFragment();
244263
vts.forEach(t=>{
245264
const row=el("tr",t.scored?null:"unscored");

web/assets/styles.css

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,19 @@ tr.unscored td.ent{opacity:.5}
257257
.itile .idesc{font-size:10.5px;color:var(--muted);margin-top:3px;line-height:1.35}
258258
#tip .tip-stat{padding:6px 12px;font-family:var(--mono);font-size:11px;color:var(--ink-2);border-top:1px solid var(--line)}
259259
#tip .tip-stat b{color:var(--ink)}
260+
/* behavioural families */
261+
.families{margin:16px 0 4px}
262+
.fam-title{font-family:var(--mono);font-size:11px;color:var(--ink-2);text-transform:uppercase;letter-spacing:.08em;margin-bottom:9px;font-weight:600}
263+
.fam-title span{text-transform:none;letter-spacing:0;color:var(--muted);font-weight:400}
264+
.fam-grid{display:flex;flex-wrap:wrap;gap:10px}
265+
.fam-card{border:1px solid var(--line-2);background:var(--surface);box-shadow:var(--shadow);padding:10px 12px;min-width:150px}
266+
.fam-card.loners{border-style:dashed;box-shadow:none;background:var(--surface-2)}
267+
.fam-h{font-family:var(--mono);font-size:11px;font-weight:700;color:var(--ink-2);margin-bottom:7px;display:flex;align-items:center;gap:7px}
268+
.fam-tag{background:var(--accent);color:var(--accent-ink);padding:1px 6px;font-weight:800}
269+
.fam-chips{display:flex;flex-wrap:wrap;gap:5px}
270+
.fam-chip{font-size:12.5px;background:var(--surface-2);border:1px solid var(--line);padding:3px 9px}
271+
.fam-card.loners .fam-chip{background:var(--surface)}
272+
th.srv .fam-badge{writing-mode:horizontal-tb;font-family:var(--mono);font-size:8.5px;font-weight:700;color:var(--accent);text-align:center;margin-top:2px}
260273
td.cell a{color:inherit;display:flex;align-items:center;justify-content:center;width:100%;height:100%}
261274
td.cell a:hover{text-decoration:none}
262275
td.cell>span{display:flex;align-items:center;justify-content:center;width:100%;height:100%}

web/build.mjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,7 @@ function entropyPage(navHtml){
225225
<div class="provstrip" id="provstrip"></div>
226226
</div>
227227
<div class="insight" id="insight"></div>
228+
<div class="families" id="families"></div>
228229
<div class="toolbar">
229230
<div class="fw-dd" id="fw-dd">
230231
<button class="fw-trigger" id="fw-trigger" aria-expanded="false" aria-haspopup="true">

0 commit comments

Comments
 (0)