Pointf5ive commited on
Commit
7ae21b5
·
1 Parent(s): 9674914

stage 2: dashboard state contract

Browse files

Scope: add dashboard state contract, gauge formula, escaping helper, and boundary adapter without changing existing scoring logic or callback return formats.

Validation: py_compile + contract smoke checks passed (0/35/weighted signal behavior verified); stage acceptance gate passed.

Files changed (1) hide show
  1. app.py +173 -0
app.py CHANGED
@@ -1,6 +1,7 @@
1
  from __future__ import annotations
2
 
3
  import hashlib
 
4
  import json
5
  import re
6
  from html import escape
@@ -350,6 +351,173 @@ REVISION_ACTIONS = {
350
  "Commercial Publishability": "Tighten hook, age fit, and list-readiness.",
351
  }
352
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
353
 
354
  def _clean_path(uploaded_file) -> Path:
355
  return workbook_path(uploaded_file)
@@ -1439,4 +1607,9 @@ with gr.Blocks(title="TOTEM Studio") as demo:
1439
 
1440
 
1441
  if __name__ == "__main__":
 
 
 
 
 
1442
  demo.launch(ssr_mode=False, css=CSS + TOTEM_CSS + SS_CSS, head=HEAD)
 
1
  from __future__ import annotations
2
 
3
  import hashlib
4
+ import html
5
  import json
6
  import re
7
  from html import escape
 
351
  "Commercial Publishability": "Tighten hook, age fit, and list-readiness.",
352
  }
353
 
