Eric Xu commited on
Commit
8b7e05d
·
unverified ·
1 Parent(s): 25a1f95

Add bias audit to web interface, make web the primary entry point, switch to MIT license

Browse files

- Web: add Step 5 (bias audit) with framing/authority/order probes,
SSE streaming progress, and auto-enable bias calibration when over-biased
- Web: add bias calibration toggle to evaluation step
- Backend: add /api/bias-audit/stream endpoint, pass bias_calibration
param through evaluate endpoint
- README: web interface is now the primary Quick Start path
- License: switch from CC-BY-4.0 to MIT

Files changed (5) hide show
  1. LICENSE +17 -9
  2. README.md +26 -13
  3. pyproject.toml +1 -1
  4. web/app.py +96 -4
  5. web/static/index.html +180 -3
LICENSE CHANGED
@@ -1,13 +1,21 @@
1
- Creative Commons Attribution 4.0 International (CC BY 4.0)
2
 
3
- Copyright 2026
4
 
5
- You are free to:
6
- - Share copy and redistribute the material in any medium or format
7
- - Adapt remix, transform, and build upon the material for any purpose, even commercially
 
 
 
8
 
9
- Under the following terms:
10
- - Attribution You must give appropriate credit, provide a link to the license,
11
- and indicate if changes were made.
12
 
13
- https://creativecommons.org/licenses/by/4.0/
 
 
 
 
 
 
 
1
+ MIT License
2
 
3
+ Copyright (c) 2026 Eric Xu
4
 
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
 
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
 
14
 
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
README.md CHANGED
@@ -45,11 +45,25 @@ In each case, SGO tells you **where you stand**, **what's working**, **what's no
45
 
46
  ---
47
 
48
- ## Install
49
 
50
- Tell your coding agent:
 
 
 
 
 
 
51
 
52
- > *"Install the SGO skill from https://github.com/xuy/sgo"*
 
 
 
 
 
 
 
 
53
 
54
  Then run:
55
 
@@ -59,19 +73,15 @@ Then run:
59
  /sgo "optimize my landing page" # Start from a description
60
  ```
61
 
 
 
62
  <details>
63
- <summary>Manual install</summary>
64
 
65
  ```bash
66
- # As a Claude Code skill
67
- git clone https://github.com/xuy/sgo.git ~/.claude/skills/sgo
68
- cd ~/.claude/skills/sgo && cp .env.example .env && uv sync
69
-
70
- # Or standalone
71
- git clone https://github.com/xuy/sgo.git && cd sgo
72
- cp .env.example .env # Add your LLM API key (any OpenAI-compatible provider)
73
- uv sync
74
  uv run python scripts/setup_data.py # Download Nemotron personas (once, ~2GB)
 
 
75
  ```
76
 
77
  </details>
@@ -302,6 +312,9 @@ Only probe changes you'd actually make:
302
  │ ├── counterfactual.py # Semantic gradient probe
303
  │ ├── bias_audit.py # CoBRA-inspired cognitive bias measurement
304
  │ └── compare.py # Cross-run diff
 
 
 
305
  ├── templates/ # Entity + changes templates
306
  ├── entities/ # Your documents (gitignored)
307
  ├── data/ # Cohorts (gitignored)
@@ -312,4 +325,4 @@ Only probe changes you'd actually make:
312
 
313
  ## License
314
 
315
- CC-BY-4.0
 
45
 
46
  ---
47
 
48
+ ## Quick Start
49
 
50
+ ```bash
51
+ git clone https://github.com/xuy/sgo.git && cd sgo
52
+ cp .env.example .env # Add your LLM API key (any OpenAI-compatible provider)
53
+ uv sync
54
+ uv run --extra web python web/app.py
55
+ # Opens at http://localhost:8000
56
+ ```
57
 
58
+ The web interface walks you through the full pipeline: describe your entity, build a panel, evaluate, find the highest-impact changes, and audit your panel for cognitive biases.
59
+
60
+ <details>
61
+ <summary>Alternative: use as a Claude Code skill</summary>
62
+
63
+ ```bash
64
+ git clone https://github.com/xuy/sgo.git ~/.claude/skills/sgo
65
+ cd ~/.claude/skills/sgo && cp .env.example .env && uv sync
66
+ ```
67
 
68
  Then run:
69
 
 
73
  /sgo "optimize my landing page" # Start from a description
74
  ```
75
 
76
+ </details>
77
+
78
  <details>
79
+ <summary>CLI-only usage (no web interface)</summary>
80
 
