// Licensed to the Apache Software Foundation (ASF) under one or more contributor
// license agreements. See the NOTICE file distributed with this work for
// additional information regarding copyright ownership. The ASF licenses this
// file to you under the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License. You may obtain a copy
// of the License at
//
//	http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations
// under the License.

package tracebaseline

import (
	"encoding/json"
	"fmt"
	"io"
	"strings"
	"time"
)

// ComparisonReport pairs pipeline-disabled and full SkyWalking runs over one fixture.
type ComparisonReport struct {
	GeneratedAt time.Time   `json:"generatedAt"`
	Baseline    SuiteReport `json:"baseline"`
	SkyWalking  SuiteReport `json:"skywalking"`
}

// RenderComparisonHTML writes the paired Phase 10 performance report.
func RenderComparisonHTML(writer io.Writer, comparison ComparisonReport) error {
	encoded, marshalErr := json.Marshal(comparison)
	if marshalErr != nil {
		return fmt.Errorf("cannot encode trace merge comparison for HTML: %w", marshalErr)
	}
	document := strings.Replace(comparisonHTML, "__COMPARISON_JSON__", string(encoded), 1)
	if _, writeErr := io.WriteString(writer, document); writeErr != nil {
		return fmt.Errorf("cannot write trace merge comparison HTML: %w", writeErr)
	}
	return nil
}

