Spaces:
Sleeping
Sleeping
Pointf5ive commited on
Commit ·
6083069
1
Parent(s): efebee3
Reject TOTEM scores that contradict fingerprint metrics
Browse files- src/totem_bridge.py +93 -7
src/totem_bridge.py
CHANGED
|
@@ -138,6 +138,7 @@ def dashboard_output_contract() -> dict[str, Any]:
|
|
| 138 |
field: {
|
| 139 |
"score": "integer 0..100 matching scores.<field>",
|
| 140 |
"fingerprint_metrics_used": ["VM metric ids used, e.g. VM-001, VM-010, VM-025"],
|
|
|
|
| 141 |
"workbook_rule": "target/threshold/rubric rule used from workbook matrix",
|
| 142 |
"manuscript_evidence": "short paraphrased manuscript evidence; no long excerpts",
|
| 143 |
"reason": "why this score follows from the workbook rule and fingerprint metrics",
|
|
@@ -199,8 +200,11 @@ Use the workbook matrix as scoring authority, manuscript fingerprint VM metrics
|
|
| 199 |
"Return exactly one JSON object matching required_output_contract. "
|
| 200 |
"Scores must be 0..100 dashboard values for the six dimensions. "
|
| 201 |
"Every score_evidence entry must cite at least one VM metric id from manuscript_fingerprint "
|
| 202 |
-
"
|
| 203 |
-
"return gate REVISE and explain the missing evidence in dashboard_message. "
|
|
|
|
|
|
|
|
|
|
| 204 |
"Do not write a report and do not include manuscript excerpts."
|
| 205 |
),
|
| 206 |
}
|
|
@@ -210,7 +214,7 @@ Use the workbook matrix as scoring authority, manuscript fingerprint VM metrics
|
|
| 210 |
if os.getenv("SPACE_ID") and not _env_truthy("TOTEM_ALLOW_FAKE_API_IN_SPACE"):
|
| 211 |
raise BridgeError("TOTEM_FAKE_API is disabled on Hugging Face Spaces.")
|
| 212 |
raw_payload = _fake_dashboard_payload(manuscript)
|
| 213 |
-
validated = validate_dashboard_payload(raw_payload)
|
| 214 |
raw_payload_json = json.dumps(raw_payload, ensure_ascii=False)
|
| 215 |
audit_path = _write_token_audit(
|
| 216 |
transaction_id=transaction_id,
|
|
@@ -319,7 +323,7 @@ Use the workbook matrix as scoring authority, manuscript fingerprint VM metrics
|
|
| 319 |
raise BridgeValidationError(f"Model returned invalid JSON: {exc}") from exc
|
| 320 |
|
| 321 |
try:
|
| 322 |
-
validated = validate_dashboard_payload(parsed)
|
| 323 |
except BridgeValidationError as exc:
|
| 324 |
audit_path = _write_token_audit(
|
| 325 |
transaction_id=transaction_id,
|
|
@@ -548,7 +552,7 @@ def _first_env_value(names: tuple[str, ...]) -> tuple[str | None, str | None]:
|
|
| 548 |
return None, None
|
| 549 |
|
| 550 |
|
| 551 |
-
def validate_dashboard_payload(payload: dict[str, Any]) -> dict[str, Any]:
|
| 552 |
if not isinstance(payload, dict):
|
| 553 |
raise BridgeValidationError("Dashboard payload must be a JSON object.")
|
| 554 |
|
|
@@ -595,7 +599,8 @@ def validate_dashboard_payload(payload: dict[str, Any]) -> dict[str, Any]:
|
|
| 595 |
score_evidence = payload.get("score_evidence")
|
| 596 |
if not isinstance(score_evidence, dict):
|
| 597 |
raise BridgeValidationError("score_evidence must be a JSON object keyed by scoring dimension.")
|
| 598 |
-
out["score_evidence"] = _normalize_score_evidence(score_evidence, out["scores"])
|
|
|
|
| 599 |
|
| 600 |
rpq = payload.get("revision_priority_queue")
|
| 601 |
if not isinstance(rpq, list):
|
|
@@ -609,7 +614,11 @@ def validate_dashboard_payload(payload: dict[str, Any]) -> dict[str, Any]:
|
|
| 609 |
return out
|
| 610 |
|
| 611 |
|
| 612 |
-
def _normalize_score_evidence(
|
|
|
|
|
|
|
|
|
|
|
|
|
| 613 |
normalized: dict[str, dict[str, Any]] = {}
|
| 614 |
missing: list[str] = []
|
| 615 |
weak: list[str] = []
|
|
@@ -626,6 +635,10 @@ def _normalize_score_evidence(evidence: dict[str, Any], scores: dict[str, int])
|
|
| 626 |
if not metric_ids or not any(m.upper().startswith("VM-") for m in metric_ids):
|
| 627 |
weak.append(field)
|
| 628 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 629 |
workbook_rule = str(item.get("workbook_rule") or "").strip()[:260]
|
| 630 |
manuscript_evidence = str(item.get("manuscript_evidence") or "").strip()[:260]
|
| 631 |
reason = str(item.get("reason") or "").strip()[:320]
|
|
@@ -635,6 +648,10 @@ def _normalize_score_evidence(evidence: dict[str, Any], scores: dict[str, int])
|
|
| 635 |
normalized[field] = {
|
| 636 |
"score": scores[field],
|
| 637 |
"fingerprint_metrics_used": metric_ids[:8],
|
|
|
|
|
|
|
|
|
|
|
|
|
| 638 |
"workbook_rule": workbook_rule,
|
| 639 |
"manuscript_evidence": manuscript_evidence,
|
| 640 |
"reason": reason,
|
|
@@ -648,6 +665,55 @@ def _normalize_score_evidence(evidence: dict[str, Any], scores: dict[str, int])
|
|
| 648 |
return normalized
|
| 649 |
|
| 650 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 651 |
def recompute_gate(metrics: dict[str, int], rubric_matrix: dict[str, Any]) -> str:
|
| 652 |
weights = rubric_matrix.get("weights", {}) if isinstance(rubric_matrix, dict) else {}
|
| 653 |
if not isinstance(weights, dict) or not weights:
|
|
@@ -802,3 +868,23 @@ def _fingerprint_metric_count(fingerprint: dict[str, Any] | None) -> int:
|
|
| 802 |
if not isinstance(fingerprint, dict):
|
| 803 |
return 0
|
| 804 |
return sum(1 for key in fingerprint if str(key).upper().startswith("VM-"))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 138 |
field: {
|
| 139 |
"score": "integer 0..100 matching scores.<field>",
|
| 140 |
"fingerprint_metrics_used": ["VM metric ids used, e.g. VM-001, VM-010, VM-025"],
|
| 141 |
+
"fingerprint_metric_values": {"VM-010": "numeric/string value used from manuscript_fingerprint"},
|
| 142 |
"workbook_rule": "target/threshold/rubric rule used from workbook matrix",
|
| 143 |
"manuscript_evidence": "short paraphrased manuscript evidence; no long excerpts",
|
| 144 |
"reason": "why this score follows from the workbook rule and fingerprint metrics",
|
|
|
|
| 200 |
"Return exactly one JSON object matching required_output_contract. "
|
| 201 |
"Scores must be 0..100 dashboard values for the six dimensions. "
|
| 202 |
"Every score_evidence entry must cite at least one VM metric id from manuscript_fingerprint "
|
| 203 |
+
"with its observed value in fingerprint_metric_values, plus one workbook rule/target from rubric_matrix. "
|
| 204 |
+
"If the fingerprint is missing or insufficient, return gate REVISE and explain the missing evidence in dashboard_message. "
|
| 205 |
+
"Hard caps for picture-book scoring: if VM-010 sentence length mean is above 30 or VM-025 reading age "
|
| 206 |
+
"is above 10, clarity and read_aloud_flow must not exceed 75. If VM-025 reading age is above 10, "
|
| 207 |
+
"commercial_viability must not exceed 75. If VM-002 syllable variance is above 5, rhythm must not exceed 75. "
|
| 208 |
"Do not write a report and do not include manuscript excerpts."
|
| 209 |
),
|
| 210 |
}
|
|
|
|
| 214 |
if os.getenv("SPACE_ID") and not _env_truthy("TOTEM_ALLOW_FAKE_API_IN_SPACE"):
|
| 215 |
raise BridgeError("TOTEM_FAKE_API is disabled on Hugging Face Spaces.")
|
| 216 |
raw_payload = _fake_dashboard_payload(manuscript)
|
| 217 |
+
validated = validate_dashboard_payload(raw_payload, manuscript.fingerprint)
|
| 218 |
raw_payload_json = json.dumps(raw_payload, ensure_ascii=False)
|
| 219 |
audit_path = _write_token_audit(
|
| 220 |
transaction_id=transaction_id,
|
|
|
|
| 323 |
raise BridgeValidationError(f"Model returned invalid JSON: {exc}") from exc
|
| 324 |
|
| 325 |
try:
|
| 326 |
+
validated = validate_dashboard_payload(parsed, manuscript.fingerprint)
|
| 327 |
except BridgeValidationError as exc:
|
| 328 |
audit_path = _write_token_audit(
|
| 329 |
transaction_id=transaction_id,
|
|
|
|
| 552 |
return None, None
|
| 553 |
|
| 554 |
|
| 555 |
+
def validate_dashboard_payload(payload: dict[str, Any], fingerprint: dict[str, Any] | None = None) -> dict[str, Any]:
|
| 556 |
if not isinstance(payload, dict):
|
| 557 |
raise BridgeValidationError("Dashboard payload must be a JSON object.")
|
| 558 |
|
|
|
|
| 599 |
score_evidence = payload.get("score_evidence")
|
| 600 |
if not isinstance(score_evidence, dict):
|
| 601 |
raise BridgeValidationError("score_evidence must be a JSON object keyed by scoring dimension.")
|
| 602 |
+
out["score_evidence"] = _normalize_score_evidence(score_evidence, out["scores"], fingerprint)
|
| 603 |
+
_validate_fingerprint_score_alignment(out, fingerprint)
|
| 604 |
|
| 605 |
rpq = payload.get("revision_priority_queue")
|
| 606 |
if not isinstance(rpq, list):
|
|
|
|
| 614 |
return out
|
| 615 |
|
| 616 |
|
| 617 |
+
def _normalize_score_evidence(
|
| 618 |
+
evidence: dict[str, Any],
|
| 619 |
+
scores: dict[str, int],
|
| 620 |
+
fingerprint: dict[str, Any] | None = None,
|
| 621 |
+
) -> dict[str, dict[str, Any]]:
|
| 622 |
normalized: dict[str, dict[str, Any]] = {}
|
| 623 |
missing: list[str] = []
|
| 624 |
weak: list[str] = []
|
|
|
|
| 635 |
if not metric_ids or not any(m.upper().startswith("VM-") for m in metric_ids):
|
| 636 |
weak.append(field)
|
| 637 |
|
| 638 |
+
required_metric = _required_bottleneck_metric(field, fingerprint)
|
| 639 |
+
if required_metric and required_metric not in {m.upper().split("_", 1)[0] for m in metric_ids}:
|
| 640 |
+
weak.append(field)
|
| 641 |
+
|
| 642 |
workbook_rule = str(item.get("workbook_rule") or "").strip()[:260]
|
| 643 |
manuscript_evidence = str(item.get("manuscript_evidence") or "").strip()[:260]
|
| 644 |
reason = str(item.get("reason") or "").strip()[:320]
|
|
|
|
| 648 |
normalized[field] = {
|
| 649 |
"score": scores[field],
|
| 650 |
"fingerprint_metrics_used": metric_ids[:8],
|
| 651 |
+
"fingerprint_metric_values": {
|
| 652 |
+
metric_id.upper().split("_", 1)[0]: _fingerprint_value(fingerprint, metric_id)
|
| 653 |
+
for metric_id in metric_ids[:8]
|
| 654 |
+
},
|
| 655 |
"workbook_rule": workbook_rule,
|
| 656 |
"manuscript_evidence": manuscript_evidence,
|
| 657 |
"reason": reason,
|
|
|
|
| 665 |
return normalized
|
| 666 |
|
| 667 |
|
| 668 |
+
def _required_bottleneck_metric(field: str, fingerprint: dict[str, Any] | None) -> str | None:
|
| 669 |
+
sentence_mean = _fingerprint_float(fingerprint, "VM-010")
|
| 670 |
+
reading_age = _fingerprint_float(fingerprint, "VM-025")
|
| 671 |
+
syllable_variance = _fingerprint_float(fingerprint, "VM-002")
|
| 672 |
+
|
| 673 |
+
if field == "rhythm" and syllable_variance is not None and syllable_variance > 5:
|
| 674 |
+
return "VM-002"
|
| 675 |
+
if field in {"clarity", "read_aloud_flow"}:
|
| 676 |
+
if sentence_mean is not None and sentence_mean > 30:
|
| 677 |
+
return "VM-010"
|
| 678 |
+
if reading_age is not None and reading_age > 10:
|
| 679 |
+
return "VM-025"
|
| 680 |
+
if field == "commercial_viability" and reading_age is not None and reading_age > 10:
|
| 681 |
+
return "VM-025"
|
| 682 |
+
return None
|
| 683 |
+
|
| 684 |
+
|
| 685 |
+
def _validate_fingerprint_score_alignment(out: dict[str, Any], fingerprint: dict[str, Any] | None) -> None:
|
| 686 |
+
if not isinstance(fingerprint, dict) or not fingerprint:
|
| 687 |
+
return
|
| 688 |
+
|
| 689 |
+
sentence_mean = _fingerprint_float(fingerprint, "VM-010")
|
| 690 |
+
reading_age = _fingerprint_float(fingerprint, "VM-025")
|
| 691 |
+
syllable_variance = _fingerprint_float(fingerprint, "VM-002")
|
| 692 |
+
problems: list[str] = []
|
| 693 |
+
|
| 694 |
+
if sentence_mean is not None and sentence_mean > 30:
|
| 695 |
+
if out["scores"].get("clarity", 0) > 75:
|
| 696 |
+
problems.append(f"clarity={out['scores']['clarity']} exceeds cap 75 while VM-010 sentence mean is {sentence_mean}")
|
| 697 |
+
if out["scores"].get("read_aloud_flow", 0) > 75:
|
| 698 |
+
problems.append(
|
| 699 |
+
f"read_aloud_flow={out['scores']['read_aloud_flow']} exceeds cap 75 while VM-010 sentence mean is {sentence_mean}"
|
| 700 |
+
)
|
| 701 |
+
if reading_age is not None and reading_age > 10:
|
| 702 |
+
if out["scores"].get("read_aloud_flow", 0) > 75:
|
| 703 |
+
problems.append(
|
| 704 |
+
f"read_aloud_flow={out['scores']['read_aloud_flow']} exceeds cap 75 while VM-025 reading age is {reading_age}"
|
| 705 |
+
)
|
| 706 |
+
if out["scores"].get("commercial_viability", 0) > 75:
|
| 707 |
+
problems.append(
|
| 708 |
+
f"commercial_viability={out['scores']['commercial_viability']} exceeds cap 75 while VM-025 reading age is {reading_age}"
|
| 709 |
+
)
|
| 710 |
+
if syllable_variance is not None and syllable_variance > 5 and out["scores"].get("rhythm", 0) > 75:
|
| 711 |
+
problems.append(f"rhythm={out['scores']['rhythm']} exceeds cap 75 while VM-002 syllable variance is {syllable_variance}")
|
| 712 |
+
|
| 713 |
+
if problems:
|
| 714 |
+
raise BridgeValidationError("Fingerprint/score contradiction: " + "; ".join(problems))
|
| 715 |
+
|
| 716 |
+
|
| 717 |
def recompute_gate(metrics: dict[str, int], rubric_matrix: dict[str, Any]) -> str:
|
| 718 |
weights = rubric_matrix.get("weights", {}) if isinstance(rubric_matrix, dict) else {}
|
| 719 |
if not isinstance(weights, dict) or not weights:
|
|
|
|
| 868 |
if not isinstance(fingerprint, dict):
|
| 869 |
return 0
|
| 870 |
return sum(1 for key in fingerprint if str(key).upper().startswith("VM-"))
|
| 871 |
+
|
| 872 |
+
|
| 873 |
+
def _fingerprint_value(fingerprint: dict[str, Any] | None, metric_id: str) -> Any:
|
| 874 |
+
if not isinstance(fingerprint, dict):
|
| 875 |
+
return None
|
| 876 |
+
wanted = str(metric_id).upper().split("_", 1)[0]
|
| 877 |
+
for key, value in fingerprint.items():
|
| 878 |
+
if str(key).upper().startswith(wanted):
|
| 879 |
+
return value
|
| 880 |
+
return None
|
| 881 |
+
|
| 882 |
+
|
| 883 |
+
def _fingerprint_float(fingerprint: dict[str, Any] | None, metric_id: str) -> float | None:
|
| 884 |
+
value = _fingerprint_value(fingerprint, metric_id)
|
| 885 |
+
try:
|
| 886 |
+
if value is None or value == "":
|
| 887 |
+
return None
|
| 888 |
+
return float(value)
|
| 889 |
+
except Exception:
|
| 890 |
+
return None
|