Hugdrigo's picture
Upload 3 files
47402ab verified
Raw History Blame Contribute Delete
40.5 kB
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta name="color-scheme" content="dark">
<title>PrivacyForge | Data Privacy Intelligence Studio</title>
<style>
:root{--bg:#080f1e;--panel:#0e1a2c;--panel2:#132238;--line:#263751;--text:#eff7ff;--muted:#9faec2;--green:#76f0c5;--sky:#76bdfa;--yellow:#ffda8c;--red:#ffa2ac;--shadow:0 14px 44px #03081350}
*{box-sizing:border-box}html,body{margin:0;min-height:100%;background:var(--bg);color:var(--text);font-family:Inter,ui-sans-serif,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif}button,input,select{font:inherit}button{cursor:pointer}a{color:var(--green)}.app{display:flex;min-height:100vh}.side{width:258px;background:#0c1627;border-right:1px solid var(--line);padding:27px 17px;position:sticky;top:0;max-height:100vh;flex-shrink:0;display:flex;flex-direction:column;gap:16px}.brand{display:flex;align-items:center;gap:11px;padding:0 8px 16px}.logo{display:grid;place-items:center;width:39px;height:39px;border-radius:12px;background:linear-gradient(135deg,#81f6bd,#50acff);color:#092036;font-size:22px;font-weight:900}.brand strong{display:block;font-size:19px;letter-spacing:-.5px}.brand small{color:var(--muted);font-size:10px;letter-spacing:1.5px;text-transform:uppercase}.nav{display:flex;flex-direction:column;gap:5px}.nav button{width:100%;text-align:left;border:1px solid transparent;border-radius:10px;padding:12px 10px;background:transparent;color:#aebed0;font-size:13px;display:flex;align-items:center;gap:10px}.nav button span{font-size:17px;width:24px;text-align:center}.nav button.active,.nav button:hover{color:#eafff5;background:#1a3441;border-color:#315a5f}.sidefoot{margin-top:auto;border:1px solid #315c60;border-radius:12px;padding:14px;background:#132c35;color:#c4e5d9;font-size:12px;line-height:1.5}.sidefoot strong{color:var(--green)}main{flex:1;min-width:0;max-width:1600px;padding:30px 35px 60px}.top{display:flex;justify-content:space-between;gap:20px;align-items:center;flex-wrap:wrap;margin-bottom:25px}.eyebrow{color:var(--green);font-size:11px;text-transform:uppercase;letter-spacing:1.8px;font-weight:800}.top h1{margin:7px 0 5px;font-size:32px;letter-spacing:-1.1px}.top p{margin:0;color:var(--muted);font-size:13px;line-height:1.6}.tools{display:flex;flex-wrap:wrap;gap:8px;align-items:flex-end}.field{display:flex;flex-direction:column;gap:5px;color:var(--muted);font-size:11px;font-weight:650}.field select,.field input{border:1px solid #344763;color:var(--text);background:#101e31;padding:10px 12px;border-radius:9px;max-width:220px}.selectwide select{width:210px}.tag{display:inline-block;border:1px solid #365367;border-radius:999px;color:#bbd7e1;padding:5px 10px;font-size:11px;font-weight:750}.banner{padding:13px 16px;border:1px solid #3a585a;border-left:3px solid var(--green);border-radius:10px;background:#142936;display:flex;gap:12px;align-items:center;margin:0 0 22px;font-size:12px;color:#c6dfe0;line-height:1.5}.banner b{color:#dffff1}.heading{display:flex;align-items:center;justify-content:space-between;gap:15px;margin:26px 0 13px;flex-wrap:wrap}.heading h2{font-size:18px;letter-spacing:-.35px;margin:0}.heading p{margin:4px 0 0;color:var(--muted);font-size:12px}.grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:14px}.panel{border:1px solid var(--line);border-radius:15px;background:var(--panel);padding:19px;box-shadow:var(--shadow);min-width:0}.metric{padding:20px}.metric small{display:block;color:var(--muted);font-size:12px}.metric strong{display:block;font-size:31px;letter-spacing:-1.2px;margin:11px 0 4px;font-variant-numeric:tabular-nums}.metric p{font-size:11px;color:var(--muted);line-height:1.4;margin:0}.green{color:var(--green)}.yellow{color:var(--yellow)}.red{color:var(--red)}.sky{color:var(--sky)}.two{display:grid;grid-template-columns:1.1fr .9fr;gap:14px}.three{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:14px}.panel h3{font-size:14px;margin:0 0 8px}.panel .subtitle{font-size:11px;color:var(--muted);margin:0 0 16px;line-height:1.5}.chartrow{display:grid;grid-template-columns:120px minmax(0,1fr) 58px;gap:10px;align-items:center;margin:14px 0;font-size:12px}.chartrow .track{height:13px;background:#23344a;border-radius:99px;overflow:hidden}.chartrow .bar{height:100%;border-radius:99px;background:linear-gradient(90deg,#49b4c7,#78ecc3)}.chartrow strong{text-align:right;font-variant-numeric:tabular-nums}.mini{font-size:11px;color:var(--muted)}.split{display:flex;gap:8px;align-items:center;flex-wrap:wrap}.pill{padding:5px 9px;border:1px solid #3a555e;border-radius:20px;font-size:11px;color:#ace7d9;background:#193239}.btn{background:#233b50;border:1px solid #3b5872;color:#eaffff;border-radius:9px;padding:10px 14px;font-size:12px;font-weight:750}.btn:hover{background:#2c5063}.btn.primary{color:#07271f;background:var(--green);border-color:#a4fbd9}.btn.primary:hover{background:#a1fbdc}.btn.ghost{background:transparent}.btn:disabled{opacity:.4;cursor:not-allowed}.control{display:flex;flex-direction:column;gap:7px;margin:15px 0;font-size:12px}.control label{display:flex;justify-content:space-between;gap:10px;color:#d3e3ee;font-weight:650}.control .hint{font-size:11px;color:var(--muted)}.control select,.control input[type=number],.control input[type=range]{width:100%}.control select,.control input[type=number]{background:#102137;border:1px solid #36506d;border-radius:8px;padding:10px;color:var(--text)}input[type=range]{accent-color:#70edc0}input[type=checkbox]{accent-color:var(--green)}.tablewrap{overflow:auto;max-width:100%}table{border-collapse:collapse;width:100%;font-size:12px;white-space:nowrap}th{text-transform:uppercase;font-size:10px;letter-spacing:.7px;color:#b3c5d9;text-align:left;background:#1b2a40}th,td{padding:11px 12px;border-bottom:1px solid #283b51}tbody tr:hover{background:#16293b}td{color:#dbe6ef}td strong{color:#fff}.status{font-size:10px;font-weight:750;padding:4px 7px;border-radius:5px;white-space:nowrap}.status.good{background:#123c36;color:#84f4c8}.status.bad{background:#542839;color:#ffb2c1}.status.warn{background:#493b2b;color:#ffe1a3}.note{font-size:12px;color:#c3d3e4;background:#14263d;border:1px solid #30475f;border-radius:10px;padding:13px 15px;line-height:1.55;margin:14px 0}.callout{color:#c5e7e5;border-left:3px solid var(--sky);padding:11px 14px;background:#10283a;font-size:12px;line-height:1.55}.progress{height:9px;background:#263950;border-radius:99px;overflow:hidden}.progress div{height:100%;background:var(--green);border-radius:99px}.kpirow{display:flex;gap:16px;flex-wrap:wrap}.kpirow div{flex:1;min-width:100px}.kpirow strong{font-size:21px;display:block;margin:6px 0}.kpirow small{color:var(--muted)}.section{display:none}.section.active{display:block}.upload{padding:26px;border:1px dashed #5d8096;border-radius:13px;text-align:center;color:#aacee4;background:#10273b;display:flex;flex-direction:column;gap:12px;align-items:center}.upload input{max-width:100%;font-size:12px}.exportgrid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px}.exportgrid button{text-align:left}.result{margin-top:12px;font-size:12px;min-height:20px;color:var(--green)}.footer{border-top:1px solid var(--line);margin-top:34px;padding:18px 0;color:#869caf;font-size:11px;line-height:1.6}.mobile-nav{display:none}.w100{width:100%} .mt{margin-top:14px}.mb{margin-bottom:14px}.smallcard{padding:13px;border:1px solid #274153;border-radius:10px;background:#12273a}.smallcard strong{font-size:13px;display:block}.smallcard span{font-size:11px;color:var(--muted)}.inlinecontrols{display:flex;gap:10px;align-items:center;flex-wrap:wrap}.inlinecontrols select{background:#15283d;border:1px solid #38516d;color:var(--text);border-radius:8px;padding:8px}.code{font-family:ui-monospace,monospace;font-size:11px;background:#1c2e40;padding:2px 5px;border-radius:3px;word-break:break-all}.tabline{font-size:12px;color:var(--muted)}.hide{display:none!important}svg text{fill:#a7bcd1;font-family:inherit;font-size:11px}.barlegend{display:flex;gap:13px;font-size:11px;color:var(--muted)}.dot{display:inline-block;width:8px;height:8px;border-radius:99px;margin-right:5px;background:var(--green)}
@media(max-width:1180px){main{padding:25px 22px}.grid{grid-template-columns:repeat(2,minmax(0,1fr))}.two{grid-template-columns:1fr}.three{grid-template-columns:1fr 1fr}}
@media(max-width:750px){.app{display:block}.side{display:none}.mobile-nav{display:flex;gap:6px;overflow-x:auto;padding:12px 14px;background:#101a2b;border-bottom:1px solid var(--line);position:sticky;top:0;z-index:5}.mobile-nav button{white-space:nowrap;border:1px solid #33455a;border-radius:8px;background:#13243a;color:#b5d0df;padding:9px 12px;font-size:11px}.mobile-nav button.active{color:#07231d;background:var(--green)}main{padding:20px 15px 35px}.top h1{font-size:27px}.top p{font-size:12px}.tools{width:100%;justify-content:space-between}.selectwide select{max-width:100%;width:100%}.field.selectwide{flex:1}.grid{gap:9px}.panel{padding:14px}.metric{padding:15px}.metric strong{font-size:25px}.three{grid-template-columns:1fr}.exportgrid{grid-template-columns:1fr}.chartrow{grid-template-columns:94px 1fr 50px;gap:6px;font-size:11px}}
</style>
</head>
<body>
<div class="app"><aside class="side"><div class="brand"><div class="logo">◈</div><div><strong>PrivacyForge</strong><small>DATA PRIVACY STUDIO</small></div></div><div class="nav" id="nav"></div><div class="sidefoot"><strong>● Local processing</strong><br>Files remain in your browser. Export your work before closing. Demo records are synthetic.</div></aside>
<div style="min-width:0;flex:1"><div class="mobile-nav" id="mobileNav"></div><main>
<header class="top"><div><div class="eyebrow">Hugdrigo research labs / project 75</div><h1 id="pageTitle">Privacy intelligence</h1><p id="pageDesc">Measure disclosure risks, explore anonymization and audit the trade-offs.</p></div><div class="tools"><label class="field selectwide">DATASET<select id="dataset"></select></label><label class="field">TARGET k<select id="topK"><option>2</option><option selected>3</option><option>4</option><option>5</option><option>6</option></select></label><label class="field">TARGET l<select id="topL"><option>1</option><option selected>2</option><option>3</option></select></label></div></header>
<div class="banner"><span style="font-size:19px">◈</span><div><b>Privacy is not a binary certification.</b> k-anonymity and distinct l-diversity identify particular disclosure risks, but do not protect against all linkage, inference or background-knowledge attacks. Only synthetic records are included.</div></div>
<section class="section active" id="overview"><div class="grid" id="metrics"></div><div class="heading"><div><h2>Disclosure surface</h2><p>Baseline and transformed equivalence groups, recalculated from source records.</p></div><span class="tag" id="overviewTag"></span></div><div class="two"><div class="panel"><h3>Equivalence class distribution</h3><p class="subtitle">Number of records belonging to groups of each size. Smaller groups are easier to single out.</p><div id="sizeBars"></div><div class="barlegend"><span><i class="dot"></i>Transformed retained records</span></div></div><div class="panel"><h3>Privacy profile</h3><p class="subtitle">Check exposure, retention and generalization side by side.</p><div id="summaryRows"></div><div class="note" id="topWarning"></div></div></div><div class="heading"><h2>Recommended next step</h2></div><div class="panel"><div id="recommendSummary"></div><div class="split mt"><button class="btn primary" data-jump="planner">Open privacy planner →</button><button class="btn ghost" data-jump="data">Import my dataset</button></div></div></section>
<section class="section" id="transform"><div class="heading"><div><h2>Generalization & suppression</h2><p>Configure transformations for quasi-identifiers and inspect actual retained records.</p></div><div class="split"><button id="resetLevels" class="btn ghost">Reset levels</button><button id="planNow" class="btn primary">Apply best feasible plan</button></div></div><div class="two"><div class="panel"><h3>Transformation controls</h3><div id="controls"></div><div class="note">Direct identifiers are never included in anonymized CSV downloads. Suppression removes entire records from export when their equivalence group fails either target.</div></div><div class="panel"><h3>Preview & quality checks</h3><div id="transformKpis" class="kpirow"></div><div class="mt" id="transformStatus"></div><div class="heading"><h2>Preview (first 8 retained rows)</h2></div><div class="tablewrap" id="transformTable"></div></div></div></section>
<section class="section" id="groups"><div class="heading"><div><h2>Equivalence class explorer</h2><p>Every class is defined by the current generalized quasi-identifiers.</p></div><div class="inlinecontrols"><label class="field">Show<select id="groupFilter"><option value="all">All groups</option><option value="fail">Below targets</option><option value="pass">Meets targets</option></select></label></div></div><div class="grid" id="groupMetrics"></div><div class="panel mt"><div class="tablewrap" id="groupTable"></div></div><div class="note">Distinct l-diversity counts different values of the sensitive outcome per group. A group that meets k and l may still leak information about an individual.</div></section>
<section class="section" id="planner"><div class="heading"><div><h2>Privacy optimization laboratory</h2><p>Exhaustively evaluates all 36 global generalization combinations under the selected privacy targets.</p></div><button class="btn primary" id="applyPlan">Apply lowest-loss feasible plan</button></div><div class="two"><div class="panel"><h3>Optimization limits</h3><div class="control"><label>Maximum suppressed records <strong id="suppressLabel"></strong></label><input id="suppressSlider" type="range" min="0" max="40" step="5" value="20"><div class="hint">Suppression allowance is a percentage of the original dataset.</div></div><div class="control"><label>Suppression penalty <strong id="penaltyLabel"></strong></label><input id="penaltySlider" type="range" min="10" max="100" step="5" value="75"><div class="hint">Higher values strongly discourage removal of records.</div></div><div id="planOutcome" class="note"></div></div><div class="panel"><h3>Search coverage</h3><div class="kpirow" id="planKpis"></div><div class="note">Every combination is assessed deterministically. Ranking minimizes weighted generalization loss plus a penalty for suppressed records. This is an exact search of the stated 36 options, not an optimality claim over all possible anonymization methods.</div></div></div><div class="heading"><h2>Top feasible configurations</h2></div><div class="panel"><div class="tablewrap" id="planTable"></div></div></section>
<section class="section" id="utility"><div class="heading"><div><h2>Utility & representation</h2><p>Measure lost records, information coarsening and possible distortion of aggregate statistics.</p></div></div><div class="grid" id="utilityMetrics"></div><div class="two mt"><div class="panel"><h3>Outcome composition</h3><p class="subtitle">Outcome distribution before and after suppression. Different proportions can signal selection bias.</p><div id="outcomeBars"></div></div><div class="panel"><h3>Information preserved</h3><p class="subtitle">Illustrative attribute-level fidelity score, not a guarantee of research utility.</p><div id="fidelity"></div><div class="note">The age variable is only generalized. IDs are removed entirely. Averages computed on retained records can differ from original averages due to suppression.</div></div></div><div class="heading"><h2>Methodological limits</h2></div><div class="panel" style="line-height:1.7;font-size:12px;color:#c4d5e5"><b>k-anonymity</b> limits unique quasi-identifier combinations within the released table. <b>Distinct l-diversity</b> requires at least l distinct outcomes in each retained equivalence class. Neither offers a formal guarantee of differential privacy. The tool does not model attacker background knowledge, linkage to external records, t-closeness, membership disclosure or inferential attacks. Use only de-identified, authorized records for demonstration and obtain specialist review for any real disclosure.</div></section>
<section class="section" id="data"><div class="heading"><div><h2>Data workbench & audit exports</h2><p>Import local data and export transformed results or a reproducible configuration.</p></div><span class="tag">Browser-only processing</span></div><div class="two"><div class="panel"><h3>Import CSV</h3><p class="subtitle">Required columns: id, age, region, role, outcome. Maximum 5,000 rows and 1 MB. Existing data remains intact if validation fails.</p><label class="upload"><span style="font-size:27px">⇧</span><strong>Select a CSV on your computer</strong><input type="file" id="file" accept=".csv,text/csv"></label><div class="result" id="importResult" role="status"></div><div class="note">Imported data is read locally; no upload request is made by this application. However, avoid placing actual personal or confidential information into a public-facing demo application. Changes are not saved between sessions unless exported.</div></div><div class="panel"><h3>Downloads</h3><p class="subtitle">Generate local, reproducible research outputs. Anonymized CSV intentionally excludes direct IDs and suppressed rows.</p><div class="exportgrid"><button class="btn" data-download="anon">↓ Anonymized CSV</button><button class="btn" data-download="groups">↓ Class audit CSV</button><button class="btn" data-download="report">↓ Audit report JSON</button><button class="btn" data-download="markdown">↓ Methodology report</button><button class="btn" data-download="config">↓ Configuration JSON</button><button class="btn" data-download="template">↓ CSV template</button></div><div class="result" id="exportResult" role="status"></div></div></div><div class="heading"><h2>Source data checks</h2></div><div class="panel"><div class="kpirow" id="dataKpis"></div><div class="heading"><h2>Current dataset fields</h2></div><div class="tablewrap" id="dataSchema"></div></div></section>
<div class="footer">PrivacyForge v1.0 · Hugdrigo · Project #75 · MIT License · Research and educational demonstration only · No external libraries, analytics or API calls.</div>
</main></div></div>
<script>
'use strict';
const Core=(()=>{
const X=(s)=>String(s??'').replace(/[&<>"']/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
function seedRand(seed){let x=seed>>>0;return ()=>{x=(Math.imul(1664525,x)+1013904223)>>>0;return x/4294967296}};
function synthetic(label,seed,n){const r=seedRand(seed),regions=['North','South','East','West','Central','Coastal'],roles=['Analyst','Coordinator','Operator','Researcher','Clerk','Supervisor'],outcomes=label==='Community library'?['Active','Inactive','New']:label==='Business training'?['Completed','Pending','Withdrawn']:['Approved','Pending','Rejected'];const rows=[];for(let i=0;i<n;i++){const age=22+Math.floor(r()*42),region=regions[Math.floor(r()*regions.length)],role=roles[Math.floor(r()*roles.length)];let z=r();const outcome=outcomes[z<.48?0:z<.80?1:2];rows.push({id:`D${seed}-${String(i+1).padStart(3,'0')}`,age,region,role,outcome})}return rows}
function genAge(age,level){if(level===0)return String(age);let w=[0,5,10,20][level],min=Math.floor(age/w)*w;return `${min}–${min+w-1}`}
function genRegion(v,level){if(level===0)return v;if(level===2)return '*';return {'North':'North zone','East':'North zone','Central':'North zone','South':'South zone','West':'South zone','Coastal':'South zone'}[v]||'Other zone'}
function genRole(v,level){if(level===0)return v;if(level===2)return '*';return ['Analyst','Researcher','Coordinator'].includes(v)?'Knowledge work':'Service operations'}
function transformed(r,levels){return {age:genAge(r.age,levels.age),region:genRegion(r.region,levels.region),role:genRole(r.role,levels.role),outcome:r.outcome}}
function groupKey(r){return JSON.stringify([r.age,r.region,r.role])}
function analyze(rows,levels,k=3,l=2){const all=rows.map(r=>({...transformed(r,levels),_source:r.id}));const groups=new Map();for(const r of all){const key=groupKey(r);if(!groups.has(key))groups.set(key,{key,age:r.age,region:r.region,role:r.role,records:[],outcomes:new Set()});let g=groups.get(key);g.records.push(r);g.outcomes.add(r.outcome)}const classList=[...groups.values()].map(g=>({key:g.key,age:g.age,region:g.region,role:g.role,count:g.records.length,l:g.outcomes.size,valid:g.records.length>=k&&g.outcomes.size>=l,records:g.records})).sort((a,b)=>a.count-b.count||a.key.localeCompare(b.key));const kept=classList.filter(g=>g.valid).flatMap(g=>g.records);const suppressed=rows.length-kept.length;let minK=kept.length?Math.min(...classList.filter(g=>g.valid).map(g=>g.count)):0,minL=kept.length?Math.min(...classList.filter(g=>g.valid).map(g=>g.l)):0;const bad=classList.filter(g=>!g.valid).reduce((n,g)=>n+g.count,0);const distortion=([0,.27,.56,.95][levels.age]+[0,.55,1][levels.region]+[0,.5,1][levels.role])/3;
const retention=rows.length?kept.length/rows.length:0;
const information=retention*(1-distortion);
return {levels:{...levels},total:rows.length,all,classList,kept,suppressed,retention,bad,exposed:rows.length?bad/rows.length:0,minK,minL,distortion,information,valid:kept.length>0&&minK>=k&&minL>=l};}
function allPlans(rows,k,l,maxSupp=.2,penalty=.75){let plans=[];for(let a=0;a<=3;a++)for(let b=0;b<=2;b++)for(let c=0;c<=2;c++){const levels={age:a,region:b,role:c},s=analyze(rows,levels,k,l),supp=s.suppressed/s.total;const feasible=s.valid&&supp<=maxSupp+1e-9;plans.push({...s,feasible,score:s.distortion*s.retention+supp*penalty,suppRate:supp})}plans.sort((a,b)=>Number(b.feasible)-Number(a.feasible)||a.score-b.score||b.kept.length-a.kept.length||a.distortion-b.distortion);return plans}
function csvParse(text){let out=[],row=[],cell='',inside=false,closed=false;const push=()=>{row.push(cell);cell='';closed=false};for(let i=0;i<text.length;i++){const c=text[i];if(inside){if(c==='"'&&text[i+1]==='"'){cell+='"';i++}else if(c==='"'){inside=false;closed=true}else cell+=c}else if(c==='"'){if(closed||cell.length)throw Error('Unexpected quote at character '+i);inside=true}else if(c===','){push()}else if(c==='\n'||c==='\r'){if(c==='\r'&&text[i+1]==='\n')i++;push();if(row.some(v=>v.length))out.push(row);row=[]}else{if(closed&&c!==' '&&c!=='\t')throw Error('Unexpected text after a closing quote');if(!closed)cell+=c}}if(inside)throw Error('Unclosed quoted field');if(cell.length||row.length){push();if(row.some(v=>v.length))out.push(row)}return out}
function readCSV(text){const rows=csvParse(text.replace(/^\uFEFF/,''));if(rows.length<2)throw Error('CSV requires a header and at least one record');const head=rows.shift().map(x=>x.trim().toLowerCase());const need=['id','age','region','role','outcome'];if(head.length!==new Set(head).size||!need.every(x=>head.includes(x)))throw Error('Required columns: id, age, region, role, outcome');if(rows.length>5000)throw Error('5,000-row limit exceeded');const ids=new Set();return rows.map((vals,i)=>{if(vals.length!==head.length)throw Error('Wrong column count on row '+(i+2));const o=Object.fromEntries(head.map((h,j)=>[h,vals[j].trim()]));if(!o.id||ids.has(o.id))throw Error('Missing or duplicate ID on row '+(i+2));ids.add(o.id);const age=Number(o.age);if(!Number.isInteger(age)||age<16||age>110)throw Error('Invalid age on row '+(i+2));for(const key of ['region','role','outcome']){if(!o[key]||o[key].length>90)throw Error('Invalid '+key+' on row '+(i+2))}return {id:o.id,age,region:o.region,role:o.role,outcome:o.outcome}})}
function escCsv(v){let s=String(v??'');if(/^[\s]*[=+@-]/.test(s))s="'"+s;return '"'+s.replace(/"/g,'""')+'"'}
function toCSV(rows,cols){return [cols.map(escCsv).join(','),...rows.map(r=>cols.map(c=>escCsv(r[c])).join(','))].join('\r\n')+'\r\n'}
function counts(arr,key){const m={};for(const r of arr)m[r[key]]=(m[r[key]]||0)+1;return m}
return {X,synthetic,genAge,genRegion,genRole,transformed,analyze,allPlans,csvParse,readCSV,toCSV,counts};})();
const SECTIONS=[['overview','◫','Command center','Understand disclosure risk and information retained'],['transform','⚙','Transform studio','Generalization controls and anonymized row previews'],['groups','▦','Class explorer','Audit every equivalence group and its diversity'],['planner','◈','Privacy planner','Search all 36 global transformations under your targets'],['utility','◧','Utility diagnostics','Compare retained data, distortion and distributions'],['data','⇩','Data & exports','Import local CSV and download reproducible audits']];
const DEMOS={'Training cohort':Core.synthetic('Business training',713,128),'Community library':Core.synthetic('Community library',219,110),'Service requests':Core.synthetic('Service requests',905,144)};
const state={name:'Training cohort',rows:DEMOS['Training cohort'],levels:{age:1,region:1,role:1},k:3,l:2,maxSupp:20,penalty:75,section:'overview',groupFilter:'all',custom:null};
const $=id=>document.getElementById(id);const X=Core.X;
function fmt(n,d=0){return Number(n).toLocaleString('en-US',{maximumFractionDigits:d,minimumFractionDigits:d})}function pct(n,d=1){return fmt(n*100,d)+'%'}
function stat(){return Core.analyze(state.rows,state.levels,state.k,state.l)}
function plans(){return Core.allPlans(state.rows,state.k,state.l,state.maxSupp/100,state.penalty/100)}
function metric(title,val,detail,color=''){return `<article class="panel metric"><small>${title}</small><strong class="${color}">${val}</strong><p>${detail}</p></article>`}
function headingTag(ok){return `<span class="status ${ok?'good':'bad'}">${ok?'MEETS TARGETS':'BELOW TARGETS'}</span>`}
function table(cols,rows){return `<table><thead><tr>${cols.map(x=>`<th>${X(x)}</th>`).join('')}</tr></thead><tbody>${rows.join('')||`<tr><td colspan="${cols.length}">No records match these filters.</td></tr>`}</tbody></table>`}
function setNav(){for(const id of ['nav','mobileNav'])$(id).innerHTML=SECTIONS.map(([key,ico,label])=>`<button data-section="${key}" class="${state.section===key?'active':''}"><span>${ico}</span>${label}</button>`).join('');for(const b of document.querySelectorAll('[data-section]'))b.addEventListener('click',()=>jump(b.dataset.section))}
function jump(id){state.section=id;for(const s of document.querySelectorAll('.section'))s.classList.toggle('active',s.id===id);$('pageTitle').textContent=SECTIONS.find(x=>x[0]===id)[2];$('pageDesc').textContent=SECTIONS.find(x=>x[0]===id)[3];setNav();render();if(window.scrollY>150)window.scrollTo({top:0,behavior:'instant'})}
function render(){const s=stat(),p=plans();renderOverview(s,p);renderTransform(s);renderGroups(s);renderPlanner(s,p);renderUtility(s);renderData(s);for(const b of document.querySelectorAll('[data-jump]'))b.onclick=()=>jump(b.dataset.jump)}
function renderOverview(s,p){$('metrics').innerHTML=metric('Original records',fmt(s.total),'Synthetic / imported source rows')+metric('Records retained',pct(s.retention),'After group suppression',s.retention>=.8?'green':'yellow')+metric('Suppression rate',pct(s.suppressed/s.total),'Rows removed from released data',s.suppressed?'yellow':'green')+metric('Minimum retained k / l',`${s.minK} / ${s.minL}`,`Requested: k ≥ ${state.k}, l ≥ ${state.l}`,s.valid?'green':'red');$('overviewTag').textContent=state.name.toUpperCase();const groupHist={};for(const g of s.classList.filter(x=>x.valid))groupHist[g.count]=(groupHist[g.count]||0)+g.count;const mx=Math.max(1,...Object.values(groupHist));$('sizeBars').innerHTML=Object.keys(groupHist).sort((a,b)=>Number(a)-Number(b)).map(key=>`<div class="chartrow"><span>Class size ${X(key)}</span><div class="track"><div class="bar" style="width:${100*groupHist[key]/mx}%"></div></div><strong>${groupHist[key]}</strong></div>`).join('')||'<p class="mini">No retained classes. Increase generalization or lower privacy targets.</p>';$('summaryRows').innerHTML=[['Original group count',s.classList.length],['Groups failing targets',s.classList.filter(x=>!x.valid).length],['Original records in failing groups',s.bad],['Attribute fidelity before suppression',pct(1-s.distortion)],['Overall data preservation',pct(s.information)]].map(([k,v])=>`<div class="chartrow" style="grid-template-columns:1fr auto"><span>${k}</span><strong>${v}</strong></div>`).join('');$('topWarning').innerHTML= s.valid?`<span class="green"><b>Targets satisfied among retained records.</b></span> That does not guarantee anonymity or non-identification.`:'<span class="red"><b>Not yet publishable under the selected criteria.</b></span> Current transformation either suppresses every record or does not produce a valid retained set.';const best=p.find(z=>z.feasible);$('recommendSummary').innerHTML=best?`<h3>Best feasible option within configured limits</h3><p class="subtitle">Age level ${best.levels.age}, region level ${best.levels.region}, role level ${best.levels.role}. Retains <b class="green">${best.kept.length} / ${best.total}</b> rows, with <b>${pct(best.information)}</b> combined preservation score.</p>`:'<h3>No feasible plan at current settings</h3><p class="subtitle">Increase the allowed suppression rate, reduce the privacy targets or import a dataset with more diverse groups.</p>'}
function renderTransform(s){const fields=[['age','Age bins',['Exact age','Five-year band','Ten-year band','Twenty-year band']],['region','Region',['Exact region','Combined zone','All regions (*)']],['role','Occupational role',['Exact role','Grouped roles','All roles (*)']]];$('controls').innerHTML=fields.map(([f,label,opts])=>`<div class="control"><label>${label}<span class="pill">Level ${state.levels[f]}</span></label><select data-level="${f}">${opts.map((x,i)=>`<option value="${i}" ${i===state.levels[f]?'selected':''}>${X(x)}</option>`).join('')}</select></div>`).join('');for(const sel of document.querySelectorAll('[data-level]'))sel.onchange=()=>{state.levels[sel.dataset.level]=+sel.value;render()};$('transformKpis').innerHTML=[['Retained',pct(s.retention)],['Suppressed',s.suppressed],['Attribute fidelity',pct(1-s.distortion)]].map(([a,b])=>`<div><small>${a}</small><strong>${b}</strong></div>`).join('');$('transformStatus').innerHTML=headingTag(s.valid);$('transformTable').innerHTML=table(['Age','Region','Role','Outcome'],s.kept.slice(0,8).map(r=>`<tr><td>${X(r.age)}</td><td>${X(r.region)}</td><td>${X(r.role)}</td><td>${X(r.outcome)}</td></tr>`))}
function renderGroups(s){const failing=s.classList.filter(g=>!g.valid);$('groupMetrics').innerHTML=metric('Equivalence classes',s.classList.length,'Before suppression')+metric('Failing classes',failing.length,'Groups failing k or l',failing.length?'red':'green')+metric('Rows in failing classes',s.bad,'Suppressed on export',s.bad?'yellow':'green')+metric('Retained class count',s.classList.length-failing.length,'Groups eligible to release');const classes=s.classList.filter(g=>state.groupFilter==='all'||(state.groupFilter==='fail'?!g.valid:g.valid));$('groupTable').innerHTML=table(['Age','Region','Role','Records (k)','Distinct outcomes (l)','Status'],classes.slice(0,160).map(g=>`<tr><td>${X(g.age)}</td><td>${X(g.region)}</td><td>${X(g.role)}</td><td><strong>${g.count}</strong></td><td>${g.l}</td><td>${headingTag(g.valid)}</td></tr>`))}
function renderPlanner(s,p){$('suppressLabel').textContent=state.maxSupp+'%';$('penaltyLabel').textContent=state.penalty+'%';const good=p.filter(z=>z.feasible),best=good[0];$('planKpis').innerHTML=[['Combinations',p.length],['Feasible',good.length],['Best fidelity',best?pct(best.information):'None']].map(([k,v])=>`<div><small>${k}</small><strong>${v}</strong></div>`).join('');$('planOutcome').innerHTML=best?`<b class="green">Feasible strategy found.</b><br>It suppresses ${best.suppressed} of ${best.total} rows (${pct(best.suppRate)}) and achieves retained k = ${best.minK}, l = ${best.minL}.`:'<b class="red">No feasible option.</b><br>Relax limits or choose a different dataset.';$('applyPlan').disabled=!best;$('planTable').innerHTML=table(['Rank','Age','Region','Role','Retained','Suppressed','Fidelity','Loss score','Action'],good.slice(0,8).map((z,i)=>`<tr><td>${i+1}</td><td>L${z.levels.age}</td><td>L${z.levels.region}</td><td>L${z.levels.role}</td><td>${z.kept.length}</td><td>${pct(z.suppRate)}</td><td>${pct(z.information)}</td><td>${fmt(z.score,3)}</td><td><button class="btn" data-plan="${i}">Apply</button></td></tr>`));for(const b of document.querySelectorAll('[data-plan]'))b.onclick=()=>{state.levels={...good[+b.dataset.plan].levels};jump('transform')}}
function renderUtility(s){const avg=x=>x.length?x.reduce((a,b)=>a+b.age,0)/x.length:null;const retainedIds=new Set(s.kept.map(x=>x._source));const retained=state.rows.filter(x=>retainedIds.has(x.id));const origAvg=avg(state.rows),newAvg=avg(retained);$('utilityMetrics').innerHTML=metric('Row retention',pct(s.retention),'Fraction of initial rows kept','green')+metric('Generalization fidelity',pct(1-s.distortion),'Unweighted attribute preservation')+metric('Overall preservation',pct(s.information),'Retention × attribute fidelity',s.information>.5?'green':'yellow')+metric('Change in mean age',retained.length?fmt(newAvg-origAvg,2)+' yr':'N/A','After suppression; descriptive only');const a=Core.counts(state.rows,'outcome'),b=Core.counts(retained,'outcome');$('outcomeBars').innerHTML=Object.keys(a).map(key=>`<div style="margin:16px 0"><div class="split" style="justify-content:space-between"><b style="font-size:12px">${X(key)}</b><span class="mini">${pct(a[key]/state.rows.length)} → ${retained.length?pct((b[key]||0)/retained.length):'n/a'}</span></div><div class="progress mt"><div style="width:${100*a[key]/state.rows.length}%;background:#6aa4e9"></div></div><div class="progress mt" style="margin-top:5px"><div style="width:${retained.length?100*(b[key]||0)/retained.length:0}%"></div></div></div>`).join('')+'<p class="mini">Blue: before suppression. Green: retained subset.</p>';$('fidelity').innerHTML=[['Age granularity',[100,73,44,5][state.levels.age]],['Region granularity',[100,45,0][state.levels.region]],['Role granularity',[100,50,0][state.levels.role]],['Row retention',s.retention*100]].map(([key,val])=>`<div style="margin:15px 0"><div class="split" style="justify-content:space-between"><span class="mini">${key}</span><b style="font-size:12px">${fmt(val)}%</b></div><div class="progress mt" style="margin-top:6px"><div style="width:${val}%"></div></div></div>`).join('')}
function renderData(s){$('dataKpis').innerHTML=[['Rows',s.total],['Columns',5],['Unique IDs',new Set(state.rows.map(x=>x.id)).size],['Source',state.custom&&state.name==='Imported CSV'?'Imported local file':'Synthetic demonstration']].map(([k,v])=>`<div><small>${k}</small><strong style="font-size:17px">${v}</strong></div>`).join('');$('dataSchema').innerHTML=table(['Column','Type','Export action'],[['id','String','Removed from release'],['age','Integer (16 to 110)','Generalized'],['region','Text','Generalized'],['role','Text','Generalized'],['outcome','Text','Retained sensitive attribute']].map(r=>`<tr>${r.map(x=>`<td>${X(x)}</td>`).join('')}</tr>`))}
function downloadFile(content,name,mime){const b=new Blob([content],{type:mime}),u=URL.createObjectURL(b),a=document.createElement('a');a.href=u;a.download=name;document.body.appendChild(a);a.click();a.remove();setTimeout(()=>URL.revokeObjectURL(u),1500)}
function audit(){const s=stat();return {project:'PrivacyForge #75',created_at:new Date().toISOString(),dataset:state.name,synthetic:state.name!=='Imported CSV',privacy_targets:{k:state.k,l:state.l},transform_levels:{...state.levels},suppression_allowance_pct:state.maxSupp,penalty_pct:state.penalty,source_rows:s.total,retained_rows:s.kept.length,suppressed_rows:s.suppressed,retention_rate:s.retention,minimum_retained_k:s.minK,minimum_retained_l:s.minL,generalization_distortion:s.distortion,combined_preservation:s.information,targets_met:s.valid,limitations:['Distinct l-diversity, not entropy or recursive l-diversity','No formal differential-privacy guarantee','Sensitive attribute still released','Does not measure external linkage or background knowledge','Suppression may bias aggregate statistics']}}
function doDownload(type){const s=stat(),base='privacyforge-'+type+'-'+state.name.toLowerCase().replace(/[^a-z0-9]+/g,'-');let txt,ext,mime='text/plain;charset=utf-8';if(type==='anon'){txt=Core.toCSV(s.kept,['age','region','role','outcome']);ext='.csv';mime='text/csv;charset=utf-8'}else if(type==='groups'){txt=Core.toCSV(s.classList.map(g=>({age:g.age,region:g.region,role:g.role,records:g.count,distinct_outcomes:g.l,meets_targets:g.valid})),['age','region','role','records','distinct_outcomes','meets_targets']);ext='.csv';mime='text/csv;charset=utf-8'}else if(type==='report'){txt=JSON.stringify({...audit(),class_counts:s.classList.map(g=>({age:g.age,region:g.region,role:g.role,k:g.count,l:g.l,valid:g.valid}))},null,2);ext='.json';mime='application/json'}else if(type==='config'){txt=JSON.stringify({project:'PrivacyForge',version:'1.0',k:state.k,l:state.l,levels:state.levels,max_suppression_pct:state.maxSupp,suppression_penalty_pct:state.penalty},null,2);ext='.json';mime='application/json'}else if(type==='template'){txt=Core.toCSV([{id:'DEMO-001',age:34,region:'North',role:'Analyst',outcome:'Completed'},{id:'DEMO-002',age:35,region:'North',role:'Operator',outcome:'Pending'}],['id','age','region','role','outcome']);ext='.csv';mime='text/csv;charset=utf-8'}else {const a=audit();txt=`# PrivacyForge privacy audit\n\nCreated: ${a.created_at}\n\nDataset: ${a.dataset} (${a.synthetic?'synthetic':'local import'})\n\n## Parameters\n\n- k target: ${a.privacy_targets.k}\n- distinct l target: ${a.privacy_targets.l}\n- age generalization level: ${a.transform_levels.age}\n- region generalization level: ${a.transform_levels.region}\n- role generalization level: ${a.transform_levels.role}\n\n## Outcomes\n\n- original rows: ${a.source_rows}\n- retained rows: ${a.retained_rows}\n- suppressed rows: ${a.suppressed_rows}\n- minimum retained k: ${a.minimum_retained_k}\n- minimum retained l: ${a.minimum_retained_l}\n- retention: ${pct(a.retention_rate)}\n- targets met on released rows: ${a.targets_met?'yes':'no'}\n\n## Limitations\n\n${a.limitations.map(x=>'- '+x).join('\n')}\n\nDo not treat this as certification that records cannot be reidentified.\n`;ext='.md'}downloadFile(txt,base+ext,mime);$('exportResult').textContent='Generated '+base+ext+' locally.'}
$('dataset').innerHTML=Object.keys(DEMOS).map(x=>`<option>${X(x)}</option>`).join('');$('dataset').onchange=e=>{if(e.target.value==='Imported CSV'){if(!state.custom)return;state.rows=state.custom;state.name='Imported CSV'}else{state.rows=DEMOS[e.target.value];state.name=e.target.value}state.levels={age:0,region:0,role:0};render()};$('topK').onchange=e=>{state.k=+e.target.value;render()};$('topL').onchange=e=>{state.l=+e.target.value;render()};$('groupFilter').onchange=e=>{state.groupFilter=e.target.value;renderGroups(stat())};$('suppressSlider').oninput=e=>{state.maxSupp=+e.target.value;renderPlanner(stat(),plans());renderOverview(stat(),plans())};$('penaltySlider').oninput=e=>{state.penalty=+e.target.value;renderPlanner(stat(),plans());renderOverview(stat(),plans())};function setBest(){const good=plans().find(p=>p.feasible);if(good){state.levels={...good.levels};jump('transform')}else{jump('planner')}}$('applyPlan').onclick=setBest;$('planNow').onclick=setBest;$('resetLevels').onclick=()=>{state.levels={age:0,region:0,role:0};render()};for(const b of document.querySelectorAll('[data-download]'))b.onclick=()=>doDownload(b.dataset.download);$('file').onchange=async e=>{const f=e.target.files[0];if(!f)return;try{if(f.size>1024*1024)throw Error('CSV is larger than 1 MB');const t=await f.text();const rows=Core.readCSV(t);state.custom=rows;state.name='Imported CSV';state.rows=rows;state.levels={age:0,region:0,role:0};if(!$('dataset').querySelector('[value="Imported CSV"]')){let op=document.createElement('option');op.value='Imported CSV';op.textContent='Imported CSV';$('dataset').appendChild(op)}$('dataset').value='Imported CSV';$('importResult').textContent=`Imported ${rows.length} records locally. Existing configuration reset to exact values.`;render()}catch(err){$('importResult').textContent='Import rejected: '+err.message}finally{e.target.value=''}};setNav();render();window.PrivacyForgeCore=Core;window.PrivacyForgeState=state;
</script>
</body>
</html>