Files
Jyotisha/scripts/rectification/probe_question_contract.py
T
Jesse_Chen 63b4591aae
Independent Staging Quality Gate / validate (push) Failing after 17m9s
Independent Staging Quality Gate / publish (push) Has been skipped
fix(rectification): surface dropped probes and filter tools by the decision
Silent unrenderable discriminators, a missing question-contract golden, and a always-on tool table were hiding fail-closed drops behind the prompt wall.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-28 20:31:15 +08:00

219 lines
8.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Shared probe → choice-card contract for Python event probes.
Must stay aligned with frontend/src/lib/rectification-agentic/v9/probe-question-contract.ts.
Canonical bytes live in contracts/probe-question-v1.json.
"""
from __future__ import annotations
import json
import re
from pathlib import Path
from typing import Any, Sequence, TypedDict
QUESTION_CONTRACT_VERSION = "probe-question-v1"
ANSWER_CLASSES = ("yes", "weak_yes", "no", "unsure")
EXISTENCE_STYLE_OPTIONS: tuple[dict[str, str], ...] = (
{"label": "明确发生且时间吻合", "answer_class": "yes"},
{"label": "发生过但程度较弱", "answer_class": "weak_yes"},
{"label": "明确没有发生", "answer_class": "no"},
{"label": "这段记不清楚", "answer_class": "unsure"},
)
QUALITY_STYLE_OPTIONS: tuple[dict[str, str], ...] = (
{"label": "发挥明显失常或压力很大", "answer_class": "yes"},
{"label": "有压力但不算明显失常", "answer_class": "weak_yes"},
{"label": "发挥正常、没有明显失常", "answer_class": "no"},
{"label": "这段记不清楚", "answer_class": "unsure"},
)
VARGA_NONE_STYLE_OPTION = {"label": "都不是这些特质", "answer_class": "no"}
UNSURE_STYLE_OPTION = {"label": "这段记不清楚", "answer_class": "unsure"}
FORBIDDEN_COPY_TOKENS = ("外貌", "体质", "胎记", "疤痕", "伤疤", "身高", "体型")
LABEL_MIN = 4
LABEL_MAX = 80
_FORBIDDEN = FORBIDDEN_COPY_TOKENS
_CLOCK = re.compile(r"(?:[01]?\d|2[0-3]):[0-5]\d")
_ANSWER_CLASS_SUFFIX = re.compile(r"·(?:yes|weak_yes|no|unsure)$")
_CONTRACT_PATH = Path(__file__).resolve().parents[2] / "contracts" / "probe-question-v1.json"
class StyleOptionsOk(TypedDict):
ok: bool
options: list[dict[str, str]]
class StyleOptionsErr(TypedDict):
ok: bool
reason: str
class ProbeRenderOk(TypedDict):
ok: bool
class ProbeRenderErr(TypedDict):
ok: bool
reason: str
def probe_question_kind(value: Any) -> str:
if value in {"varga_style", "event_quality"}:
return str(value)
return "existence"
def question_contract_version_is_compatible(value: Any) -> bool:
if value is None:
return True
if isinstance(value, str):
return value == QUESTION_CONTRACT_VERSION
if not isinstance(value, dict):
return False
version = value.get("version") or value.get("question_contract_version")
return version is None or version == QUESTION_CONTRACT_VERSION
def probe_question_contract_payload() -> dict[str, Any]:
return {
"version": QUESTION_CONTRACT_VERSION,
"answer_classes": list(ANSWER_CLASSES),
"label_min": LABEL_MIN,
"label_max": LABEL_MAX,
"forbidden_copy_tokens": list(FORBIDDEN_COPY_TOKENS),
"existence_style_options": [dict(item) for item in EXISTENCE_STYLE_OPTIONS],
"quality_style_options": [dict(item) for item in QUALITY_STYLE_OPTIONS],
"varga_none_style_option": dict(VARGA_NONE_STYLE_OPTION),
"unsure_style_option": dict(UNSURE_STYLE_OPTION),
}
def canonical_probe_question_contract_json() -> str:
return json.dumps(probe_question_contract_payload(), ensure_ascii=False, indent=2) + "\n"
def load_probe_question_contract_golden() -> str:
return _CONTRACT_PATH.read_text(encoding="utf-8")
def clipped_probe_label(value: Any, minimum: int = LABEL_MIN, maximum: int = LABEL_MAX) -> str | None:
if not isinstance(value, str):
return None
text = " ".join(value.split())
if len(text) < minimum or len(text) > maximum:
return None
if any(token in text for token in _FORBIDDEN) or _CLOCK.search(text):
return None
return text
def _label_reject_reason(value: Any) -> str | None:
if not isinstance(value, str):
return "empty"
text = " ".join(value.split())
if len(text) < LABEL_MIN or len(text) > LABEL_MAX:
return "label_length"
if any(token in text for token in _FORBIDDEN) or _CLOCK.search(text):
return "forbidden_copy"
return None
def _incoming_option(row: Any) -> dict[str, str] | dict[str, str] | None:
if not isinstance(row, dict):
return None
answer = row.get("answer_class") or row.get("answerClass")
reject = _label_reject_reason(row.get("label"))
if reject == "empty" or answer not in ANSWER_CLASSES:
return None
if reject:
return {"reason": reject}
label = clipped_probe_label(row.get("label"))
if not label:
return {"reason": "not_renderable"}
payload = {"label": label, "answer_class": str(answer)}
sign = row.get("sign")
if isinstance(sign, str) and sign.strip():
payload["sign"] = sign.strip()
return payload
def complete_style_options(
choice_kind: Any,
style_options: Sequence[Any] | None = None,
) -> StyleOptionsOk | StyleOptionsErr:
kind = probe_question_kind(choice_kind)
incoming_reason: str | None = None
incoming: list[dict[str, str]] = []
for row in style_options or []:
parsed = _incoming_option(row)
if not parsed:
continue
if "reason" in parsed and "label" not in parsed:
incoming_reason = incoming_reason or str(parsed["reason"])
continue
incoming.append(parsed)
by_class: dict[str, dict[str, str]] = {}
if kind == "varga_style":
for option in incoming:
by_class[option["answer_class"]] = option
by_class.setdefault("unsure", dict(UNSURE_STYLE_OPTION))
scoring = [item for item in ANSWER_CLASSES if item != "unsure" and item in by_class]
if len(scoring) < 2:
return {"ok": False, "reason": incoming_reason or "varga_insufficient_scoring"}
by_class.setdefault("no", dict(VARGA_NONE_STYLE_OPTION))
if "yes" not in by_class or "weak_yes" not in by_class:
return {"ok": False, "reason": "varga_missing_weak_yes"}
else:
catalog = QUALITY_STYLE_OPTIONS if kind == "event_quality" else EXISTENCE_STYLE_OPTIONS
for option in catalog:
by_class[option["answer_class"]] = dict(option)
for option in incoming:
by_class[option["answer_class"]] = option
ordered: list[dict[str, str]] = []
seen: set[str] = set()
for index, answer_class in enumerate(ANSWER_CLASSES):
option = by_class.get(answer_class)
if not option:
return {"ok": False, "reason": "not_renderable"}
label = option["label"]
if label in seen and option.get("sign"):
label = f"{label}{option['sign']}"
if label in seen:
label = f"{option['label']}·{index + 1}"
seen.add(label)
ordered.append({**option, "label": label})
labels = {item["label"] for item in ordered}
classes = {item["answer_class"] for item in ordered}
if (
len(ordered) != 4
or len(labels) != 4
or classes != set(ANSWER_CLASSES)
or any(_ANSWER_CLASS_SUFFIX.search(item["label"]) for item in ordered)
):
return {"ok": False, "reason": "not_renderable"}
return {"ok": True, "options": ordered}
def completed_style_options(
choice_kind: Any,
style_options: Sequence[Any] | None = None,
) -> list[dict[str, str]] | None:
result = complete_style_options(choice_kind, style_options)
if result.get("ok"):
return result.get("options") # type: ignore[return-value]
return None
def is_renderable_probe(probe: dict[str, Any]) -> ProbeRenderOk | ProbeRenderErr:
gain = probe.get("information_gain")
if not isinstance(gain, (int, float)) or gain <= 0:
return {"ok": False, "reason": "zero_gain"}
candidate_ids = probe.get("candidate_ids") or []
outcomes = probe.get("expected_outcomes") or []
if len(candidate_ids) < 2:
return {"ok": False, "reason": "insufficient_candidates"}
if len(outcomes) < 2:
return {"ok": False, "reason": "insufficient_outcomes"}
completed = complete_style_options(probe.get("choice_kind"), probe.get("style_options"))
if not completed.get("ok"):
return {"ok": False, "reason": str(completed.get("reason") or "not_renderable")}
return {"ok": True}