81
  ```bash
 
 
 
 
 
 
 
 
82
  uv run python scripts/setup_data.py # Download Nemotron personas (once, ~2GB)
83
+ # Then use scripts directly: evaluate.py, counterfactual.py, bias_audit.py, compare.py
84
+ # See AGENT.md for the full pipeline reference
85
  ```
86
 
87
  </details>
 
312
  │ ├── counterfactual.py # Semantic gradient probe
313
  │ ├── bias_audit.py # CoBRA-inspired cognitive bias measurement
314
  │ └── compare.py # Cross-run diff
315
+ ├── web/
316
+ │ ├── app.py # FastAPI backend (primary entry point)
317
+ │ └── static/index.html # Single-page frontend
318
  ├── templates/ # Entity + changes templates
319
  ├── entities/ # Your documents (gitignored)
320
  ├── data/ # Cohorts (gitignored)
 
325
 
326
  ## License
327
 
328
+ MIT
pyproject.toml CHANGED
@@ -3,7 +3,7 @@ name = "semantic-gradient-optimization"
3
  version = "0.1.0"
4
  description = "Optimize entities against evaluator populations using LLMs and counterfactual probes"
5
  requires-python = ">=3.11"
6
- license = {text = "CC-BY-4.0"}
7
 
8
  dependencies = [
9
  "datasets>=4.0.0",
 
3
  version = "0.1.0"
4
  description = "Optimize entities against evaluator populations using LLMs and counterfactual probes"
5
  requires-python = ">=3.11"
6
+ license = {text = "MIT"}
7
 
8
  dependencies = [
9
  "datasets>=4.0.0",
web/app.py CHANGED
@@ -37,9 +37,13 @@ from openai import OpenAI
37
  # Import core functions from existing scripts
38
  import sys
39
  sys.path.insert(0, str(PROJECT_ROOT / "scripts"))
40
- from evaluate import evaluate_one, analyze as analyze_eval
41
  from counterfactual import probe_one, analyze_gradient, build_changes_block
42
  from generate_cohort import generate_segment
 
 
 
 
43
 
44
  app = FastAPI(title="SGO — Semantic Gradient Optimization")
45
  app.mount("/static", StaticFiles(directory=Path(__file__).parent / "static"), name="static")
@@ -175,7 +179,7 @@ async def upload_cohort(sid: str, cohort: list[dict]):
175
  # ── SSE streaming endpoints ──────────────────────────────────────────────
176
 
177
  @app.get("/api/evaluate/stream/{sid}")
178
- async def evaluate_stream(sid: str, parallel: int = 5):
179
  """Run evaluation with Server-Sent Events for real-time progress."""
180
  if sid not in sessions:
181
  raise HTTPException(404, "Session not found")
@@ -189,8 +193,12 @@ async def evaluate_stream(sid: str, parallel: int = 5):
189
  cohort = session["cohort"]
190
  entity_text = session["entity_text"]
191
  total = len(cohort)
 
192
 
193
- yield {"event": "start", "data": json.dumps({"total": total, "model": model})}
 
 
 
194
 
195
  results = [None] * total
196
  done = 0
@@ -199,7 +207,8 @@ async def evaluate_stream(sid: str, parallel: int = 5):
199
 
200
  with concurrent.futures.ThreadPoolExecutor(max_workers=parallel) as pool:
201
  futs = {
202
- pool.submit(evaluate_one, client, model, ev, entity_text): i
 
203
  for i, ev in enumerate(cohort)
204
  }
205
  for fut in concurrent.futures.as_completed(futs):
@@ -322,6 +331,89 @@ async def counterfactual_stream(
322
  return EventSourceResponse(event_generator())
323
 
324
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
325
  @app.get("/api/results/{sid}")
326
  async def get_results(sid: str):
327
  """Get full results for a session."""
 
37
  # Import core functions from existing scripts
38
  import sys
39
  sys.path.insert(0, str(PROJECT_ROOT / "scripts"))
40
+ from evaluate import evaluate_one, analyze as analyze_eval, SYSTEM_PROMPT, BIAS_CALIBRATION_ADDENDUM
41
  from counterfactual import probe_one, analyze_gradient, build_changes_block
42
  from generate_cohort import generate_segment
43
+ from bias_audit import (
44
+ reframe_entity, add_authority_signals, reorder_entity,
45
+ run_paired_evaluation, analyze_probe, generate_report, HUMAN_BASELINES,
46
+ )
47
 
48
  app = FastAPI(title="SGO — Semantic Gradient Optimization")
49
  app.mount("/static", StaticFiles(directory=Path(__file__).parent / "static"), name="static")
 
179
  # ── SSE streaming endpoints ──────────────────────────────────────────────
180
 
181
  @app.get("/api/evaluate/stream/{sid}")
182
+ async def evaluate_stream(sid: str, parallel: int = 5, bias_calibration: bool = False):
183
  """Run evaluation with Server-Sent Events for real-time progress."""
184
  if sid not in sessions:
185
  raise HTTPException(404, "Session not found")
 
193
  cohort = session["cohort"]
194
  entity_text = session["entity_text"]
195
  total = len(cohort)
196
+ sys_prompt = SYSTEM_PROMPT + BIAS_CALIBRATION_ADDENDUM if bias_calibration else None
197
 
198
+ yield {"event": "start", "data": json.dumps({
199
+ "total": total, "model": model,
200
+ "bias_calibration": bias_calibration,
201
+ })}
202
 
203
  results = [None] * total
204
  done = 0
 
207
 
208
  with concurrent.futures.ThreadPoolExecutor(max_workers=parallel) as pool:
209
  futs = {
210
+ pool.submit(evaluate_one, client, model, ev, entity_text,
211
+ system_prompt=sys_prompt): i
212
  for i, ev in enumerate(cohort)
213
  }
214
  for fut in concurrent.futures.as_completed(futs):
 
331
  return EventSourceResponse(event_generator())
332
 
333
 
334
+ @app.get("/api/bias-audit/stream/{sid}")
335
+ async def bias_audit_stream(
336
+ sid: str, probes: str = "framing,authority,order",
337
+ sample: int = 10, parallel: int = 5
338
+ ):
339
+ """Run bias audit probes with SSE progress."""
340
+ if sid not in sessions:
341
+ raise HTTPException(404, "Session not found")
342
+ session = sessions[sid]
343
+ if not session["cohort"]:
344
+ raise HTTPException(400, "No cohort — generate or upload one first")
345
+
346
+ probe_list = [p.strip() for p in probes.split(",") if p.strip()]
347
+
348
+ async def event_generator():
349
+ import random
350
+ client = get_client()
351
+ model = get_model()
352
+ cohort = session["cohort"]
353
+ entity_text = session["entity_text"]
354
+
355
+ random.seed(42)
356
+ evaluators = random.sample(cohort, min(sample, len(cohort)))
357
+
358
+ yield {"event": "start", "data": json.dumps({
359
+ "probes": probe_list,
360
+ "sample_size": len(evaluators),
361
+ "model": model,
362
+ })}
363
+
364
+ all_analyses = []
365
+
366
+ for probe_name in probe_list:
367
+ yield {"event": "probe_start", "data": json.dumps({"probe": probe_name})}
368
+
369
+ t0 = time.time()
370
+
371
+ if probe_name == "framing":
372
+ gain_entity = reframe_entity(client, model, entity_text, "gain")
373
+ loss_entity = reframe_entity(client, model, entity_text, "loss")
374
+ results = run_paired_evaluation(
375
+ client, model, evaluators, gain_entity, loss_entity,
376
+ "gain", "loss", parallel,
377
+ )
378
+ label_a, label_b = "gain", "loss"
379
+ elif probe_name == "authority":
380
+ entity_with_auth = add_authority_signals(entity_text)
381
+ results = run_paired_evaluation(
382
+ client, model, evaluators, entity_text, entity_with_auth,
383
+ "baseline", "authority", parallel,
384
+ )
385
+ label_a, label_b = "baseline", "authority"
386
+ elif probe_name == "order":
387
+ reordered = reorder_entity(entity_text)
388
+ results = run_paired_evaluation(
389
+ client, model, evaluators, entity_text, reordered,
390
+ "original", "reordered", parallel,
391
+ )
392
+ label_a, label_b = "original", "reordered"
393
+ else:
394
+ continue
395
+
396
+ elapsed = time.time() - t0
397
+ analysis = analyze_probe(results, probe_name, label_a, label_b)
398
+ analysis["elapsed_s"] = round(elapsed, 1)
399
+ all_analyses.append(analysis)
400
+
401
+ yield {"event": "probe_complete", "data": json.dumps({
402
+ "probe": probe_name,
403
+ "analysis": analysis,
404
+ })}
405
+
406
+ report = generate_report(all_analyses, model)
407
+ session["bias_audit"] = {"analyses": all_analyses, "report": report}
408
+
409
+ yield {"event": "complete", "data": json.dumps({
410
+ "analyses": all_analyses,
411
+ "report": report,
412
+ })}
413
+
414
+ return EventSourceResponse(event_generator())
415
+
416
+
417
  @app.get("/api/results/{sid}")
418
  async def get_results(sid: str):
419
  """Get full results for a session."""
web/static/index.html CHANGED
@@ -374,6 +374,10 @@
374
 
375
  <div class="btn-row">
376
  <button onclick="runEval()" id="evalBtn">Evaluate</button>
 
 
 
 
377
  <div style="flex:1"></div>
378
  <label style="display:flex;align-items:center;gap:6px;margin:0">
379
  <span style="font-size:0.8rem;color:var(--text2)">Parallel:</span>
@@ -399,7 +403,10 @@
399
  <summary style="cursor:pointer;color:var(--text2);font-size:0.9rem">Full analysis</summary>
400
  <div class="results-details" id="evalAnalysis"></div>
401
  </details>
402
- <button class="mt-16" onclick="goToStep(4)">Continue to gradient</button>
 
 
 
403
  </div>
404
  </div>
405
 
@@ -446,6 +453,66 @@
446
  </details>
447
  </div>
448
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
449
  </div>
450
 
451
  <script>
@@ -547,8 +614,9 @@ function loadTemplate(name) {
547
  // ── Step navigation ──
548
 
549
  function goToStep(n) {
550
- for (let i = 1; i <= 4; i++) {
551
  const el = document.getElementById(`step${i}`);
 
552
  if (i < n) {
553
  el.classList.remove('hidden', 'active');
554
  el.classList.add('done');
@@ -560,6 +628,12 @@ function goToStep(n) {
560
  el.classList.remove('active', 'done');
561
  }
562
  }
 
 
 
 
 
 
563
  }
564
 
565
  // ── Step 1: Entity ──
@@ -662,6 +736,7 @@ async function generateCohort() {
662
  function runEval() {
663
  if (!sessionId) return alert('No session.');
664
  const parallel = parseInt(document.getElementById('evalParallel').value) || 5;
 
665
  const btn = document.getElementById('evalBtn');
666
  btn.disabled = true;
667
 
@@ -669,7 +744,8 @@ function runEval() {
669
  document.getElementById('evalResults').classList.add('hidden');
670
  document.getElementById('evalLog').innerHTML = '';
671
 
672
- const es = new EventSource(`/api/evaluate/stream/${sessionId}?parallel=${parallel}`);
 
673
 
674
  es.addEventListener('start', (e) => {
675
  const d = JSON.parse(e.data);
@@ -872,6 +948,107 @@ function renderGradientTable(results, changes) {
872
  });
873
  }
874
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
875
  // Boot
876
  init();
877
  </script>
 
374
 
375
  <div class="btn-row">
376
  <button onclick="runEval()" id="evalBtn">Evaluate</button>
377
+ <label style="display:flex;align-items:center;gap:6px;margin:0">
378
+ <input type="checkbox" id="biasCalibration">
379
+ <span style="font-size:0.8rem;color:var(--text2)">Bias calibration</span>
380
+ </label>
381
  <div style="flex:1"></div>
382
  <label style="display:flex;align-items:center;gap:6px;margin:0">
383
  <span style="font-size:0.8rem;color:var(--text2)">Parallel:</span>
 
403
  <summary style="cursor:pointer;color:var(--text2);font-size:0.9rem">Full analysis</summary>
404
  <div class="results-details" id="evalAnalysis"></div>
405
  </details>
406
+ <div class="btn-row mt-16">
407
+ <button onclick="goToStep(4)">Continue to gradient</button>
408
+ <button class="secondary" onclick="goToStep(5)">Run bias audit</button>
409
+ </div>
410
  </div>
411
  </div>
412
 
 
453
  </details>
454
  </div>
455
  </div>
456
+
457
+ <!-- STEP 5: Bias Audit -->
458
+ <div class="step hidden" id="step5">
459
+ <div class="step-header">
460
+ <div class="step-num">5</div>
461
+ <div class="step-title">Bias Audit</div>
462
+ </div>
463
+ <p class="step-desc">
464
+ Measure cognitive biases in your evaluator panel using validated social science experiments
465
+ (<a href="https://arxiv.org/abs/2509.13588" target="_blank" style="color:var(--accent2)">CoBRA, CHI'26</a>).
466
+ </p>
467
+
468
+ <div class="field">
469
+ <label>Probes to run</label>
470
+ <div style="display:flex;gap:16px;margin-bottom:12px">
471
+ <label style="display:flex;align-items:center;gap:6px;margin:0;font-size:0.85rem">
472
+ <input type="checkbox" id="probeFraming" checked> Framing effect
473
+ </label>
474
+ <label style="display:flex;align-items:center;gap:6px;margin:0;font-size:0.85rem">
475
+ <input type="checkbox" id="probeAuthority" checked> Authority bias
476
+ </label>
477
+ <label style="display:flex;align-items:center;gap:6px;margin:0;font-size:0.85rem">
478
+ <input type="checkbox" id="probeOrder" checked> Order effect
479
+ </label>
480
+ </div>
481
+ </div>
482
+
483
+ <div class="btn-row">
484
+ <button onclick="runBiasAudit()" id="auditBtn">Run audit</button>
485
+ <div style="flex:1"></div>
486
+ <label style="display:flex;align-items:center;gap:6px;margin:0">
487
+ <span style="font-size:0.8rem;color:var(--text2)">Sample size:</span>
488
+ <input type="number" id="auditSample" value="10" min="1" max="50"
489
+ style="width:60px;padding:6px;text-align:center">
490
+ </label>
491
+ </div>
492
+
493
+ <div id="auditProgress" class="hidden mt-16">
494
+ <div class="progress-text" id="auditProgressText">Running bias probes...</div>
495
+ <div class="progress-bar"><div class="progress-fill" id="auditProgressBar"></div></div>
496
+ </div>
497
+
498
+ <div id="auditResults" class="hidden mt-16">
499
+ <h3 style="margin-bottom:12px">Bias Audit Results</h3>
500
+ <table class="gradient-table" id="auditTable">
501
+ <thead>
502
+ <tr><th>Probe</th><th>Shifted %</th><th>Avg |Delta|</th><th>Human Baseline</th><th>Assessment</th></tr>
503
+ </thead>
504
+ <tbody></tbody>
505
+ </table>
506
+ <details class="mt-12">
507
+ <summary style="cursor:pointer;color:var(--text2);font-size:0.9rem">Full report</summary>
508
+ <div class="results-details" id="auditReport"></div>
509
+ </details>
510
+ <div class="btn-row mt-16">
511
+ <button class="secondary" onclick="goToStep(3)">Re-evaluate with bias calibration</button>
512
+ <button class="secondary" onclick="goToStep(4)">Continue to gradient</button>
513
+ </div>
514
+ </div>
515
+ </div>
516
  </div>
517
 
518
  <script>
 
614
  // ── Step navigation ──
615
 
616
  function goToStep(n) {
617
+ for (let i = 1; i <= 5; i++) {
618
  const el = document.getElementById(`step${i}`);
619
+ if (!el) continue;
620
  if (i < n) {
621
  el.classList.remove('hidden', 'active');
622
  el.classList.add('done');
 
628
  el.classList.remove('active', 'done');
629
  }
630
  }
631
+ // When going to bias audit, keep steps 4/5 both visible context
632
+ if (n === 5) {
633
+ const step3 = document.getElementById('step3');
634
+ step3.classList.remove('hidden');
635
+ step3.classList.add('done');
636
+ }
637
  }
638
 
639
  // ── Step 1: Entity ──
 
736
  function runEval() {
737
  if (!sessionId) return alert('No session.');
738
  const parallel = parseInt(document.getElementById('evalParallel').value) || 5;
739
+ const biasCal = document.getElementById('biasCalibration').checked;
740
  const btn = document.getElementById('evalBtn');
741
  btn.disabled = true;
742
 
 
744
  document.getElementById('evalResults').classList.add('hidden');
745
  document.getElementById('evalLog').innerHTML = '';
746
 
747
+ const params = new URLSearchParams({parallel, bias_calibration: biasCal});
748
+ const es = new EventSource(`/api/evaluate/stream/${sessionId}?${params}`);
749
 
750
  es.addEventListener('start', (e) => {
751
  const d = JSON.parse(e.data);
 
948
  });
949
  }
950
 
951
+ // ── Step 5: Bias Audit ──
952
+
953
+ function runBiasAudit() {
954
+ if (!sessionId) return alert('No session.');
955
+ const probes = [];
956
+ if (document.getElementById('probeFraming').checked) probes.push('framing');
957
+ if (document.getElementById('probeAuthority').checked) probes.push('authority');
958
+ if (document.getElementById('probeOrder').checked) probes.push('order');
959
+ if (probes.length === 0) return alert('Select at least one probe.');
960
+
961
+ const sample = parseInt(document.getElementById('auditSample').value) || 10;
962
+ const btn = document.getElementById('auditBtn');
963
+ btn.disabled = true;
964
+
965
+ document.getElementById('auditProgress').classList.remove('hidden');
966
+ document.getElementById('auditResults').classList.add('hidden');
967
+
968
+ let probesDone = 0;
969
+ const totalProbes = probes.length;
970
+
971
+ const params = new URLSearchParams({probes: probes.join(','), sample, parallel: 5});
972
+ const es = new EventSource(`/api/bias-audit/stream/${sessionId}?${params}`);
973
+
974
+ es.addEventListener('start', (e) => {
975
+ const d = JSON.parse(e.data);
976
+ document.getElementById('auditProgressText').textContent =
977
+ `Running ${d.probes.length} probes on ${d.sample_size} evaluators (${d.model})...`;
978
+ });
979
+
980
+ es.addEventListener('probe_start', (e) => {
981
+ const d = JSON.parse(e.data);
982
+ document.getElementById('auditProgressText').textContent =
983
+ `Running ${d.probe} probe...`;
984
+ });
985
+
986
+ es.addEventListener('probe_complete', (e) => {
987
+ probesDone++;
988
+ const pct = Math.round(probesDone / totalProbes * 100);
989
+ document.getElementById('auditProgressBar').style.width = pct + '%';
990
+ const d = JSON.parse(e.data);
991
+ document.getElementById('auditProgressText').textContent =
992
+ `${d.probe}: ${d.analysis.shifted_pct}% shifted (${probesDone}/${totalProbes} probes done)`;
993
+ });
994
+
995
+ es.addEventListener('complete', (e) => {
996
+ es.close();
997
+ const d = JSON.parse(e.data);
998
+
999
+ document.getElementById('auditProgressBar').style.width = '100%';
1000
+ document.getElementById('auditProgressText').textContent = 'Audit complete';
1001
+
1002
+ const tbody = document.querySelector('#auditTable tbody');
1003
+ tbody.innerHTML = '';
1004
+ const baselines = {framing: 30, authority: 20, order: 0};
1005
+
1006
+ d.analyses.forEach(a => {
1007
+ if (a.error) {
1008
+ tbody.innerHTML += `<tr><td>${a.probe}</td><td colspan="4">Error: ${a.error}</td></tr>`;
1009
+ return;
1010
+ }
1011
+ const expected = baselines[a.probe];
1012
+ const gap = a.shifted_pct - (expected || 0);
1013
+ let assessment, assessCls;
1014
+ if (expected !== undefined) {
1015
+ if (gap > 10) { assessment = 'Over-biased'; assessCls = 'color:var(--red)'; }
1016
+ else if (gap < -10) { assessment = 'Under-biased'; assessCls = 'color:var(--yellow)'; }
1017
+ else { assessment = 'Well-calibrated'; assessCls = 'color:var(--green)'; }
1018
+ } else {
1019
+ assessment = '—'; assessCls = '';
1020
+ }
1021
+
1022
+ tbody.innerHTML += `
1023
+ <tr>
1024
+ <td style="font-weight:600">${a.probe}</td>
1025
+ <td>${a.shifted_pct.toFixed(1)}%</td>
1026
+ <td>${a.avg_abs_delta.toFixed(2)}</td>
1027
+ <td style="color:var(--text2)">${expected !== undefined ? expected + '%' : '—'}</td>
1028
+ <td style="${assessCls};font-weight:600">${assessment}</td>
1029
+ </tr>
1030
+ `;
1031
+ });
1032
+
1033
+ document.getElementById('auditReport').textContent = d.report;
1034
+ document.getElementById('auditResults').classList.remove('hidden');
1035
+
1036
+ // If over-biased, auto-check the bias calibration checkbox
1037
+ const hasOverBias = d.analyses.some(a => a.shifted_pct - (baselines[a.probe] || 0) > 10);
1038
+ if (hasOverBias) {
1039
+ document.getElementById('biasCalibration').checked = true;
1040
+ }
1041
+
1042
+ btn.disabled = false;
1043
+ });
1044
+
1045
+ es.onerror = () => {
1046
+ es.close();
1047
+ document.getElementById('auditProgressText').textContent = 'Connection lost';
1048
+ btn.disabled = false;
1049
+ };
1050
+ }
1051
+
1052
  // Boot
1053
  init();
1054
  </script>