354
+ DASHBOARD_STATE_KEYS = (
355
+ "project_name",
356
+ "workbook_loaded",
357
+ "analysis_status",
358
+ "last_analysis_at",
359
+ "totem_signal",
360
+ "metrics",
361
+ "metric_history",
362
+ "revision_queue",
363
+ "risk_clusters",
364
+ )
365
+
366
+
367
+ def esc(value) -> str:
368
+ """HTML-escape UI text payloads safely."""
369
+ return html.escape(str(value or ""))
370
+
371
+
372
+ def _state_bool(value) -> bool:
373
+ if isinstance(value, bool):
374
+ return value
375
+ if isinstance(value, str):
376
+ low = value.strip().lower()
377
+ if low in {"1", "true", "yes", "y", "on"}:
378
+ return True
379
+ if low in {"0", "false", "no", "n", "off", "", "none", "null"}:
380
+ return False
381
+ return bool(value)
382
+
383
+
384
+ def _state_float(value, default: float = 0.0) -> float:
385
+ try:
386
+ return float(value)
387
+ except Exception:
388
+ return float(default)
389
+
390
+
391
+ def compute_totem_signal(
392
+ metrics: dict,
393
+ workbook_loaded: bool,
394
+ analysis_timestamp: str | None,
395
+ ) -> int:
396
+ """
397
+ Hero gauge contract from the design manual:
398
+ - 0 when no workbook is loaded.
399
+ - 35 when workbook is loaded but analysis has not run yet.
400
+ - Otherwise weighted metric blend, clamped 0..100.
401
+ """
402
+ if not _state_bool(workbook_loaded):
403
+ return 0
404
+ if not analysis_timestamp:
405
+ return 35
406
+
407
+ weights = {
408
+ "overall_publishability": 0.30,
409
+ "read_aloud_flow": 0.15,
410
+ "emotional_truth": 0.20,
411
+ "visual_strength": 0.20,
412
+ "commercial_viability": 0.15,
413
+ }
414
+
415
+ total = 0.0
416
+ metric_map = metrics or {}
417
+ for key, weight in weights.items():
418
+ total += _state_float(metric_map.get(key, 0.0), 0.0) * weight
419
+ return max(0, min(100, int(round(total))))
420
+
421
+
422
+ def get_initial_dashboard_state() -> dict:
423
+ """
424
+ Stage 2 state contract.
425
+ Placeholder values are deliberate before first analysis run.
426
+ """
427
+ metrics = {
428
+ "overall_publishability": 0,
429
+ "read_aloud_flow": 0,
430
+ "emotional_truth": 0,
431
+ "visual_strength": 0,
432
+ "commercial_viability": 0,
433
+ }
434
+ return {
435
+ "project_name": "Editorial Workspace",
436
+ "workbook_loaded": False,
437
+ "analysis_status": "idle", # idle | ready | running | complete | error
438
+ "last_analysis_at": None,
439
+ "totem_signal": compute_totem_signal(metrics, workbook_loaded=False, analysis_timestamp=None),
440
+ "metrics": metrics,
441
+ "metric_history": {
442
+ "overall_publishability": [],
443
+ "read_aloud_flow": [],
444
+ "emotional_truth": [],
445
+ "visual_strength": [],
446
+ "commercial_viability": [],
447
+ },
448
+ "revision_queue": [],
449
+ "risk_clusters": [],
450
+ }
451
+
452
+
453
+ def normalize_dashboard_state(raw_existing_outputs) -> dict:
454
+ """
455
+ Boundary adapter that normalizes scattered callback outputs into the Stage 2 state contract.
456
+ This adapter is UI-boundary only and does not alter extractor/scoring logic internals.
457
+ """
458
+ state = get_initial_dashboard_state()
459
+ if raw_existing_outputs is None:
460
+ return state
461
+ if not isinstance(raw_existing_outputs, dict):
462
+ return state
463
+
464
+ state["project_name"] = str(raw_existing_outputs.get("project_name") or state["project_name"])
465
+ state["workbook_loaded"] = _state_bool(raw_existing_outputs.get("workbook_loaded", state["workbook_loaded"]))
466
+
467
+ status = str(raw_existing_outputs.get("analysis_status") or state["analysis_status"]).strip().lower()
468
+ if status not in {"idle", "ready", "running", "complete", "error"}:
469
+ status = state["analysis_status"]
470
+ state["analysis_status"] = status
471
+
472
+ ts_value = raw_existing_outputs.get("last_analysis_at")
473
+ state["last_analysis_at"] = str(ts_value).strip() if ts_value else None
474
+
475
+ incoming_metrics = raw_existing_outputs.get("metrics")
476
+ if isinstance(incoming_metrics, dict):
477
+ for key in state["metrics"]:
478
+ state["metrics"][key] = int(round(_state_float(incoming_metrics.get(key, state["metrics"][key]))))
479
+ state["metrics"][key] = max(0, min(100, state["metrics"][key]))
480
+
481
+ incoming_history = raw_existing_outputs.get("metric_history")
482
+ if isinstance(incoming_history, dict):
483
+ normalized_history = {}
484
+ for key in state["metric_history"].keys():
485
+ values = incoming_history.get(key, [])
486
+ if isinstance(values, list):
487
+ normalized_history[key] = [
488
+ max(0, min(100, int(round(_state_float(v)))))
489
+ for v in values
490
+ ]
491
+ else:
492
+ normalized_history[key] = []
493
+ state["metric_history"] = normalized_history
494
+
495
+ revision_queue = raw_existing_outputs.get("revision_queue")
496
+ if isinstance(revision_queue, list):
497
+ state["revision_queue"] = revision_queue
498
+
499
+ risk_clusters = raw_existing_outputs.get("risk_clusters")
500
+ if isinstance(risk_clusters, list):
501
+ state["risk_clusters"] = risk_clusters
502
+
503
+ if "totem_signal" in raw_existing_outputs:
504
+ explicit = int(round(_state_float(raw_existing_outputs.get("totem_signal"), state["totem_signal"])))
505
+ state["totem_signal"] = max(0, min(100, explicit))
506
+ else:
507
+ state["totem_signal"] = compute_totem_signal(
508
+ state["metrics"],
509
+ workbook_loaded=state["workbook_loaded"],
510
+ analysis_timestamp=state["last_analysis_at"],
511
+ )
512
+
513
+ return state
514
+
515
+
516
+ def _dashboard_state_contract_smoke_test() -> tuple[bool, list[str]]:
517
+ state = get_initial_dashboard_state()
518
+ missing = [key for key in DASHBOARD_STATE_KEYS if key not in state]
519
+ return len(missing) == 0, missing
520
+
521
 
522
  def _clean_path(uploaded_file) -> Path:
523
  return workbook_path(uploaded_file)
 
1607
 
1608
 
1609
  if __name__ == "__main__":
1610
+ state_ok, missing_keys = _dashboard_state_contract_smoke_test()
1611
+ if state_ok:
1612
+ print("[stage2] dashboard_state contract OK")
1613
+ else:
1614
+ print(f"[stage2] dashboard_state missing keys: {missing_keys}")
1615
  demo.launch(ssr_mode=False, css=CSS + TOTEM_CSS + SS_CSS, head=HEAD)