//nolint:lll // The report is a self-contained artifact whose HTML, CSS, and JavaScript are intentionally embedded.
const comparisonHTML = `<!doctype html>
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>BanyanDB trace plugin performance comparison</title>
<style>
:root{--bg:#071118;--panel:#0d1d26;--line:#23404d;--text:#e4eee8;--muted:#8ba4a7;--base:#54d8e8;--plugin:#b1f568;--amber:#ffbd4a;--bad:#ff6b6b}*{box-sizing:border-box}html{background:var(--bg);color:var(--text);font-family:Inter,ui-sans-serif,system-ui,sans-serif}body{margin:0;background:radial-gradient(circle at 80% 0,#15415188,transparent 35%),linear-gradient(#ffffff04 1px,transparent 1px),linear-gradient(90deg,#ffffff04 1px,transparent 1px);background-size:auto,30px 30px,30px 30px}.page{max-width:1480px;margin:auto;padding:34px 32px 80px}.mast{display:grid;grid-template-columns:1fr auto;align-items:end;border-bottom:1px solid var(--line);padding-bottom:24px}.eyebrow{color:var(--plugin);font:700 11px ui-monospace,monospace;letter-spacing:.18em;text-transform:uppercase}.mast h1{font:400 clamp(43px,6vw,82px) Georgia,serif;letter-spacing:-.045em;line-height:.9;margin:12px 0 0}.badge{border:1px solid var(--plugin);border-radius:99px;padding:16px 22px;color:var(--plugin);font:700 12px ui-monospace,monospace}.boundary{color:var(--muted);font:12px ui-monospace,monospace;margin:16px 0 24px}.grid{display:grid;grid-template-columns:repeat(12,1fr);gap:14px}.card{background:linear-gradient(145deg,#102630ed,#091820ed);border:1px solid var(--line);padding:20px;overflow:hidden}.s12{grid-column:span 12}.s8{grid-column:span 8}.s7{grid-column:span 7}.s6{grid-column:span 6}.s5{grid-column:span 5}.s4{grid-column:span 4}.title{display:flex;align-items:baseline;gap:12px;margin-bottom:18px}.title h2{font:400 25px Georgia,serif;margin:0}.title small{color:var(--muted)}.kpis{display:grid;grid-template-columns:repeat(5,1fr);gap:1px;background:var(--line);border:1px solid var(--line);margin-bottom:14px}.kpi{background:var(--panel);padding:17px}.kpi label{display:block;color:var(--muted);font:10px ui-monospace,monospace;letter-spacing:.1em}.kpi strong{display:block;font:400 30px Georgia,serif;margin:8px 0 4px}.kpi em{font:10px ui-monospace,monospace;color:var(--plugin);font-style:normal}.legend{display:flex;gap:18px;color:var(--muted);font:10px ui-monospace,monospace;margin-top:10px}.dot{display:inline-block;width:8px;height:8px;border-radius:50%;margin-right:6px}svg{display:block;width:100%;overflow:visible}.axis{stroke:#49616a}.gridline{stroke:#19343f}.label{fill:var(--muted);font:10px ui-monospace,monospace}.value{fill:var(--text);font:11px ui-monospace,monospace}table{border-collapse:collapse;width:100%;font:11px ui-monospace,monospace}th,td{padding:9px 8px;border-bottom:1px solid var(--line);text-align:right}th:first-child,td:first-child{text-align:left}th{color:var(--muted);font-weight:500}.positive{color:var(--plugin)}.negative{color:var(--bad)}.gates{display:grid;grid-template-columns:repeat(4,1fr);gap:9px}.gate{border:1px solid var(--line);padding:13px;font:11px ui-monospace,monospace}.gate.pass{border-color:#5c9e4e}.gate.fail{border-color:#a34f4f}.gate b{display:block}.gate span{display:block;color:var(--muted);margin-top:7px;font-size:9px;line-height:1.45}.note{margin-top:12px;color:var(--muted);font:10px/1.6 ui-monospace,monospace}.foot{color:var(--muted);font:10px ui-monospace,monospace;margin-top:26px;display:flex;justify-content:space-between}@media(max-width:1000px){.kpis{grid-template-columns:1fr 1fr}.s8,.s7,.s6,.s5,.s4{grid-column:span 12}.gates{grid-template-columns:1fr 1fr}}@media(max-width:650px){.page{padding:22px 12px}.mast h1{font-size:42px}.badge{display:none}.kpis,.gates{grid-template-columns:1fr}}
</style></head><body><main class="page">
<header class="mast"><div><div class="eyebrow">Apache SkyWalking BanyanDB · Phase 10</div><h1>PLUGIN COST /<br>DATA BENEFIT</h1></div><div class="badge" id="badge">CHECKING</div></header>
<div class="boundary" id="boundary"></div><section class="kpis" id="kpis"></section>
<div class="grid">
<article class="card s8"><div class="title"><h2>System cost comparison</h2><small>median of measured processes · lower is better</small></div><svg id="resource-chart" viewBox="0 0 900 650" aria-label="baseline versus plugin resource comparison"></svg><div class="legend"><span><i class="dot" style="background:var(--base)"></i>pipeline disabled</span><span><i class="dot" style="background:var(--plugin)"></i>full SkyWalking</span></div></article>
<article class="card s4"><div class="title"><h2>Data removed</h2><small>plugin result on the same input</small></div><svg id="removal-chart" viewBox="0 0 420 330" aria-label="plugin data removal diagram"></svg></article>
<article class="card s7"><div class="title"><h2>Plugin call latency histogram</h2><small>exact non-cumulative call counts across measured runs</small></div><svg id="latency-histogram" viewBox="0 0 760 360" aria-label="plugin latency histogram"></svg><div class="note" id="latency-note"></div></article>
<article class="card s5"><div class="title"><h2>Plugin work</h2><small>evaluation and decision cost</small></div><table><tbody id="plugin-table"></tbody></table></article>
<article class="card s6"><div class="title"><h2>Merge execution</h2><small>full 24-hour replay + two-hour cooldown</small></div><table><thead><tr><th>METRIC</th><th>DISABLED</th><th>SKYWALKING</th><th>DELTA</th></tr></thead><tbody id="merge-table"></tbody></table><div class="note">Active merge time is the sum of per-merge elapsed intervals; concurrent intervals can overlap. End-to-end time is measured at phase barriers.</div></article>
<article class="card s6"><div class="title"><h2>On-disk result</h2><small>final core and secondary-index inventory</small></div><svg id="disk-chart" viewBox="0 0 620 330" aria-label="final disk composition comparison"></svg></article>
<article class="card s12"><div class="title"><h2>Run distributions</h2><small>absolute measurements; no best-run selection</small></div><table><thead><tr><th>VARIANT / RUN</th><th>END-TO-END</th><th>CPU</th><th>CGROUP PEAK</th><th>DISK</th><th>MERGE ACTIVE</th><th>ROUNDS</th><th>DROPPED TRACES</th><th>RESULT</th></tr></thead><tbody id="run-table"></tbody></table></article>
<article class="card s12"><div class="title"><h2>Comparison validity</h2><small>the pair is rejected if its data or execution boundary differs</small></div><div class="gates" id="gates"></div></article>
</div><footer class="foot"><span id="identity"></span><span>Standalone HTML · embedded raw suites</span></footer>
<script type="application/json" id="comparison-data">__COMPARISON_JSON__</script><script>
const d=JSON.parse(document.getElementById('comparison-data').textContent),B=d.baseline,S=d.skywalking,br=B.serialRuns||[],sr=S.serialRuns||[];
const ns=v=>(v||0)/1e9,mb=v=>(v||0)/1048576,fmt=(v,p=2)=>Number(v||0).toFixed(p),median=a=>{a=[...a].sort((x,y)=>x-y);if(!a.length)return 0;const m=Math.floor(a.length/2);return a.length%2?a[m]:(a[m-1]+a[m])/2},pct=(value,base)=>base?(value/base-1)*100:0,delta=(value,base,lower=true)=>{const v=pct(value,base),good=lower?v<=0:v>=0;return '<span class="'+(good?'positive':'negative')+'">'+(v>=0?'+':'')+fmt(v,1)+'%</span>'};
const events=r=>r.merges?.events||[],phaseDelta=(r,key)=>((r.primary?.end?.[key]||0)-(r.primary?.start?.[key]||0))+((r.cooldown?.end?.[key]||0)-(r.cooldown?.start?.[key]||0)),wall=r=>ns((r.primary?.wallNanos||0)+(r.cooldown?.wallNanos||0)),cpu=r=>ns(phaseDelta(r,'cpuNanos')),peak=r=>mb(Math.max(r.primary?.end?.cgroupPeakBytes||0,r.cooldown?.end?.cgroupPeakBytes||0,...events(r).map(e=>e.resources?.peakRSSBytes||0))),diskParts=r=>({core:r.inventory?.coreBytes||0,latency:r.inventory?.indexBytes?.latency||0,start:r.inventory?.indexBytes?.start_time||0}),disk=r=>Object.values(diskParts(r)).reduce((a,b)=>a+b,0),active=r=>ns(events(r).reduce((a,e)=>a+(e.resources?.elapsedNanos||0),0)),rounds=r=>events(r).length,dropped=r=>events(r).reduce((a,e)=>a+(e.tracesDropped||0),0),evaluated=r=>events(r).reduce((a,e)=>a+(e.tracesEvaluated||0),0),immature=r=>events(r).reduce((a,e)=>a+(e.tracesImmature||0),0),oracleEvaluated=r=>r.samplingOracle?.evaluated||0,oracleDropped=r=>r.samplingOracle?.dropped||0,samplingRounds=r=>events(r).filter(e=>e.sampling==='executed').length,matureRounds=r=>events(r).filter(e=>(e.matureInputParts||0)>0).length,readMiB=r=>mb(phaseDelta(r,'readBytes')),writeMiB=r=>mb(phaseDelta(r,'writeBytes')),allocatedMiB=r=>mb(phaseDelta(r,'allocatedBytes')),writeAmplification=r=>r.logicalWriteAmplification||0;
const stat=(runs,fn)=>median(runs.map(fn)),bm={wall:stat(br,wall),cpu:stat(br,cpu),peak:stat(br,peak),disk:stat(br,r=>mb(disk(r))),active:stat(br,active),rounds:stat(br,rounds),read:stat(br,readMiB),write:stat(br,writeMiB),allocated:stat(br,allocatedMiB),wa:stat(br,writeAmplification)},sm={wall:stat(sr,wall),cpu:stat(sr,cpu),peak:stat(sr,peak),disk:stat(sr,r=>mb(disk(r))),active:stat(sr,active),rounds:stat(sr,rounds),read:stat(sr,readMiB),write:stat(sr,writeMiB),allocated:stat(sr,allocatedMiB),wa:stat(sr,writeAmplification)};
const evalCount=stat(sr,oracleEvaluated),dropCount=stat(sr,oracleDropped),evaluationVisits=stat(sr,evaluated),immatureVisits=stat(sr,immature),dropRatio=dropCount/Math.max(1,evalCount),baseRows=stat(br,r=>r.inventory?.coreRows||0),pluginRows=stat(sr,r=>r.inventory?.coreRows||0),rowsRemoved=Math.max(0,baseRows-pluginRows),bytesSaved=Math.max(0,(bm.disk-sm.disk));
document.getElementById('boundary').textContent='Same immutable fixture '+String(B.fixtureSHA256||'').slice(0,12)+' · same schedule '+String(B.scheduleSHA256||'').slice(0,12)+' · '+br.length+' disabled and '+sr.length+' SkyWalking measured processes · 2 CPUs / 4 GiB · one shard.';
document.getElementById('kpis').innerHTML=[['TRACES EVALUATED',fmt(evalCount,0),'complete traces presented'],['TRACES REMOVED',fmt(dropCount,0),fmt(dropRatio*100,2)+'% plugin verdict'],['ROWS REMOVED',fmt(rowsRemoved,0),fmt((1-pluginRows/Math.max(1,baseRows))*100,2)+'% stored spans'],['DISK SAVED',fmt(bytesSaved,1)+' MiB',fmt((1-sm.disk/Math.max(.001,bm.disk))*100,2)+'% final inventory'],['PLUGIN TIME / TRACE','—','calculated below']].map(v=>'<div class="kpi"><label>'+v[0]+'</label><strong>'+v[1]+'</strong><em>'+v[2]+'</em></div>').join('');
function tx(x,y,s,c='label',a='start'){return '<text class="'+c+'" x="'+x+'" y="'+y+'" text-anchor="'+a+'">'+s+'</text>'}
{const metrics=[['END-TO-END s',bm.wall,sm.wall],['CPU s',bm.cpu,sm.cpu],['CGROUP PEAK MiB',bm.peak,sm.peak],['FINAL DISK MiB',bm.disk,sm.disk],['ACTIVE MERGE s',bm.active,sm.active],['READ I/O MiB',bm.read,sm.read],['WRITE I/O MiB',bm.write,sm.write],['ALLOCATED MiB',bm.allocated,sm.allocated],['MERGE ROUNDS',bm.rounds,sm.rounds]],svg=document.getElementById('resource-chart');let h='';metrics.forEach((m,i)=>{const y=42+i*67,local=Math.max(m[1],m[2],.001),bw=520*m[1]/local,sw=520*m[2]/local;h+=tx(10,y+8,m[0],'value')+'<rect x="205" y="'+(y-15)+'" width="'+bw+'" height="14" fill="#54d8e866" stroke="#54d8e8"/><rect x="205" y="'+(y+5)+'" width="'+sw+'" height="14" fill="#b1f56866" stroke="#b1f568"/>'+tx(740,y-4,fmt(m[1],2)+' / '+fmt(m[2],2),'label','end')+tx(875,y+12,(pct(m[2],m[1])>=0?'+':'')+fmt(pct(m[2],m[1]),1)+'%','value','end')});svg.innerHTML=h}
{const kept=Math.max(0,evalCount-dropCount),total=Math.max(1,evalCount),cx=205,cy=140,r=100,circ=2*Math.PI*r,dropLen=circ*dropCount/total;document.getElementById('removal-chart').innerHTML='<circle cx="'+cx+'" cy="'+cy+'" r="'+r+'" fill="none" stroke="#173541" stroke-width="35"/><circle cx="'+cx+'" cy="'+cy+'" r="'+r+'" fill="none" stroke="#ffbd4a" stroke-width="35" stroke-dasharray="'+dropLen+' '+(circ-dropLen)+'" transform="rotate(-90 '+cx+' '+cy+')"/>'+tx(cx,132,fmt(dropRatio*100,2)+'%','value','middle')+tx(cx,151,'TRACES DROPPED','label','middle')+tx(70,295,fmt(dropCount,0)+' dropped','value')+tx(350,295,fmt(kept,0)+' retained','value','end')}
const executionRows=sr.flatMap(r=>r.merges?.pluginExecutions||[]).filter(e=>e.result==='success'),pluginCalls=executionRows.reduce((a,e)=>a+(e.calls||0),0),pluginNanos=executionRows.reduce((a,e)=>a+(e.elapsedNanos||0),0),pluginMax=Math.max(0,...executionRows.map(e=>e.maxElapsedNanos||0)),timePerTrace=pluginNanos/Math.max(1,sr.reduce((a,r)=>a+evaluated(r),0));document.querySelectorAll('.kpi')[4].querySelector('strong').textContent=fmt(timePerTrace,1)+' ns';document.querySelectorAll('.kpi')[4].querySelector('em').textContent=fmt(ns(pluginNanos),4)+'s total across '+pluginCalls+' calls';
const hist=new Map;let overflow=0;executionRows.forEach(e=>{(e.durationBuckets||[]).forEach(b=>hist.set(b.upperBoundNanos,(hist.get(b.upperBoundNanos)||0)+(b.calls||0)));overflow+=e.durationOverflow||0});const histRows=[...hist.entries()].sort((a,b)=>a[0]-b[0]).filter(([,count])=>count>0);if(overflow)histRows.push([Infinity,overflow]);{const maxCount=Math.max(1,...histRows.map(x=>x[1])),svg=document.getElementById('latency-histogram');let h='';histRows.forEach((row,i)=>{const x=65+i*(650/Math.max(1,histRows.length)),w=Math.max(16,Math.min(48,560/Math.max(1,histRows.length))),height=245*row[1]/maxCount,label=Number.isFinite(row[0])?'≤'+fmt(row[0]/1e6,row[0]<1e6?2:1)+'ms':'>10s';h+='<rect x="'+(x-w/2)+'" y="'+(285-height)+'" width="'+w+'" height="'+height+'" fill="#b1f56855" stroke="#b1f568"/>'+tx(x,307,label,'label','middle')+tx(x,275-height,row[1],'value','middle')});if(!histRows.length)h+=tx(380,170,'No exact histogram collected — rerun with current binary','value','middle');svg.innerHTML=h}document.getElementById('latency-note').textContent='Mean '+fmt(ns(pluginNanos)/Math.max(1,pluginCalls)*1000,3)+' ms · maximum '+fmt(pluginMax/1e6,3)+' ms · '+pluginCalls+' calls. Buckets match pipeline_plugin_execution_duration_seconds.';
document.getElementById('plugin-table').innerHTML=[['Configured plugin',[...new Set(executionRows.map(e=>e.pluginName))].join(', ')||'none'],['Successful calls',fmt(pluginCalls,0)],['Total Decide wall',fmt(ns(pluginNanos),4)+' s'],['Mean call',fmt(pluginNanos/Math.max(1,pluginCalls)/1e6,3)+' ms'],['Maximum call',fmt(pluginMax/1e6,3)+' ms'],['Unique traces / run',fmt(evalCount,0)],['Trace evaluation visits / run',fmt(evaluationVisits,0)],['Immature trace visits / run',fmt(immatureVisits,0)],['Dropped traces / run',fmt(dropCount,0)],['Deletion ratio',fmt(dropRatio*100,3)+'%'],['Sampling merge rounds',fmt(stat(sr,samplingRounds),0)],['Mature-containing rounds',fmt(stat(sr,matureRounds),0)]].map(v=>'<tr><th>'+v[0]+'</th><td>'+v[1]+'</td></tr>').join('');
const mergeMetrics=[['Primary wall',stat(br,r=>ns(r.primary?.wallNanos)),stat(sr,r=>ns(r.primary?.wallNanos)),'s',true],['Cooldown wall',stat(br,r=>ns(r.cooldown?.wallNanos)),stat(sr,r=>ns(r.cooldown?.wallNanos)),'s',true],['End-to-end wall',bm.wall,sm.wall,'s',true],['Process CPU',bm.cpu,sm.cpu,'s',true],['Active merge time',bm.active,sm.active,'s',true],['Read I/O',bm.read,sm.read,' MiB',true],['Write I/O',bm.write,sm.write,' MiB',true],['Allocated bytes',bm.allocated,sm.allocated,' MiB',true],['Logical write amplification',bm.wa,sm.wa,'×',true],['Merge rounds',bm.rounds,sm.rounds,'',true],['Hot-containing rounds',stat(br,r=>r.hotMerges||0),stat(sr,r=>r.hotMerges||0),'',false],['Mature-containing rounds',stat(br,r=>r.matureMerges||0),stat(sr,r=>r.matureMerges||0),'',false],['Sampling-executed rounds',stat(br,samplingRounds),stat(sr,samplingRounds),'',false]];document.getElementById('merge-table').innerHTML=mergeMetrics.map(m=>'<tr><td>'+m[0]+'</td><td>'+fmt(m[1],m[3]?2:0)+m[3]+'</td><td>'+fmt(m[2],m[3]?2:0)+m[3]+'</td><td>'+delta(m[2],m[1],m[4])+'</td></tr>').join('');
{const names=['CORE','LATENCY SIDX','START-TIME SIDX'],colors=['#b1f568','#54d8e8','#ffbd4a'],parts=runs=>names.map((_,i)=>stat(runs,r=>{const p=diskParts(r);return mb([p.core,p.latency,p.start][i])})),bp=parts(br),sp=parts(sr),mx=Math.max(1,bp.reduce((a,b)=>a+b,0),sp.reduce((a,b)=>a+b,0)),svg=document.getElementById('disk-chart');let h='',ys=[105,235];[bp,sp].forEach((vals,row)=>{let x=120;vals.forEach((v,i)=>{const w=440*v/mx;h+='<rect x="'+x+'" y="'+(ys[row]-30)+'" width="'+w+'" height="55" fill="'+colors[i]+'66" stroke="'+colors[i]+'"/>';if(w>55)h+=tx(x+w/2,ys[row]+4,fmt(v,1),'value','middle');x+=w});h+=tx(105,ys[row]+4,row?'SKYWALKING':'DISABLED','label','end')});h+=names.map((n,i)=>tx(40+i*190,310,'■ '+n,'label')).join('');svg.innerHTML=h}
const allRows=[...br.map(r=>['DISABLED',r]),...sr.map(r=>['SKYWALKING',r])];document.getElementById('run-table').innerHTML=allRows.map(([variant,r])=>'<tr><td>'+variant+' / '+r.runID+'</td><td>'+fmt(wall(r),2)+'s</td><td>'+fmt(cpu(r),2)+'s</td><td>'+fmt(peak(r),1)+' MiB</td><td>'+fmt(mb(disk(r)),1)+' MiB</td><td>'+fmt(active(r),2)+'s</td><td>'+rounds(r)+'</td><td>'+dropped(r)+'</td><td class="'+(r.correct?'positive':'negative')+'">'+(r.correct?'PASS':'FAIL')+'</td></tr>').join('');
const same=(fn)=>br.length>0&&sr.length>0&&fn(B)===fn(S),allCorrect=br.length>=5&&sr.length>=5&&[...br,...sr].every(r=>r.correct&&r.ledgerVerified),pluginHealthy=sr.every(r=>r.pluginMetricsVerified&&events(r).every(e=>(e.pluginExecutions||[]).every(x=>x.result==='success'&&!x.bypassReason))),noBaselineCalls=br.every(r=>r.samplingCalls===0&&(r.merges?.pluginExecutions||[]).length===0),histComplete=pluginCalls>0&&histRows.reduce((a,x)=>a+x[1],0)===pluginCalls;const gates=[['SAME FIXTURE',same(x=>x.fixtureSHA256),'identical immutable input manifest'],['SAME SCHEDULE',same(x=>x.scheduleSHA256),'identical 24-hour publication schedule'],['SAME BINARY',same(x=>x.binarySHA256),'same storage implementation; only pipeline configuration differs'],['FIVE + FIVE RUNS',br.length>=5&&sr.length>=5,br.length+' disabled / '+sr.length+' SkyWalking'],['CORRECT LEDGERS',allCorrect,'every measured run passed its logical ledger'],['BASELINE IS CLEAN',noBaselineCalls,'no sampler work in pipeline-disabled runs'],['PLUGIN HEALTHY',pluginHealthy,'all executions succeeded without bypass'],['HISTOGRAM COMPLETE',histComplete,histRows.reduce((a,x)=>a+x[1],0)+' bucketed / '+pluginCalls+' calls']];document.getElementById('gates').innerHTML=gates.map(g=>'<div class="gate '+(g[1]?'pass':'fail')+'"><b>'+g[0]+'</b><span>'+g[2]+'</span></div>').join('');const valid=gates.every(g=>g[1]);document.getElementById('badge').textContent=valid?'VALID COMPARISON':'HOLD';document.getElementById('badge').style.borderColor=valid?'var(--plugin)':'var(--bad)';document.getElementById('identity').textContent='commit '+String(B.commit||'').slice(0,12)+' · generated '+d.generatedAt;
</script></main></body></html>`
