Files
Jyotisha/scripts/rectification/api_service.py
T
Jesse_Chen e8bd3a5208
Independent Staging Quality Gate / validate (push) Successful in 16m46s
Independent Staging Quality Gate / publish (push) Successful in 19m37s
feat(rectification): expose candidate result reports
2026-08-31 05:52:38 +08:00

293 lines
13 KiB
Python

from __future__ import annotations
from typing import Any, Sequence
from uuid import NAMESPACE_URL, uuid5
from scripts.active_rectification_events import build_candidate_result_summary
from scripts.rectification.candidate_feature_service import build_candidate_feature_snapshot
from scripts.rectification.contracts import (
EVENT_CONTRACT_VERSION,
RectificationRequest,
is_primary_scoreable_event,
)
from scripts.rectification.decision_policy import (
EXECUTION_LEDGER_VERSION,
POLICY_VERSION,
build_candidate_decisions,
build_decision_receipt,
build_execution_ledger,
)
from scripts.rectification.diagnostics_service import run_diagnostics
from scripts.rectification.scoring_service import (
ALGORITHM_VERSION,
build_event_contribution_matrix,
calculation_spec,
score_from_matrix,
scoreable_request,
sha256,
)
def _clock_minutes(value: str) -> int:
hour, minute = value[:5].split(":", 1)
return int(hour) * 60 + int(minute)
def _window_width(start_time: str, end_time: str) -> int:
return (_clock_minutes(end_time) - _clock_minutes(start_time)) % 1_440 + 1
def _report_candidate_range(
request: RectificationRequest,
candidate_scores: Sequence[dict[str, Any]],
representative_time: str | None,
) -> dict[str, Any]:
top_score = max((float(row.get("score") or 0) for row in candidate_scores), default=None)
top_times = [
str(row.get("time"))[:5]
for row in candidate_scores
if top_score is not None and float(row.get("score") or 0) == top_score
]
if not top_times:
return {
"start_time": request["start_time"],
"end_time": request["end_time"],
"representative_time": representative_time,
"width_minutes": _window_width(request["start_time"], request["end_time"]),
"representative_is_unique": False,
}
return {
"start_time": top_times[0],
"end_time": top_times[-1],
"representative_time": representative_time or top_times[len(top_times) // 2],
"width_minutes": len(top_times),
"representative_is_unique": False,
}
def _report_evidence(
request: RectificationRequest,
built: dict[str, Any],
representative_time: str | None,
) -> list[dict[str, Any]]:
matrix = built.get("matrix") or {}
rows: list[dict[str, Any]] = []
for event in request.get("events") or []:
if not is_primary_scoreable_event(event):
continue
contribution = (matrix.get(event["id"]) or {}).get(representative_time or "")
contribution = contribution if isinstance(contribution, dict) else {}
points = float(contribution.get("points") or 0)
status = "supporting" if points > 0 else "contradictory" if points < 0 else "unconfirmed"
rows.append({
"event_id": event["id"],
"summary": str(event.get("summary") or "").strip(),
"domain": event["domain"],
"date": {
"start": event["date_start"],
"end": event["date_end"],
"precision": event["precision"],
},
"status": status,
"supports_candidate_time": representative_time if status == "supporting" else None,
"methods": sorted({str(layer) for layer in contribution.get("technique_layers") or []}),
})
return rows
def _report_excluded_candidates(
candidate_decisions: Sequence[dict[str, Any]],
representative_time: str | None,
) -> list[dict[str, Any]]:
return [
{
"time": str(candidate.get("time") or "")[:5],
"reason": "not_the_leading_candidate",
"representative_time": representative_time,
}
for candidate in candidate_decisions
if str(candidate.get("time") or "")[:5] != (representative_time or "")
]
def _confirmation_blockers(receipt: dict[str, Any]) -> list[dict[str, str]]:
allowed = {"VedAstro 分钟级校验", "唯一分钟确认"}
return [
{
"technique": str(row.get("technique")),
"status": str(row.get("status")),
"user_meaning": str(row.get("note") or ""),
}
for row in receipt.get("technique_audit_table") or []
if isinstance(row, dict)
and str(row.get("technique")) in allowed
and str(row.get("status")) != "executed"
]
def _rectification_report(
request: RectificationRequest,
built: dict[str, Any],
candidate_scores: Sequence[dict[str, Any]],
candidate_decisions: Sequence[dict[str, Any]],
receipt: dict[str, Any],
) -> dict[str, Any]:
representative_time = str(receipt.get("representative_time") or "")[:5] or None
blockers = _confirmation_blockers(receipt)
candidate_range = _report_candidate_range(request, candidate_scores, representative_time)
limitations = [item["user_meaning"] for item in blockers if item["user_meaning"]]
if not limitations:
limitations.append("本会话以代表性时间收口,不确认唯一分钟。")
return {
"candidate_range": candidate_range,
"representative_time": representative_time,
"representative_label": "代表性候选,不是唯一解",
"confidence": receipt.get("overall_confidence", "low"),
"evidence": _report_evidence(request, built, representative_time),
"excluded_candidates": _report_excluded_candidates(candidate_decisions, representative_time),
"next_step_codes": [],
"confirmation_gate_blockers": blockers,
"limitations": limitations,
"claim_status": "candidate_range_not_birth_time_truth",
}
def candidate_features(request: RectificationRequest) -> dict[str, Any]:
spec = calculation_spec(request)
spec_hash = sha256(spec)
scoring_request = scoreable_request(request)
return {
"algorithm_version": ALGORITHM_VERSION,
"event_contract_version": EVENT_CONTRACT_VERSION,
"decision_policy_version": POLICY_VERSION,
"calculation_spec": spec,
"calculation_spec_hash": spec_hash,
"candidate_feature_snapshot": build_candidate_feature_snapshot(scoring_request, spec_hash),
"can_confirm_exact_minute": False,
}
def score_candidates(request: RectificationRequest) -> dict[str, Any]:
scoring_request = scoreable_request(request)
built = build_event_contribution_matrix(scoring_request)
rows = score_from_matrix(scoring_request, built)
spec = calculation_spec(request)
spec_hash = sha256(spec)
diagnostic_values = run_diagnostics(scoring_request, rows, built)
fingerprint = sha256(request)
result_id = str(uuid5(NAMESPACE_URL, f"{ALGORITHM_VERSION}:{fingerprint}"))
candidate_decisions = build_candidate_decisions(
rows,
result_id=result_id,
static_contexts=built.get("static_contexts") if isinstance(built.get("static_contexts"), list) else None,
)
decision_receipt = build_decision_receipt(request, candidate_decisions, built, diagnostic_values)
execution_ledger = build_execution_ledger(request, built, diagnostic_values, candidate_decisions)
representative = candidate_decisions[0] if candidate_decisions else None
representative_time = str(representative.get("time") or "")[:5] if representative else None
report_range = _report_candidate_range(request, rows, representative_time)
report_evidence = _report_evidence(request, built, representative_time)
summary_evidence = [
{
"event_id": item["event_id"],
"domain": item["domain"],
"candidate_time": representative_time or "",
"rule_ids": item["methods"],
"points": 1 if item["status"] == "supporting" else -1 if item["status"] == "contradictory" else 0,
}
for item in report_evidence
]
candidate_summary = build_candidate_result_summary({
"winning_segment": report_range,
"event_count": len(scoring_request.get("events", [])),
"margin_percent": decision_receipt.get("margin_percent", 0),
"reasons": decision_receipt.get("reasons", []),
"evidence": summary_evidence,
})
candidate_summary["stability"] = {"label": decision_receipt.get("overall_confidence", "low")}
rectification_report = _rectification_report(
request, built, [{
"time": row["time"],
"score": row["score"],
} for row in rows], candidate_decisions, decision_receipt,
)
rectification_report["next_step_codes"] = candidate_summary["next_step_codes"]
candidate_summary["report"] = rectification_report
return {
"result_id": result_id,
"algorithm_version": ALGORITHM_VERSION,
"event_contract_version": EVENT_CONTRACT_VERSION,
"decision_policy_version": POLICY_VERSION,
"calculation_spec": spec,
"calculation_spec_hash": spec_hash,
"candidate_scores": [{
"time": row["time"],
"score": row["score"],
"supporting_event_ids": [item["event_id"] for item in row["evidence"] if item["points"] > 0],
"conflicting_event_ids": [item["event_id"] for item in row["evidence"] if item["points"] < 0],
} for row in rows],
"candidate_decisions": candidate_decisions,
"candidate_decision_receipt": decision_receipt,
"decision_receipt": decision_receipt,
"execution_ledger_version": EXECUTION_LEDGER_VERSION,
"execution_ledger": execution_ledger,
"event_contribution_matrix": built["matrix"],
"candidate_feature_snapshot": build_candidate_feature_snapshot(
scoring_request, spec_hash, built.get("static_contexts")
),
"diagnostics": diagnostic_values,
"candidate_summary": candidate_summary,
"next_step_codes": candidate_summary["next_step_codes"],
"stability": candidate_summary["stability"],
"rectification_report": rectification_report,
"robustness": {
"neighbor_support_minutes": diagnostic_values.get("neighbor_support_minutes", 0),
"leave_one_out_retention_rate": diagnostic_values.get("leave_one_event_out_retention_rate", 0),
"leave_one_domain_out_retention_rate": diagnostic_values.get("leave_one_domain_out_retention_rate", 0),
"date_sensitivity_retention_rate": diagnostic_values.get("date_sensitivity_retention_rate", 0),
},
"missing_layers": built["missing_layers"],
"display_allowed": decision_receipt["display_allowed"],
"selection_allowed": decision_receipt["selection_allowed"],
"acceptance_allowed": decision_receipt["acceptance_allowed"],
"propose_allowed": decision_receipt["propose_allowed"],
"confirmation_allowed": decision_receipt["confirmation_allowed"],
"representative_candidate_id": representative["candidate_id"] if representative else None,
"representative_time": representative["time"] if representative else None,
"overall_confidence": decision_receipt["overall_confidence"],
"margin_percent": decision_receipt["margin_percent"],
"can_confirm_exact_minute": decision_receipt["confirmation_allowed"],
}
def diagnostics(request: RectificationRequest) -> dict[str, Any]:
scored = score_candidates(request)
return {
"result_id": scored["result_id"],
"algorithm_version": scored["algorithm_version"],
"event_contract_version": scored["event_contract_version"],
"decision_policy_version": scored["decision_policy_version"],
"calculation_spec_hash": scored["calculation_spec_hash"],
"candidate_decisions": scored["candidate_decisions"],
"candidate_decision_receipt": scored["candidate_decision_receipt"],
"decision_receipt": scored["decision_receipt"],
"execution_ledger_version": scored["execution_ledger_version"],
"execution_ledger": scored["execution_ledger"],
"diagnostics": scored["diagnostics"],
"candidate_summary": scored.get("candidate_summary", {"next_step_codes": ["do_not_apply_as_birth_time_truth"]}),
"next_step_codes": scored.get("next_step_codes", ["do_not_apply_as_birth_time_truth"]),
"stability": scored.get("stability", {"label": scored.get("overall_confidence", "low")}),
"rectification_report": scored.get("rectification_report", {}),
"missing_layers": scored["missing_layers"],
"display_allowed": scored["display_allowed"],
"selection_allowed": scored["selection_allowed"],
"acceptance_allowed": scored["acceptance_allowed"],
"propose_allowed": scored["propose_allowed"],
"confirmation_allowed": scored["confirmation_allowed"],
"representative_candidate_id": scored["representative_candidate_id"],
"representative_time": scored["representative_time"],
"overall_confidence": scored["overall_confidence"],
"margin_percent": scored["margin_percent"],
"can_confirm_exact_minute": scored["confirmation_allowed"],
}