Hugdrigo's picture
Upload 3 files
893cd0f verified
Raw History Blame Contribute Delete
52.8 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0">
<meta name="theme-color" content="#0b1320"><meta name="description" content="SynthShield: local-first synthetic data generation, privacy risk diagnostics and utility analysis.">
<title>SynthShield | Synthetic Data Privacy Lab</title>
<style>
:root{font-family:Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;color:#e7edf3;background:#09111c;font-synthesis:none}*{box-sizing:border-box}body{margin:0;min-width:320px}button,select,input{font:inherit}button{cursor:pointer}button:focus-visible,input:focus-visible,select:focus-visible{outline:2px solid #62e3bd;outline-offset:2px}button:disabled{opacity:.45;cursor:not-allowed}a{color:#9cefd2}.app-shell{display:flex;min-height:100vh;background:#0a121e}.sidebar{width:254px;flex:0 0 254px;border-right:1px solid #21303d;background:#0a121d;min-height:100vh;padding:30px 15px 26px 18px;display:flex;flex-direction:column;position:sticky;top:0;height:100vh;z-index:20}.brand{display:flex;gap:11px;align-items:center;padding:3px 9px 38px}.brand-mark{background:linear-gradient(145deg,#2fd49b,#237a82);width:37px;height:37px;border-radius:11px;color:#e4fffa;font-size:27px;font-weight:bold;text-align:center;line-height:36px;box-shadow:0 0 30px #40cfa029}.brand strong{display:block;color:#f2fbfb;font-size:19px;letter-spacing:-.7px}.brand small{display:block;font-size:8px;letter-spacing:1.2px;color:#69808e;margin-top:4px;font-weight:800}.workspace-label,.subhead{color:#758999;font-size:10px;letter-spacing:2px;font-weight:800}.workspace-label{padding:0 16px 15px}.sidebar nav{display:grid;gap:5px}.nav-link{display:flex;align-items:center;text-align:left;gap:13px;color:#9aaabb;background:transparent;padding:12px 14px;border:1px solid transparent;border-radius:9px;font-size:12px;font-weight:650;width:100%}.nav-link span{font-size:18px;color:#7d92a2;width:18px;text-align:center}.nav-link:hover{background:#182533}.nav-link.active{background:linear-gradient(90deg,#163c37,#14262e);border-color:#23584d;color:#91f6d6}.nav-link.active span{color:#79ebc9}.sidebar-bottom{margin-top:auto;display:flex;gap:8px;padding:13px 11px;background:#101f28;border:1px solid #224239;border-radius:9px;color:#80ab9e;font-size:9px;font-weight:800;letter-spacing:.75px}.sidebar-bottom strong{color:#63dbad}.status-dot,.inline-dot{background:#56e4a5;display:inline-block;width:7px;height:7px;border-radius:50%;box-shadow:0 0 13px #40daa1}.version{color:#627583;font-size:10px;line-height:1.9;margin:18px 10px 0}.main{min-width:0;flex:1}.topbar{height:69px;border-bottom:1px solid #22303e;display:flex;align-items:center;justify-content:space-between;padding:0 39px;background:#0a131e}.breadcrumb{font-size:10px;color:#758796;letter-spacing:1.5px;font-weight:800}.breadcrumb span{margin:0 7px;color:#425967}.breadcrumb b{color:#b5c3cc}.topbar-right{display:flex;gap:16px;align-items:center}.chip{font-size:10px;font-weight:800;letter-spacing:.7px;padding:9px 13px;border:1px solid #305e50;border-radius:7px;background:#14342e;color:#6ae3b5}.avatar{border:1px solid #3a5963;border-radius:50%;width:30px;height:30px;display:grid;place-items:center;font-size:11px;color:#b7e4d7;background:#193237;font-weight:800}.content{max-width:1610px;margin:auto;padding:30px 39px 20px}.hero{display:flex;min-height:258px;justify-content:space-between;background:radial-gradient(ellipse at 92% 43%,#174c48 0%,#123631 22%,#102431 48%,#101c2a 80%);border:1px solid #28464c;border-radius:15px;overflow:hidden;position:relative;padding:36px 41px 31px}.hero-left{max-width:700px;z-index:1}.eyebrow{font-size:10px;letter-spacing:1.7px;color:#8be1c6;font-weight:850;display:flex;gap:9px;align-items:center}.hero-sep{color:#496a66}.hero h1{margin:17px 0 12px;font-size:clamp(32px,3.8vw,51px);letter-spacing:-2.4px;line-height:1.1;color:#f5fbfc}.hero h1 span{color:#75e8bf}.hero p{color:#a3b6bf;font-size:13px;line-height:1.8;max-width:590px;margin:0 0 21px}.hero-actions{display:flex;gap:11px;flex-wrap:wrap}.btn{border:1px solid #344553;border-radius:8px;background:#182935;color:#dfebeb;padding:11px 16px;font-size:11px;font-weight:750;white-space:nowrap}.btn:hover{filter:brightness(1.15)}.btn-primary{background:#65e4b6;color:#09241f;border-color:#65e4b6;box-shadow:0 9px 25px #3fd1a021}.btn-primary span{margin-left:13px}.btn-ghost{background:#1c3038;border-color:#38565c}.btn-compact{padding:10px 13px}.hero-visual{width:270px;position:absolute;right:36px;top:8px;height:253px;opacity:.95}.orbit{border:1px solid #4fbdac44;border-radius:50%;position:absolute;inset:18px}.orbit-a{inset:10px;transform:rotateX(67deg) rotateZ(-17deg)}.orbit-b{inset:10px;transform:rotateY(68deg) rotateZ(36deg)}.orbit-c{inset:32px;border-style:dashed}.core-shield{position:absolute;left:90px;top:77px;background:linear-gradient(140deg,#7eeec1,#267b87);box-shadow:0 0 75px #24d4a350,0 0 0 20px #3aca9a0c;width:93px;height:93px;border:1px solid #adfce5;border-radius:27px;transform:rotate(45deg);display:grid;place-items:center;font-size:50px;color:#0e4b4c}.core-shield::first-letter{transform:rotate(-45deg)}.floating-label{position:absolute;font-size:8px;color:#bcf4df;letter-spacing:.8px;padding:8px 9px;background:#142d35df;border:1px solid #3c746d;border-radius:6px;box-shadow:0 5px 20px #0005}.floating-label strong{color:#73f0b1}.l1{right:0;top:59px}.l2{left:0;bottom:56px}.control-strip{border:1px solid #253744;background:#101e2c;border-radius:11px;margin:19px 0 31px;padding:13px 19px;display:flex;align-items:center;justify-content:space-between;gap:15px}.control-info,.control-actions{display:flex;align-items:center;gap:12px}.control-info strong{font-size:12px;display:block;color:#e1ebee}.control-info small{font-size:10px;color:#7994a3;display:block;margin-top:4px}.mini-icon{color:#71eac0;border-radius:7px;background:#1b3938;width:34px;height:34px;display:grid;place-items:center;font-size:19px}.control-actions select,.field-select{background:#122430;color:#d8e6e8;border:1px solid #344b54;border-radius:7px;padding:9px 29px 9px 12px;font-size:11px;max-width:240px}.section-title{display:flex;align-items:end;justify-content:space-between;gap:14px;margin-bottom:18px}.section-title h2{font-size:25px;margin:6px 0 6px;letter-spacing:-.9px}.section-title p{color:#8498a8;font-size:12px;margin:0}.timestamp{font-size:9px;color:#779c9e;border:1px solid #264947;background:#10292a;padding:9px 12px;border-radius:6px;letter-spacing:.6px;font-weight:850}.metric-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:15px}.metric-card,.panel{background:#101e2b;border:1px solid #293d4a;border-radius:12px}.metric-card{padding:19px 19px 16px;min-height:125px}.metric-label{font-size:10px;color:#90a6b0;letter-spacing:.45px;font-weight:700;display:flex;justify-content:space-between;align-items:center}.metric-label b{font-size:17px;font-weight:400;color:#72c4b7}.metric-value{font-size:31px;font-weight:800;letter-spacing:-1.5px;margin-top:10px;color:#f2fafb;font-variant-numeric:tabular-nums}.metric-help{font-size:10px;color:#7b91a0;margin-top:7px}.metric-help em{font-style:normal;color:#7ee6b8}.analysis-grid{display:grid;grid-template-columns:1.4fr 1fr;gap:16px;margin-top:16px}.panel{padding:19px;min-width:0}.panel-head{display:flex;justify-content:space-between;align-items:start;gap:9px;margin-bottom:16px}.panel-title{font-size:13px;font-weight:800;color:#e6efef}.panel-description{font-size:10px;color:#8296a3;margin-top:5px;line-height:1.6}.panel-tag{font-size:9px;letter-spacing:.4px;color:#94c7b5;background:#183d38;border:1px solid #2b6556;padding:5px 8px;border-radius:5px;white-space:nowrap}.bar-row{margin:13px 0}.bar-top{display:flex;justify-content:space-between;color:#a6b8bf;font-size:11px;margin:0 0 6px;gap:10px}.bar-top strong{color:#c8dbe1;font-size:11px}.track{height:9px;background:#20333f;border-radius:20px;overflow:hidden}.fill{display:block;background:linear-gradient(90deg,#379c95,#6ee6b6);height:100%;border-radius:20px;min-width:1px}.fill.blue{background:linear-gradient(90deg,#417fbb,#7cacdc)}.fill.yellow{background:linear-gradient(90deg,#bf8745,#eac279)}.two-bar{display:grid;grid-template-columns:1fr 1fr;gap:3px}.legend{display:flex;flex-wrap:wrap;gap:14px;color:#889eaa;font-size:10px;margin:13px 0 2px}.legend span:before{content:'';display:inline-block;width:8px;height:8px;border-radius:2px;background:#65dab2;margin-right:6px}.legend span:last-child:before{background:#699ed1}.table-wrap{overflow:auto;border-radius:8px;border:1px solid #263a47}table{border-collapse:collapse;width:100%;text-align:left;font-size:11px}th,td{padding:11px 12px;border-bottom:1px solid #223543;white-space:nowrap}th{background:#152634;color:#90aab5;text-transform:uppercase;font-size:9px;letter-spacing:.7px}td{color:#c1d4d9}tr:last-child td{border-bottom:0}tr:hover td{background:#152634}.tag-danger{color:#f4a5a1}.tag-safe{color:#7fe0b6}.tag-warn{color:#eec58b}.notice{padding:12px 14px;background:#172b33;border:1px solid #385b57;border-radius:9px;color:#a8c1c4;font-size:11px;line-height:1.75;margin:14px 0}.notice b{color:#bfe8d8}.notice.warning{background:#2a261e;border-color:#6e5035;color:#dec5ab}.section-stack{display:grid;gap:16px}.twocol{display:grid;grid-template-columns:1fr 1fr;gap:16px}.form-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:17px}.field{display:block}.field>span,.field-label{display:block;font-size:11px;font-weight:700;color:#a9bbc3;margin-bottom:9px}.field input[type=number],.field input[type=text]{background:#102231;border:1px solid #304955;border-radius:6px;width:100%;padding:10px 11px;color:#edf6f7}.field input[type=range]{width:100%;accent-color:#67e1b8}.field-select{max-width:none;width:100%}.field-checks{display:flex;gap:9px;flex-wrap:wrap}.pillcheck{display:inline-flex;gap:7px;align-items:center;background:#1a2c39;border:1px solid #38505b;border-radius:7px;color:#bbced4;padding:10px;font-size:11px}.pillcheck input{accent-color:#54d9af}.buttonrow{display:flex;gap:9px;flex-wrap:wrap;margin-top:15px}.sparkline{width:100%;height:210px;display:block}.plot-grid line{stroke:#2c4150;stroke-width:.75}.plot-label{fill:#8199a5;font-size:9px}.plot-reference{fill:none;stroke:#7cafd9;stroke-width:2.5}.plot-synthetic{fill:none;stroke:#5be0ac;stroke-width:2.5}.metric-sm{font-size:23px;font-weight:800;color:#ecfaf8;letter-spacing:-.7px}.kv{display:grid;grid-template-columns:1fr auto;padding:12px 0;border-bottom:1px solid #283a47;color:#9eb4bc;font-size:11px;gap:15px}.kv strong{color:#e3eff0}.fine{font-size:10px;line-height:1.7;color:#8096a4}.big-score{font-size:57px;color:#76e8b6;font-weight:850;letter-spacing:-3px}.score-caption{color:#99aeb5;font-size:12px;margin-top:4px}.progress-ring{background:conic-gradient(#69e0b4 var(--value),#243846 0);border-radius:50%;width:116px;height:116px;display:grid;place-items:center}.progress-ring>div{width:90px;height:90px;display:grid;place-items:center;border-radius:50%;background:#10202d;color:#e3faf4;font-weight:800;font-size:23px}.empty{padding:25px;text-align:center;color:#8aabb2;font-size:12px}.upload{border:1px dashed #4e7c7a;background:#112b31;border-radius:12px;padding:23px;text-align:center;color:#b6cfd0;display:block;cursor:pointer}.upload strong{color:#e2f6f1}.upload input{display:block;margin:15px auto 0;max-width:100%;font-size:11px}.sample-code{white-space:pre-wrap;overflow-wrap:anywhere;padding:13px;border-radius:7px;background:#0a1721;border:1px solid #28424c;color:#8fdfbd;font-size:11px;line-height:1.6}.privacy-meter{display:flex;align-items:center;gap:13px}.privacy-meter .meter{height:12px;background:#223643;border-radius:12px;flex:1;overflow:hidden}.privacy-meter .meter div{height:100%;background:linear-gradient(90deg,#6ce4b7,#ffb76b);border-radius:12px}.privacy-meter b{color:#e8f3ef;min-width:52px;text-align:right}footer{margin-top:29px;border-top:1px solid #223441;padding:20px 0;display:flex;justify-content:space-between;gap:15px;color:#617984;font-size:9px;letter-spacing:.4px} .toast{position:fixed;right:21px;bottom:20px;padding:13px 17px;background:#1b3e3b;border:1px solid #50ad9a;color:#e6fff7;border-radius:8px;font-size:12px;z-index:50;box-shadow:0 10px 30px #0008;opacity:0;pointer-events:none;transform:translateY(10px);transition:.2s}.toast.show{opacity:1;transform:translateY(0)}.mobile-menu{display:none}.visually-hidden{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0,0,0,0)}.hint{font-size:10px;color:#8ba4ad;margin-top:8px}code{color:#9be6c9} @media(max-width:1160px){.sidebar{width:213px;flex-basis:213px}.hero-visual{opacity:.38}.metric-grid{grid-template-columns:repeat(2,1fr)}.content{padding:25px}.topbar{padding:0 25px}}@media(max-width:780px){.sidebar{position:fixed;left:-260px;top:0;transition:left .2s;width:254px}.sidebar.open{left:0;box-shadow:0 0 0 100vmax #0008}.mobile-menu{display:block;border:0;background:transparent;color:#b7d8d6;font-size:21px;margin-right:12px}.topbar{height:58px;padding:0 17px;justify-content:start}.topbar-right{margin-left:auto}.breadcrumb{font-size:9px}.topbar .chip{display:none}.content{padding:18px}.hero{padding:23px 21px;min-height:auto}.hero-visual{display:none}.hero h1{font-size:38px;letter-spacing:-1.8px}.hero p{font-size:12px}.control-strip,.control-actions{align-items:stretch;flex-direction:column}.control-actions{width:100%}.control-actions select{max-width:none;width:100%}.section-title{align-items:start}.timestamp{display:none}.analysis-grid,.twocol,.form-grid{grid-template-columns:1fr}.metric-grid{gap:9px}.metric-card{padding:13px;min-height:110px}.metric-value{font-size:25px}.metric-label{font-size:9px}.panel{padding:15px}footer{flex-direction:column}.eyebrow{font-size:8px}.hero-actions{flex-direction:column;align-items:stretch}}@media(max-width:390px){.metric-grid{grid-template-columns:1fr 1fr}.metric-value{font-size:23px}.hero h1{font-size:32px}}
</style>
</head>
<body>
<div class="app-shell">
<aside class="sidebar" id="sidebar">
<div class="brand"><span class="brand-mark">✦</span><div><strong>SynthShield</strong><small>PRIVACY INTELLIGENCE / 34</small></div></div>
<div class="workspace-label">WORKSPACE</div>
<nav id="navigation" aria-label="Application sections">
<button data-page="overview" class="nav-link active"><span>β—ˆ</span> Command center</button>
<button data-page="privacy" class="nav-link"><span>β—‡</span> Privacy audit</button>
<button data-page="synthetic" class="nav-link"><span>βš—</span> Synthetic studio</button>
<button data-page="utility" class="nav-link"><span>β–€</span> Utility benchmark</button>
<button data-page="noise" class="nav-link"><span>β‰ˆ</span> Noise simulator</button>
<button data-page="data" class="nav-link"><span>β–§</span> Data & methods</button>
</nav>
<div class="sidebar-bottom"><div class="status-dot"></div><span>LOCAL-FIRST ENGINE <strong>ACTIVE</strong></span></div>
<div class="version">Research demo Β· v1.0.0<br>No APIs. No tracking. No uploads.</div>
</aside>
<main class="main">
<div class="topbar"><button class="mobile-menu" id="mobileMenu" aria-label="Toggle navigation">☰</button><div class="breadcrumb">RESEARCH PORTFOLIO <span> / </span> <b>PROJECT 034</b></div><div class="topbar-right"><span class="chip chip-green">● SYSTEM ONLINE</span><span class="avatar">HG</span></div></div>
<div class="content">
<section class="hero"><div class="hero-left"><div class="eyebrow"><span class="inline-dot"></span> PRIVACY ENGINEERING LABORATORY <span class="hero-sep"> / </span> PROJECT 34</div><h1>Synthetic data.<br><span>Measurable trust.</span></h1><p>Explore privacy risk, generate realistic synthetic records, and quantify utility trade-offs in a reproducible browser-based research studio.</p><div class="hero-actions"><button class="btn btn-primary" data-page="synthetic">Open synthetic studio <span>β†—</span></button><button class="btn btn-ghost" data-action="download-report">Export audit report ↓</button></div></div><div class="hero-visual" aria-hidden="true"><div class="orbit orbit-a"></div><div class="orbit orbit-b"></div><div class="orbit orbit-c"></div><div class="core-shield">✦</div><div class="floating-label l1">K-ANONYMITY <strong>●</strong></div><div class="floating-label l2">UTILITY SCORE <strong>β†—</strong></div></div></section>
<div class="control-strip"><div class="control-info"><span class="mini-icon">β–₯</span><div><strong>Active dataset</strong><small id="datasetLabel">Omnichannel commerce Β· Synthetic demo</small></div></div><div class="control-actions"><label class="visually-hidden" for="scenarioSelect">Demo scenario</label><select id="scenarioSelect" aria-label="Demo scenario"><option value="commerce">Omnichannel commerce</option><option value="public">Community service access</option><option value="education">Learning engagement</option></select><button class="btn btn-compact" data-page="data">Import CSV β†—</button></div></div>
<div class="section-title" id="viewHeading"><div><span class="subhead">PORTFOLIO INTELLIGENCE</span><h2>Command center</h2><p>Your privacy posture and synthetic-data quality, in one workspace.</p></div><div class="timestamp" id="modeTag">LOCAL ANALYSIS Β· NO SERVER</div></div>
<div id="dashboard" aria-live="polite"></div>
<footer> <span>SYNTHSHIELD / RESEARCH PORTFOLIO PROJECT 034</span> <span>Demo metrics are not certifications of anonymity or differential privacy.</span></footer>
</div>
</main>
</div>
<div class="toast" id="toast" role="status" aria-live="polite"></div>
<script type="module">
// Original module core.mjs
/** SynthShield analytical core. No dependencies, no network, no personal data collection. */
const FIELDS=['age_band','region','segment','channel','spend','outcome'];
const QI_FIELDS=['age_band','region','segment','channel'];
const CATEGORICAL=['age_band','region','segment','channel','outcome'];
const MAX_ROWS=5000;
function seeded(seed=42){let s=(Math.floor(Number(seed))>>>0)||1;return ()=>{s=(s+0x6D2B79F5)|0;let t=Math.imul(s^(s>>>15),1|s);t^=t+Math.imul(t^(t>>>7),61|t);return ((t^(t>>>14))>>>0)/4294967296;};}
function pick(values,weights,rng){if(!values.length)throw Error('Empty choice set');let total=weights.reduce((a,b)=>a+b,0),x=rng()*total;for(let i=0;i<values.length;i++){x-=weights[i];if(x<0)return values[i]}return values.at(-1)}
function mean(xs){return xs.length?xs.reduce((s,x)=>s+x,0)/xs.length:0}
function sd(xs){if(xs.length<2)return 0;let m=mean(xs);return Math.sqrt(xs.reduce((s,x)=>s+(x-m)**2,0)/xs.length)}
function norm(rng){const u=Math.max(rng(),1e-10),v=rng();return Math.sqrt(-2*Math.log(u))*Math.cos(2*Math.PI*v)}
function clamp(x,a,b){return Math.min(b,Math.max(a,x))}
function currency(n){return new Intl.NumberFormat('en-US',{style:'currency',currency:'USD',maximumFractionDigits:0}).format(n)}
function countBy(rows,key){const map=new Map;for(const r of rows)map.set(String(r[key]),(map.get(String(r[key]))||0)+1);return map}
function distribution(rows,key){let n=rows.length||1,counts=countBy(rows,key);return Object.fromEntries([...counts].map(([k,v])=>[k,v/n]));}
function tvd(p,q){return [...new Set([...Object.keys(p),...Object.keys(q)])].reduce((sum,k)=>sum+Math.abs((p[k]||0)-(q[k]||0)),0)/2}
function entropy(p){return -Object.values(p).reduce((s,v)=>s+(v>0?v*Math.log2(v):0),0)}
function privacyAudit(rows,qis=['age_band','region'],sensitive='outcome'){
if(!rows.length)throw Error('No data to audit');if(!qis.length||qis.some(k=>!QI_FIELDS.includes(k)))throw Error('Select valid quasi-identifiers');
if(!CATEGORICAL.includes(sensitive))throw Error('Unknown sensitive field');
const groups=new Map;for(const r of rows){const label=qis.map(k=>String(r[k])).join(' | ');if(!groups.has(label))groups.set(label,[]);groups.get(label).push(r)}
const global=distribution(rows,sensitive),arr=[...groups].map(([key,items])=>{
const p=distribution(items,sensitive);return {key,count:items.length,diversity:Object.keys(p).length,tvd:tvd(p,global),members:items.length};
});
const sorted=[...arr].sort((a,b)=>a.count-b.count||a.key.localeCompare(b.key));
const minK=Math.min(...arr.map(g=>g.count));const riskRows=arr.filter(g=>g.count<5).reduce((s,g)=>s+g.count,0);
const lFailRows=arr.filter(g=>g.diversity<2).reduce((s,g)=>s+g.count,0);
const tFailRows=arr.filter(g=>g.tvd>0.2).reduce((s,g)=>s+g.count,0);
const unique=arr.filter(g=>g.count===1).reduce((s,g)=>s+g.count,0);
return {n:rows.length,groupCount:arr.length,qis:[...qis],minK,medianK:sorted.map(x=>x.count).sort((a,b)=>a-b)[Math.floor(arr.length/2)],riskRows,riskPct:100*riskRows/rows.length,unique,uniquePct:100*unique/rows.length,lFailRows,lFailPct:100*lFailRows/rows.length,tFailRows,tFailPct:100*tFailRows/rows.length,groups:sorted,global};
}
function makeSynthetic(original,count=400,seed=2026,blend=0.7){
if(!original.length)throw Error('Load data first');count=Math.floor(Number(count));if(!Number.isFinite(count)||count<10||count>MAX_ROWS)throw Error('Synthetic sample size must be 10–5000');
const rng=seeded(seed);const categoryFreq=Object.fromEntries(CATEGORICAL.map(k=>[k,[...countBy(original,k)]]));
const draw=(name,subset=original)=>{const vals=[...countBy(subset,name)];if(!vals.length)return draw(name,original);return pick(vals.map(v=>v[0]),vals.map(v=>v[1]+(blend===0?1:0)),rng)};
const combos=(keys)=>{let m=new Map;for(const r of original){let key=keys.map(k=>r[k]).join('\u0001');if(!m.has(key))m.set(key,[]);m.get(key).push(r)}return m};
const byRegion=combos(['region']),bySegment=combos(['segment']),bySegmentChannel=combos(['segment','channel']);
const allSpends=original.map(x=>x.spend),globalMean=mean(allSpends),globalSD=sd(allSpends);
const out=[];for(let i=0;i<count;i++){
let region=draw('region'),age_band=draw('age_band',byRegion.get(region)||original),segment=draw('segment',byRegion.get(region)||original);
let match=bySegment.get(segment)||original,channel=draw('channel',match);
let selected=bySegmentChannel.get([segment,channel].join('\u0001'))||match;
let outcome=draw('outcome',selected);
let typical=mean(selected.map(r=>r.spend)),dispersion=sd(selected.map(r=>r.spend));
let target=blend*typical+(1-blend)*globalMean;
let variability=(blend*dispersion+(1-blend)*globalSD)||Math.max(globalMean*0.2,1);
let spend=Math.round(clamp(target+norm(rng)*variability,0,1000000)*100)/100;
out.push({age_band,region,segment,channel,spend,outcome});
}
return out;
}
function utilityAudit(original,synthetic){if(!original.length||!synthetic.length)throw Error('Both datasets are required');
const cat=CATEGORICAL.map(k=>({field:k,drift:tvd(distribution(original,k),distribution(synthetic,k))}));
const a=original.map(r=>r.spend),b=synthetic.map(r=>r.spend);const meanA=mean(a),meanB=mean(b);
const avgCat=mean(cat.map(x=>x.drift));const meanGap=Math.abs(meanA-meanB)/Math.max(1,Math.abs(meanA));
const segs=[...new Set([...original.map(r=>r.segment),...synthetic.map(r=>r.segment)])].sort().map(k=>{
let a=original.filter(r=>r.segment===k),b=synthetic.filter(r=>r.segment===k);return {name:k,reference:a.length,synthetic:b.length,meanReference:mean(a.map(x=>x.spend)),meanSynthetic:mean(b.map(x=>x.spend))};
});
const originalExact=new Set(original.map(r=>FIELDS.map(k=>String(r[k])).join('|')));
const exact=synthetic.filter(r=>originalExact.has(FIELDS.map(k=>String(r[k])).join('|'))).length;
return {avgCat,meanGap,meanA,meanB,sdA:sd(a),sdB:sd(b),fields:cat,segments:segs,exactRows:exact,exactPct:100*exact/synthetic.length,qualityScore:100*clamp(1-0.65*avgCat-0.35*Math.min(1,meanGap),0,1)};
}
function laplace(rng,scale){const u=rng()-0.5;return -scale*Math.sign(u)*Math.log(1-2*Math.abs(u));}
function noisyHistogram(rows,field='segment',epsilon=1,seed=777){
epsilon=Number(epsilon);if(!CATEGORICAL.includes(field))throw Error('Unknown histogram field');if(!(epsilon>0&&epsilon<=10))throw Error('Epsilon must be between 0 and 10');
const rng=seeded(seed),counts=countBy(rows,field),values=[...counts].sort((a,b)=>a[0].localeCompare(b[0]));
const scale=1/epsilon;let error=0;
const bars=values.map(([label,actual])=>{const noisy=Math.max(0,actual+laplace(rng,scale));error+=Math.abs(noisy-actual);return {label,actual,noisy}});
return {field,epsilon,scale,meanAbsoluteCountError:bars.length?error/bars.length:0,bars,seed,warning:'Educational seeded Laplace simulation only; not a certified DP release. The seed is reproducible and not secret.'};
}
function parseCSV(text){if(typeof text!=='string'||text.length>2_000_000)throw Error('CSV must be smaller than 2 MB');text=text.replace(/^\uFEFF/,'');let records=[],row=[],part='',quote=false;
for(let i=0;i<text.length;i++){let c=text[i];if(quote){if(c==='"'&&text[i+1]==='"'){part+='"';i++}else if(c==='"')quote=false;else part+=c}else if(c==='"'){if(part.length)throw Error('Invalid CSV quote');quote=true}else if(c===','){row.push(part);part=''}else if(c==='\n'||c==='\r'){if(c==='\r'&&text[i+1]==='\n')i++;row.push(part);if(row.some(x=>x.trim()))records.push(row);row=[];part=''}else part+=c;}
if(quote)throw Error('Unterminated CSV quote');row.push(part);if(row.some(x=>x.trim()))records.push(row);
if(records.length<2)throw Error('CSV needs a header and at least one row');if(records.length-1>MAX_ROWS)throw Error('Maximum 5000 rows');
const headers=records[0].map(x=>x.trim().toLowerCase());if(new Set(headers).size!==headers.length)throw Error('Duplicate CSV headers');const missing=FIELDS.filter(k=>!headers.includes(k));if(missing.length)throw Error('Missing columns: '+missing.join(', '));
const arr=records.slice(1).map((cells,i)=>{if(cells.length!==headers.length)throw Error(`CSV row ${i+2} has ${cells.length} columns; expected ${headers.length}`);let obj={};for(let name of FIELDS){let value=cells[headers.indexOf(name)].trim();if(!value)throw Error(`Row ${i+2}: empty ${name}`);if(name==='spend'){const n=Number(value);if(!Number.isFinite(n)||n<0||n>1000000)throw Error(`Row ${i+2}: spend must be from 0 to 1000000`);obj[name]=n}else{if(value.length>70)throw Error(`Row ${i+2}: ${name} too long`);obj[name]=value}}return obj});
return arr;
}
function escapeCsv(value){let str=String(value??'');if(/^[\s]*[=+@\-]/.test(str))str="'"+str;return /[",\r\n]/.test(str)?'"'+str.replaceAll('"','""')+'"':str;}
function toCSV(rows){return [FIELDS.join(','),...rows.map(r=>FIELDS.map(k=>escapeCsv(r[k])).join(','))].join('\r\n')+'\r\n';}
// Original module data.mjs
const SCENARIOS=[
{id:'commerce',label:'Omnichannel commerce',tag:'Retail analytics',description:'Synthetic customer journeys across channels and spend tiers.',count:540},
{id:'public',label:'Community service access',tag:'Public programs',description:'Fictional service requests with small regional groups and diverse outcomes.',count:480},
{id:'education',label:'Learning engagement',tag:'Education analytics',description:'Fictional learning journeys with intentionally unbalanced segments.',count:520}
];
function exampleRows(id='commerce'){
const idx=Math.max(0,SCENARIOS.findIndex(s=>s.id===id)),profile=SCENARIOS[idx],rng=seeded(3217+idx*71),out=[];
const bands=['18-24','25-34','35-44','45-54','55+'];const regions=['North','South','East','West'];const segments=['Core','Growth','Priority','Emerging'];const channels=['Web','Mobile','Branch','Partner'];
const outcomes=[['Retained','Churned'],['Served','Delayed'],['Completed','At risk']][idx];
for(let i=0;i<profile.count;i++){
let age_band=pick(bands,[16,28,24,19,13],rng);
let region=pick(regions,idx===1?[42,31,21,6]:[31,26,25,18],rng);
let segment=pick(segments,idx===2?[50,28,17,5]:[40,29,23,8],rng);
let channel=pick(channels,idx===1?[12,11,56,21]:[30,45,17,8],rng);
// Construct intentional quasi-identifier subgroups, never sourced from real people.
if(i%41===0){region='West';segment='Emerging';channel='Partner';age_band='55+'}
let base=[155,84,124][idx]+segments.indexOf(segment)*22+channels.indexOf(channel)*11;
let spend=Math.round(clamp(base+norm(rng)*Math.max(20,base*0.36),0,100000)*100)/100;
let chance=clamp(0.74-segments.indexOf(segment)*0.09+((idx===1&&region==='West')?-0.18:0)+((idx===2&&segment==='Emerging')?-0.20:0),0.08,0.94);
let outcome=rng()<chance?outcomes[0]:outcomes[1];
out.push({age_band,region,segment,channel,spend,outcome});
}
return out;
}
// Original module app.mjs
const $=s=>document.querySelector(s),escapeHtml=v=>String(v??'').replace(/[&<>"']/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
const percent=v=>Number(v).toFixed(1)+'%',dec=v=>Number(v).toFixed(3);
const state={page:'overview',scenario:'commerce',source:'demo',original:exampleRows('commerce'),synthetic:[],qis:['age_band','region','channel'],sensitive:'outcome',count:400,seed:2026,blend:.7,epsilon:1,noiseField:'segment',noiseSeed:777};
state.synthetic=makeSynthetic(state.original,state.count,state.seed,state.blend);
const panel=(title,subtitle,content,tag='')=>`<article class="panel"><header class="panel-head"><div><div class="panel-title">${title}</div><div class="panel-description">${subtitle}</div></div>${tag?`<span class="panel-tag">${tag}</span>`:''}</header>${content}</article>`;
const stat=(label,value,desc,symbol='β—‡')=>`<div class="metric-card"><div class="metric-label">${label}<b>${symbol}</b></div><div class="metric-value">${value}</div><div class="metric-help">${desc}</div></div>`;
const bar=(label,value,valueLabel=null,color='')=>`<div class="bar-row"><div class="bar-top"><span>${escapeHtml(label)}</span><strong>${valueLabel??percent(value)}</strong></div><div class="track"><div class="fill ${color}" style="width:${clamp(value,0,100)}%"></div></div></div>`;
const metricGrid=(p,u)=>`<div class="metric-grid">${stat('REFERENCE RECORDS',p.n.toLocaleString(),'Rows in current dataset','β–€')}${stat('AT-RISK ROWS',percent(p.riskPct),'Equivalence classes below k = 5','β—ˆ')}${stat('MINIMUM k',p.minK,'Smallest equivalence class','⬑')}${stat('UTILITY SCORE',u.qualityScore.toFixed(1),'Illustrative marginal fidelity, not privacy','β†—')}</div>`;
function table(headers,rows){return `<div class="table-wrap"><table><thead><tr>${headers.map(h=>`<th>${escapeHtml(h)}</th>`).join('')}</tr></thead><tbody>${rows.map(r=>`<tr>${r.map(c=>`<td>${c}</td>`).join('')}</tr>`).join('')}</tbody></table></div>`}
function kv(k,v){return `<div class="kv"><span>${k}</span><strong>${v}</strong></div>`}
function overview(p,u){
const split=p.groups.reduce((m,g)=>{const k=g.count<5?'Below 5':g.count<10?'5 to 9':g.count<20?'10 to 19':'20+';m[k]=(m[k]||0)+1;return m},{});
const max=Math.max(1,...Object.values(split));
return `${metricGrid(p,u)}<div class="analysis-grid">${panel('Equivalence class distribution','How many records share the same quasi-identifiers.',Object.entries({'Below 5':split['Below 5']||0,'5 to 9':split['5 to 9']||0,'10 to 19':split['10 to 19']||0,'20+':split['20+']||0}).map(([k,v])=>bar(k,100*v/max,`${v} groups`,k==='Below 5'?'yellow':'')).join('')+`<div class="notice"><b>Quasi-identifiers:</b> ${p.qis.map(escapeHtml).join(' + ')}. Smaller groups may make records easier to distinguish.</div>`,'K-GROUPS')}${panel('Privacy risk signals','Share of rows in groups failing illustrative screening thresholds.',bar('k < 5 (small groups)',p.riskPct)+bar('β„“ < 2 (low sensitive diversity)',p.lFailPct)+bar('t > 0.20 (distribution disparity)',p.tFailPct)+`<div class="notice warning">These are screening diagnostics, not a guarantee of anonymity or safe disclosure.</div>`,'SCREENING')}</div><div class="analysis-grid">${panel('Synthetic fidelity snapshot','Comparison of source and generated feature distributions.',u.fields.map(v=>bar(v.field,100*(1-v.drift),percent(100*(1-v.drift)))).join('')+`<div class="fine">1 βˆ’ total variation distance for each categorical field. Higher means more similar. Similarity alone does not establish privacy.</div>`,'UTILITY')}${panel('Dataset provenance','Local-only operation and reproducibility metadata.',kv('Input source',state.source==='demo'?'Clearly labeled synthetic demo':'User-imported CSV')+kv('Synthetic records',state.synthetic.length.toLocaleString())+kv('Generator seed',state.seed)+kv('Generation strategy','Conditional resampling')+kv('Backend requests','None')+`<div class="buttonrow"><button class="btn btn-primary" data-page="privacy">Inspect risk signals β†—</button><button class="btn" data-page="utility">Compare utility β†—</button></div>`,'TRANSPARENCY')}</div>`;
}
function privacy(p){let checks=QI_FIELDS.map(k=>`<label class="pillcheck"><input type="checkbox" data-qi="${k}" ${state.qis.includes(k)?'checked':''}> ${k.replace('_',' ')}</label>`).join('');
const worst=p.groups.slice(0,13).map(g=>[escapeHtml(g.key),g.count,`<span class="${g.count<5?'tag-danger':'tag-safe'}">${g.count<5?'REVIEW':'OK'}</span>`,g.diversity,dec(g.tvd)]);
return `<div class="metric-grid">${stat('MINIMUM k',p.minK,'Minimum group size','⬑')}${stat('GROUPS BELOW k=5',p.groups.filter(g=>g.count<5).length,'Groups with fewer than 5 records','β—‡')}${stat('LOW DIVERSITY',percent(p.lFailPct),'Rows in groups with fewer than 2 outcomes','β—ˆ')}${stat('HIGH DISPARITY',percent(p.tFailPct),'Rows in groups where TVD > .20','≋')}</div><div class="section-stack" style="margin-top:16px">${panel('Audit configuration','Choose the fields that might be known to an external party.',`<div class="field-label">Quasi-identifiers (select at least one)</div><div class="field-checks">${checks}</div><div class="notice"><b>Interpretation:</b> k is the smallest equivalence-class size; β„“ counts distinct values of the sensitive outcome; t uses total variation distance against the global outcome distribution. Thresholds are examples, not regulatory standards.</div>`,'ADJUSTABLE')}${panel('Smallest equivalence classes','Groups sorted from smallest to largest. Higher distinctness increases potential singling-out risk.',table(['Quasi-identifier combination','Records','k = 5 screen','Distinct outcomes','TVD'],worst),'ROW-LEVEL SUMMARY')}${panel('Risk audit summary','Descriptive diagnostics only, not a privacy certification.',kv('Equivalence groups',p.groupCount)+kv('Rows in groups with k < 5',`${p.riskRows} (${percent(p.riskPct)})`)+kv('Unique quasi-identifier combinations',`${p.unique} rows`)+kv('Rows in groups with β„“ < 2',p.lFailRows)+kv('Rows in groups with t > 0.20',p.tFailRows)+`<div class="buttonrow"><button class="btn" data-action="download-report">Download audit report ↓</button></div>`,'AUDIT')}</div>`;
}
function syntheticView(p,u){let example=state.synthetic.slice(0,8).map(r=>[...FIELDS.map(k=>escapeHtml(k==='spend'?currency(r[k]):r[k]))]);
return `<div class="metric-grid">${stat('SYNTHETIC ROWS',state.synthetic.length.toLocaleString(),'Generated locally in the browser','⬑')}${stat('CATEGORICAL FIDELITY',percent((1-u.avgCat)*100),'1 βˆ’ mean categorical TVD','β†—')}${stat('SOURCE MEAN SPEND',currency(u.meanA),'Reference sample average','β–€')}${stat('SYNTHETIC MEAN SPEND',currency(u.meanB),'Generated sample average','β—‡')}</div><div class="analysis-grid">${panel('Generation controls','Adjust sample size, random seed, and how much conditional structure to retain.',`<div class="form-grid"><label class="field"><span>Rows to generate</span><input id="sampleCount" type="number" min="10" max="5000" step="10" value="${state.count}"></label><label class="field"><span>Deterministic seed</span><input id="seedInput" type="number" min="1" max="4294967295" step="1" value="${state.seed}"></label><label class="field" style="grid-column:1/-1"><span>Conditional structure blend Β· <b>${Math.round(state.blend*100)}%</b></span><input id="blendInput" type="range" min="0" max="1" step="0.05" value="${state.blend}"><small class="hint">Higher blend favors conditional group statistics; lower blend shrinks spend toward its global mean.</small></label></div><div class="buttonrow"><button class="btn btn-primary" data-action="generate">Regenerate dataset β†—</button><button class="btn" data-action="download-synthetic">Export synthetic CSV ↓</button></div><div class="notice warning"><b>Release safety:</b> These records are generated for experimentation, not certified as anonymous. Review privacy, memorization and disclosure risks before sharing data derived from confidential sources.</div>`,'CONFIGURE')}${panel('Generation process','What the browser actually computes.',kv('1. Estimate frequencies','Observed categories')+kv('2. Generate cohort combinations','Conditional draws')+kv('3. Sample sensitive outcome','Segment + channel')+kv('4. Generate spend value','Group moments + noise')+kv('5. Evaluate utility','Original vs generated')+`<div class="notice">No model is trained on a server. The browser uses a reproducible pseudorandom generator. Determinism helps audits, not privacy.</div>`,'METHOD')}</div><div style="margin-top:16px">${panel('Synthetic record preview','First 8 generated records. No real identifying information is included in demo data.',table(['Age band','Region','Segment','Channel','Spend','Outcome'],example),'PREVIEW')}</div>`;
}
function svgLine(pointsA,pointsB,labels){const vals=[...pointsA,...pointsB];const max=Math.max(...vals,1),min=Math.min(...vals,0),w=580,h=205,l=47,r=15,t=15,b=29;const pos=(v,i)=>[l+i*(w-l-r)/Math.max(1,vals.length/2-1),t+(max-v)/Math.max(0.001,max-min)*(h-t-b)];const path=x=>x.map((v,i)=>pos(v,i).join(',')).map((v,i)=>(i?'L':'M')+v).join(' ');
let grid=Array.from({length:4},(_,i)=>{const y=t+i*(h-t-b)/3;return `<line x1="${l}" x2="${w-r}" y1="${y}" y2="${y}"/><text x="4" y="${y+3}" class="plot-label">${Math.round(max-(max-min)*i/3)}</text>`}).join('');
let xLabels=labels.map((s,i)=>`<text x="${pos(0,i)[0]}" y="195" text-anchor="middle" class="plot-label">${escapeHtml(s)}</text>`).join('');return `<svg class="sparkline" viewBox="0 0 ${w} ${h}" role="img" aria-label="Comparison of original and synthetic segment means"><g class="plot-grid">${grid}</g><path class="plot-reference" d="${path(pointsA)}"/><path class="plot-synthetic" d="${path(pointsB)}"/>${xLabels}</svg>`}
function utility(p,u){return `<div class="metric-grid">${stat('FIDELITY SCORE',u.qualityScore.toFixed(1)+'/100','Illustrative marginal fidelity','β†—')}${stat('AVG CATEGORY TVD',dec(u.avgCat),'0 means identical marginals','β—ˆ')}${stat('MEAN SPEND GAP',percent(100*u.meanGap),'Relative absolute mean difference','β‰ˆ')}${stat('EXACT ROW MATCHES',percent(u.exactPct),'Literal row matches across data sets','β—‡')}</div><div class="analysis-grid">${panel('Group-level numeric utility','Mean spend by segment, with original and synthetic values.',svgLine(u.segments.map(x=>x.meanReference),u.segments.map(x=>x.meanSynthetic),u.segments.map(x=>x.name))+`<div class="legend"><span>Reference</span><span>Synthetic</span></div><div class="fine">Different segment frequencies may also affect the overall average.</div>`,'SEGMENTS')}${panel('Feature fidelity','1 βˆ’ categorical total variation distance.',u.fields.map(v=>bar(v.field,100*(1-v.drift),`${dec(v.drift)} TVD`)).join('')+`<div class="notice"><b>Important:</b> high fidelity can coexist with high privacy risk. Do not interpret utility measurements as anonymization evidence.</div>`,'DISTRIBUTIONS')}</div><div style="margin-top:16px">${panel('Segment breakdown','Compare cohort sizes and numeric means.',table(['Segment','Reference n','Synthetic n','Reference mean','Synthetic mean','Absolute gap'],u.segments.map(s=>[escapeHtml(s.name),s.reference,s.synthetic,currency(s.meanReference),currency(s.meanSynthetic),currency(Math.abs(s.meanReference-s.meanSynthetic))])),'DIAGNOSTICS')}</div>`}
function noise(){const hist=noisyHistogram(state.original,state.noiseField,state.epsilon,state.noiseSeed);const max=Math.max(1,...hist.bars.flatMap(x=>[x.actual,x.noisy]));return `<div class="metric-grid">${stat('EPSILON (Ξ΅)',state.epsilon.toFixed(2),'Lower epsilon adds more noise','β‰ˆ')}${stat('LAPLACE SCALE',hist.scale.toFixed(2),'1 / epsilon for unit sensitivity','β—‡')}${stat('AVG COUNT ERROR',hist.meanAbsoluteCountError.toFixed(2),'Illustrative observed error','β–€')}${stat('HISTOGRAM BINS',hist.bars.length,'Categories in selected field','β–₯')}</div><div class="analysis-grid">${panel('Noise experiment controls','Simulate Laplace perturbation for a categorical count histogram.',`<div class="form-grid"><label class="field"><span>Histogram field</span><select id="noiseField" class="field-select">${CATEGORICAL.map(k=>`<option value="${k}" ${state.noiseField===k?'selected':''}>${k.replace('_',' ')}</option>`).join('')}</select></label><label class="field"><span>Simulation seed</span><input type="number" id="noiseSeed" min="1" max="4294967295" value="${state.noiseSeed}"></label><label class="field" style="grid-column:1/-1"><span>Privacy-noise parameter Ξ΅ Β· ${state.epsilon.toFixed(2)}</span><input type="range" id="epsilonSlider" min="0.2" max="3" step="0.1" value="${state.epsilon}"><small class="hint">Lower Ξ΅ produces more noise in this demonstration.</small></label></div><div class="buttonrow"><button class="btn btn-primary" data-action="run-noise">Recompute simulation β†—</button><button class="btn" data-action="download-report">Download report ↓</button></div><div class="notice warning">This is a seeded, reproducible <b>educational simulation</b>, not a certified differential-privacy system. The seed is public and not cryptographically secure. It does not protect a synthetic dataset export.</div>`,'EDUCATIONAL')}${panel('Reference vs perturbed counts','Each category receives a Laplace noise draw; negatives are clamped after the draw.',hist.bars.map(v=>`<div class="bar-row"><div class="bar-top"><span>${escapeHtml(v.label)}</span><strong>${Math.round(v.actual)} β†’ ${v.noisy.toFixed(1)}</strong></div><div class="two-bar"><div class="track"><span class="fill blue" style="width:${100*v.actual/max}%"></span></div><div class="track"><span class="fill" style="width:${100*v.noisy/max}%"></span></div></div></div>`).join('')+`<div class="legend"><span>Noise-perturbed</span><span>Original</span></div>`,'HISTOGRAM')}</div><div style="margin-top:16px">${panel('Mechanism and limitations','Why the noise is an illustration rather than a privacy guarantee.',`<div class="notice"><b>Formula:</b> noisy count = observed count + Laplace(scale = 1/Ξ΅). Each input row contributes to one bin. The deterministic seed enables exact reproduction of a classroom experiment but does not provide secure private randomness.</div><div class="fine">Formal differential privacy requires a specified neighboring-data relation, an appropriately implemented randomized mechanism, sound randomness, correct composition and operational controls. None of those are certified here.</div>`,'METHODOLOGY')}</div>`}
function data(){const source=state.source==='demo'?'Synthetic demonstration dataset':'User-imported CSV (retained in this browser tab only)';return `<div class="twocol">${panel('Import your own data','Choose a local UTF-8 CSV file. Nothing is uploaded to a server.',`<label class="upload"><strong>Drop-in CSV analysis</strong><br>Choose a CSV file up to 2 MB and 5,000 rows.<input id="fileInput" type="file" accept=".csv,text/csv" aria-label="Select CSV file"></label><div class="notice">When imported, the source records stay in memory in this browser tab. Do not publish the original CSV to a public repository.</div><div class="buttonrow"><button class="btn" data-action="reset-demo">Restore demo data ↻</button><button class="btn" data-action="download-template">Download CSV template ↓</button></div>`,'PRIVATE PROCESSING')}${panel('Current dataset','Dataset provenance, dimensions and schema.',kv('Source',escapeHtml(source))+kv('Records',state.original.length)+kv('Generated records',state.synthetic.length)+kv('Required columns',FIELDS.length)+kv('Active quasi-identifiers',state.qis.map(escapeHtml).join(', '))+`<div class="fine">The dashboard is a research demonstration, not a privacy audit certification.</div>`,'METADATA')}</div><div class="analysis-grid">${panel('Required CSV schema','Headers must match exactly. Categorical fields can contain text, but spend must be numeric.',`<pre class="sample-code">age_band,region,segment,channel,spend,outcome\n25-34,North,Core,Web,120.50,Retained\n35-44,South,Growth,Mobile,95.00,Churned</pre><div class="fine">CSV files can contain quoted commas and quotes. Maximum 5,000 rows. Values are validated before replacing the current dataset.</div>`,'SCHEMA')}${panel('Reproducibility & exports','Download a reviewable snapshot of inputs, metrics and methods.',`<div class="buttonrow"><button class="btn btn-primary" data-action="download-report">Download audit JSON ↓</button><button class="btn" data-action="download-markdown">Download research memo ↓</button><button class="btn" data-action="download-synthetic">Download synthetic CSV ↓</button></div><div class="notice warning"><b>Warning:</b> Generated records can still reveal information about their source. Never treat this tool as sufficient clearance to release sensitive training data.</div>`,'OPEN SCIENCE')}</div></div><div style="margin-top:16px">${panel('Methodology and scope','Exact assumptions and limitations.',`<div class="kv"><span>Risk screening</span><strong>k-anonymity Β· distinct β„“ Β· categorical TVD t</strong></div><div class="kv"><span>Synthesis</span><strong>Conditional categorical draws + group-moment sampling</strong></div><div class="kv"><span>Quality</span><strong>Marginal TVD Β· numeric mean gap Β· row overlap</strong></div><div class="kv"><span>Noise</span><strong>Seeded educational Laplace demonstration</strong></div><div class="kv"><span>Processing</span><strong>100% client side, no backend</strong></div><p class="fine">No disclosure-risk model is complete. The generator does not claim membership-inference resistance, differential privacy, k-anonymity enforcement or safe commercial disclosure. Results are descriptive and depend on the chosen fields.</p>`,'RESEARCH LIMITATIONS')}</div>`}
function report(){const p=privacyAudit(state.original,state.qis,state.sensitive),u=utilityAudit(state.original,state.synthetic),n=noisyHistogram(state.original,state.noiseField,state.epsilon,state.noiseSeed);return {app:'SynthShield',version:'1.0.0',project:34,createdAt:new Date().toISOString(),data:{source:state.source,scenario:state.scenario,referenceRows:state.original.length,syntheticRows:state.synthetic.length},parameters:{qis:state.qis,sensitive:state.sensitive,seed:state.seed,blend:state.blend,epsilon:state.epsilon,noiseSeed:state.noiseSeed},privacy:{minimumK:p.minK,groups:p.groupCount,atRiskRows:p.riskRows,atRiskPct:p.riskPct,lowDiversityPct:p.lFailPct,highDisparityPct:p.tFailPct},utility:{score:u.qualityScore,averageCategoricalTVD:u.avgCat,meanSpendGap:u.meanGap,exactRowOverlapPct:u.exactPct,categoricalByFeature:u.fields,bySegment:u.segments},noise:{field:n.field,epsilon:n.epsilon,averageAbsoluteError:n.meanAbsoluteCountError,bars:n.bars},limitations:['Not an anonymity certification','Synthetic generator does not provide differential privacy','Seeded Laplace simulation is not secure private randomness','All metrics are descriptive and depend on input schema and assumptions']};}
function save(name,text,type='text/plain'){const blob=new Blob([text],{type}),url=URL.createObjectURL(blob),a=document.createElement('a');a.href=url;a.download=name;document.body.appendChild(a);a.click();a.remove();setTimeout(()=>URL.revokeObjectURL(url),1000);toast(`${name} downloaded`)}
function toast(message){let t=$('#toast');t.textContent=message;t.classList.add('show');clearTimeout(toast.id);toast.id=setTimeout(()=>t.classList.remove('show'),2900)}
function render(){const p=privacyAudit(state.original,state.qis,state.sensitive),u=utilityAudit(state.original,state.synthetic);let content={overview:()=>overview(p,u),privacy:()=>privacy(p),synthetic:()=>syntheticView(p,u),utility:()=>utility(p,u),noise, data}[state.page]();
$('#dashboard').innerHTML=content;document.querySelectorAll('.nav-link').forEach(x=>x.classList.toggle('active',x.dataset.page===state.page));
const titles={overview:['Command center','Your privacy posture and synthetic-data quality, in one workspace.'],privacy:['Privacy audit','Explore quasi-identifier group sizes, sensitive diversity and distribution shift.'],synthetic:['Synthetic studio','Generate test data and inspect the structure learned from your source records.'],utility:['Utility benchmark','Measure statistical similarities and trade-offs without hiding privacy risk.'],noise:['Noise simulator','Explore seeded Laplace noise and its limitations.'],data:['Data & methods','Work with local CSV files and export reproducible audit summaries.']};
$('#viewHeading').querySelector('h2').textContent=titles[state.page][0];$('#viewHeading').querySelector('p').textContent=titles[state.page][1];
const s=SCENARIOS.find(s=>s.id===state.scenario);$('#datasetLabel').textContent=state.source==='demo'?`${s.label} Β· Synthetic demo`:'User-imported local CSV';$('#scenarioSelect').value=state.scenario;$('#scenarioSelect').disabled=false;}
function go(page){state.page=page;render();$('#sidebar').classList.remove('open');window.scrollTo?.({left:0,top:370,behavior:'smooth'});}
document.addEventListener('click',event=>{let btn=event.target.closest('[data-page],[data-action]');if(!btn)return;
if(btn.dataset.page)return go(btn.dataset.page);try{switch(btn.dataset.action){
case'generate':{const count=Number($('#sampleCount')?.value),seed=Number($('#seedInput')?.value),blend=Number($('#blendInput')?.value);const rows=makeSynthetic(state.original,count,seed,blend);state.count=count;state.seed=seed;state.blend=blend;state.synthetic=rows;render();toast('Synthetic dataset regenerated');break}
case'run-noise':{let epsilon=Number($('#epsilonSlider').value),noiseSeed=Number($('#noiseSeed').value),noiseField=$('#noiseField').value;noisyHistogram(state.original,noiseField,epsilon,noiseSeed);Object.assign(state,{epsilon,noiseSeed,noiseField});render();toast('Noise experiment updated');break}
case'download-report':save('synthshield_audit.json',JSON.stringify(report(),null,2),'application/json');break;
case'download-markdown':{let r=report(),lines=[`# SynthShield research audit`,``,`Generated: ${r.createdAt}`,`Dataset: ${r.data.source}, ${r.data.referenceRows} records`,``,`## Privacy screening`,`- Selected quasi-identifiers: ${r.parameters.qis.join(', ')}`,`- Minimum k: ${r.privacy.minimumK}`,`- Rows in groups below k=5: ${percent(r.privacy.atRiskPct)}`,`- Rows with low outcome diversity: ${percent(r.privacy.lowDiversityPct)}`,`- Rows with high distribution disparity: ${percent(r.privacy.highDisparityPct)}`,``,`## Synthetic utility`,`- Illustrative fidelity score: ${r.utility.score.toFixed(1)} / 100`,`- Mean categorical TVD: ${dec(r.utility.averageCategoricalTVD)}`,`- Relative spend mean gap: ${percent(100*r.utility.meanSpendGap)}`,``,`## Limitations`,...r.limitations.map(x=>'- '+x),``,`Synthetic and source data are not included in the audit report.`];save('synthshield_research_memo.md',lines.join('\n'),'text/markdown');break}
case'download-synthetic':save('synthshield_synthetic.csv',toCSV(state.synthetic),'text/csv;charset=utf-8');break;
case'download-template':save('synthshield_template.csv',FIELDS.join(',')+'\r\n25-34,North,Core,Web,120.50,Retained\r\n35-44,South,Growth,Mobile,95.00,Churned\r\n','text/csv;charset=utf-8');break;
case'reset-demo':state.original=exampleRows(state.scenario);state.source='demo';state.synthetic=makeSynthetic(state.original,state.count,state.seed,state.blend);render();toast('Demonstration data restored');break;
}}catch(e){toast('Action failed: '+e.message)}});
document.addEventListener('change',async event=>{const el=event.target;if(el.dataset.qi){let selected=Array.from(document.querySelectorAll('[data-qi]:checked')).map(x=>x.dataset.qi);if(!selected.length){el.checked=true;toast('Select at least one quasi-identifier');return}state.qis=selected;render();return}
if(el.id==='scenarioSelect'){state.scenario=el.value;state.source='demo';state.original=exampleRows(state.scenario);state.synthetic=makeSynthetic(state.original,state.count,state.seed,state.blend);render();toast('Demonstration dataset changed');return}
if(el.id==='fileInput'){let file=el.files?.[0];if(!file)return;try{if(file.size>2_000_000)throw Error('File exceeds 2 MB');let text=await file.text(),rows=parseCSV(text);let synthetic=makeSynthetic(rows,state.count,state.seed,state.blend);state.original=rows;state.synthetic=synthetic;state.source='imported';render();toast(`${rows.length} valid CSV records imported`);}catch(e){toast('Import rejected: '+e.message);el.value=''}return}
});
$('#mobileMenu').addEventListener('click',()=>$('#sidebar').classList.toggle('open'));
render();
</script>
</body></html>