Cache work that depends only on the candidate minute, drop unread response fields, and reject windows above 241 candidates before any chart is built. Eleven canonical scenario hashes match 77057a819. Sealed and reported-offset replays match the 2026-10-08 records (changed_trial_count 8 and 581) and are stored as scoring_perf_2026_10_13.
Co-Authored-By: Grok (agent loop)
600 lines
25 KiB
Python
600 lines
25 KiB
Python
from __future__ import annotations
|
|
|
|
from typing import Any, Mapping, Sequence
|
|
from uuid import NAMESPACE_URL, uuid5
|
|
|
|
from scripts.active_rectification_events import build_candidate_result_summary
|
|
from scripts.offline_compute_mixins import BadRequest
|
|
from scripts.rectification.candidate_feature_service import build_candidate_feature_snapshot
|
|
from scripts.rectification.candidate_window import enumerate_candidate_window
|
|
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 "candidate_intervals" in request:
|
|
from scripts.rectification.candidate_window import candidate_positions, enumerate_candidate_window, intervals_from_positions, interval_union_width
|
|
positions = candidate_positions(request, enumerate_candidate_window(request))
|
|
selected = [row for row in positions if not top_times or row["time"] in top_times]
|
|
parts = intervals_from_positions(selected)
|
|
return {
|
|
"start_time": selected[0]["time"], "end_time": selected[-1]["time"],
|
|
"candidate_intervals": [{"start_at": part["start_at"], "end_at": part["end_at"]} for part in parts],
|
|
"representative_time": representative_time or selected[len(selected) // 2]["time"],
|
|
"width_minutes": interval_union_width(parts), "representative_is_unique": False,
|
|
}
|
|
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 _window_span_minutes(start_time: str, end_time: str) -> int:
|
|
start = _clock_minutes(start_time)
|
|
end = _clock_minutes(end_time)
|
|
return end - start if end >= start else 1440 - start + end
|
|
|
|
|
|
def _adaptive_minute_step(start_time: str, end_time: str) -> int:
|
|
width = _window_span_minutes(start_time, end_time)
|
|
if width > 360:
|
|
return 10
|
|
if width > 180:
|
|
return 5
|
|
return 2
|
|
|
|
|
|
def _block_scan_periods(request: RectificationRequest) -> list[tuple[str, str, str]]:
|
|
custom = request.get("blocks")
|
|
if isinstance(custom, list) and custom:
|
|
periods: list[tuple[str, str, str]] = []
|
|
for item in custom:
|
|
if not isinstance(item, dict):
|
|
continue
|
|
label = str(item.get("period") or item.get("label") or "")
|
|
start_time = str(item.get("start_time") or "")[:5]
|
|
end_time = str(item.get("end_time") or "")[:5]
|
|
if label and start_time and end_time:
|
|
periods.append((label, start_time, end_time))
|
|
if periods:
|
|
return periods
|
|
return list(BLOCK_SCAN_PERIODS)
|
|
|
|
|
|
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",
|
|
}
|
|
|
|
|
|
MAX_CANDIDATES_PER_REQUEST = 241
|
|
|
|
|
|
class TooManyCandidates(BadRequest):
|
|
"""Rejected before any chart is computed.
|
|
|
|
The API server maps every BadRequest to HTTP 400 with error_code
|
|
ERR_BAD_REQUEST and this task must not edit that server. The token
|
|
too_many_candidates lives in the message, with the cap and the count.
|
|
"""
|
|
|
|
code = "too_many_candidates"
|
|
|
|
def __init__(self, count: int) -> None:
|
|
self.count = int(count)
|
|
self.limit = MAX_CANDIDATES_PER_REQUEST
|
|
super().__init__(f"too_many_candidates: limit={self.limit} count={self.count}")
|
|
|
|
|
|
def _candidate_count(request: RectificationRequest) -> int:
|
|
return len(enumerate_candidate_window(request))
|
|
|
|
|
|
def _reject_if_too_many_candidates(request: RectificationRequest) -> None:
|
|
count = _candidate_count(request)
|
|
if count > MAX_CANDIDATES_PER_REQUEST:
|
|
raise TooManyCandidates(count)
|
|
|
|
|
|
def engine_scoring_versions() -> dict[str, str]:
|
|
"""Identity fields also returned by `/api/rectification/v5/score`, without scoring."""
|
|
return {
|
|
"algorithm_version": ALGORITHM_VERSION,
|
|
"event_contract_version": EVENT_CONTRACT_VERSION,
|
|
"decision_policy_version": POLICY_VERSION,
|
|
}
|
|
|
|
|
|
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,
|
|
*,
|
|
include_internal: bool = False,
|
|
enforce_candidate_cap: bool = True,
|
|
) -> dict[str, Any]:
|
|
if enforce_candidate_cap:
|
|
_reject_if_too_many_candidates(request)
|
|
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({
|
|
key: value
|
|
for key, value in request.items()
|
|
if key not in {"asked_probe_keys", "dropped_asked_probe_keys", "declined_domains", "column_times", "refresh_probes"}
|
|
})
|
|
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)
|
|
from scripts.rectification.candidate_window import candidate_intervals
|
|
decision_receipt.update({"candidate_window_contract": "dated-v1", "candidate_intervals": candidate_intervals(request),
|
|
"candidate_timezone_offset": request["tz"], "candidate_timezone_id": request.get("timezone_id", "")})
|
|
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
|
|
payload = {
|
|
"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,
|
|
"decision_receipt": decision_receipt,
|
|
"execution_ledger_version": EXECUTION_LEDGER_VERSION,
|
|
"execution_ledger": execution_ledger,
|
|
"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"],
|
|
}
|
|
if include_internal:
|
|
payload["candidate_decision_receipt"] = decision_receipt
|
|
payload["event_contribution_matrix"] = built["matrix"]
|
|
return payload
|
|
|
|
|
|
def diagnostics(
|
|
request: RectificationRequest,
|
|
*,
|
|
enforce_candidate_cap: bool = True,
|
|
) -> dict[str, Any]:
|
|
if enforce_candidate_cap:
|
|
_reject_if_too_many_candidates(request)
|
|
scored = score_candidates(request, enforce_candidate_cap=enforce_candidate_cap)
|
|
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"],
|
|
"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"],
|
|
}
|
|
|
|
|
|
def range_reading(request: Mapping[str, Any]) -> dict[str, Any]:
|
|
"""Stable vs minute-sensitive themes for one unresolved clock window."""
|
|
from types import SimpleNamespace
|
|
|
|
from scripts.jyotish_engine import _build_birth_time_sensitivity
|
|
|
|
body = dict(request or {})
|
|
birth_date = str(body.get("birth_date") or "").strip()
|
|
if birth_date:
|
|
year_text, month_text, day_text = birth_date.split("-", 2)
|
|
year, month, day = int(year_text), int(month_text), int(day_text)
|
|
else:
|
|
year, month, day = int(body["year"]), int(body["month"]), int(body["day"])
|
|
representative = str(body.get("representative_time") or "")[:5]
|
|
if len(representative) == 5 and representative[2] == ":":
|
|
hour, minute = int(representative[:2]), int(representative[3:])
|
|
else:
|
|
hour = int(body.get("hour") or 12)
|
|
minute = int(body.get("minute") or 0)
|
|
representative = f"{hour:02d}:{minute:02d}"
|
|
raw_range = body.get("candidate_range")
|
|
if isinstance(raw_range, Mapping):
|
|
start_time = str(raw_range.get("start_time") or "")[:5]
|
|
end_time = str(raw_range.get("end_time") or "")[:5]
|
|
representative = str(raw_range.get("representative_time") or representative)[:5]
|
|
else:
|
|
start_time = str(body.get("start_time") or "")[:5]
|
|
end_time = str(body.get("end_time") or "")[:5]
|
|
hour, minute = int(representative[:2]), int(representative[3:])
|
|
accuracy = str(body.get("birth_time_accuracy") or "provisional")
|
|
args = SimpleNamespace(
|
|
year=year,
|
|
month=month,
|
|
day=day,
|
|
hour=hour,
|
|
minute=minute,
|
|
second=0,
|
|
lat=float(body["lat"]),
|
|
lon=float(body["lon"]),
|
|
tz=float(body["tz"]),
|
|
ayanamsa=body.get("ayanamsa") or "raman",
|
|
node_mode=body.get("node_mode") or "mean",
|
|
birth_time_accuracy=accuracy,
|
|
candidate_range={
|
|
"start_time": start_time,
|
|
"end_time": end_time,
|
|
"representative_time": representative,
|
|
},
|
|
representative_time=representative,
|
|
declared_window_start=None,
|
|
declared_window_end=None,
|
|
uncertainty_before_minutes=None,
|
|
uncertainty_after_minutes=None,
|
|
)
|
|
sensitivity = _build_birth_time_sensitivity(args)
|
|
themes = sensitivity.get("theme_sensitivity")
|
|
themes = themes if isinstance(themes, dict) else {}
|
|
stable = [
|
|
key for key, row in themes.items()
|
|
if isinstance(row, dict) and row.get("status") == "stable"
|
|
]
|
|
sensitive = [
|
|
key for key, row in themes.items()
|
|
if isinstance(row, dict) and row.get("status") == "sensitive"
|
|
]
|
|
return {
|
|
"window": sensitivity.get("window"),
|
|
"stable_themes": stable,
|
|
"sensitive_themes": sensitive,
|
|
"claim_boundary": sensitivity.get("claim_boundary"),
|
|
"theme_sensitivity": themes,
|
|
"accuracy": sensitivity.get("accuracy"),
|
|
"status": sensitivity.get("status"),
|
|
}
|
|
|
|
|
|
BLOCK_SCAN_PERIODS: tuple[tuple[str, str, str], ...] = (
|
|
("early_morning", "04:00", "07:59"),
|
|
("morning", "08:00", "11:59"),
|
|
("afternoon", "12:00", "17:59"),
|
|
("evening", "18:00", "22:59"),
|
|
("late_night", "23:00", "03:59"),
|
|
)
|
|
|
|
|
|
def _clock_in_declared_period(clock: str, start_time: str, end_time: str) -> bool:
|
|
current = _clock_minutes(clock[:5])
|
|
start = _clock_minutes(start_time)
|
|
end = _clock_minutes(end_time)
|
|
if start <= end:
|
|
return start <= current <= end
|
|
return current >= start or current <= end
|
|
|
|
|
|
def _normalize_relative_support(raw: Sequence[float]) -> list[float]:
|
|
floored = [max(0.0, float(value)) for value in raw]
|
|
total = sum(floored)
|
|
if not floored:
|
|
return []
|
|
if total <= 0:
|
|
shares = [round(100.0 / len(floored), 1) for _ in floored]
|
|
else:
|
|
shares = [round(100.0 * value / total, 1) for value in floored]
|
|
delta = round(100.0 - sum(shares), 1)
|
|
if shares:
|
|
shares[shares.index(max(shares))] = round(shares[shares.index(max(shares))] + delta, 1)
|
|
return shares
|
|
|
|
|
|
def block_scan(
|
|
request: RectificationRequest,
|
|
*,
|
|
enforce_candidate_cap: bool = True,
|
|
) -> dict[str, Any]:
|
|
"""Aggregate event scores into declared periods or caller-supplied sub-blocks."""
|
|
requested_step = request.get("minute_step")
|
|
if isinstance(requested_step, int) and requested_step > 1:
|
|
step = requested_step
|
|
else:
|
|
step = _adaptive_minute_step(request["start_time"], request["end_time"])
|
|
if not request.get("blocks"):
|
|
step = int(request.get("minute_step") or 10)
|
|
if step <= 1:
|
|
step = 10
|
|
scoring_request = {**request, "minute_step": step}
|
|
if enforce_candidate_cap:
|
|
_reject_if_too_many_candidates(scoring_request)
|
|
scored = score_candidates(scoring_request, enforce_candidate_cap=enforce_candidate_cap)
|
|
events_by_id = {
|
|
str(event.get("id") or ""): event
|
|
for event in request.get("events") or []
|
|
if isinstance(event, dict)
|
|
}
|
|
rows = [
|
|
row for row in scored.get("candidate_scores") or []
|
|
if isinstance(row, dict) and str(row.get("time") or "")[:5]
|
|
]
|
|
day_scores = [float(row.get("score") or 0) for row in rows]
|
|
min_day = min(day_scores) if day_scores else 0.0
|
|
periods = _block_scan_periods(request)
|
|
raw_support: list[float] = []
|
|
blocks: list[dict[str, Any]] = []
|
|
for period, start_time, end_time in periods:
|
|
members = [
|
|
row for row in rows
|
|
if _clock_in_declared_period(str(row.get("time") or "")[:5], start_time, end_time)
|
|
]
|
|
counts: dict[str, int] = {}
|
|
for row in members:
|
|
for event_id in row.get("supporting_event_ids") or []:
|
|
key = str(event_id)
|
|
if key:
|
|
counts[key] = counts.get(key, 0) + 1
|
|
top_events = []
|
|
for event_id, _count in sorted(counts.items(), key=lambda item: (-item[1], item[0]))[:3]:
|
|
event = events_by_id.get(event_id) or {}
|
|
top_events.append({
|
|
"event_id": event_id,
|
|
"domain": event.get("domain"),
|
|
"summary": event.get("summary"),
|
|
})
|
|
member_scores = [float(row.get("score") or 0) for row in members]
|
|
mean = (sum(member_scores) / len(member_scores)) if member_scores else 0.0
|
|
raw_support.append(max(mean - min_day, 0.0))
|
|
from scripts.rectification.candidate_window import narrow_candidate_intervals
|
|
blocks.append({
|
|
"candidate_intervals": narrow_candidate_intervals(request, start_time, end_time),
|
|
"period": period,
|
|
"start_time": start_time,
|
|
"end_time": end_time,
|
|
"relative_support": 0,
|
|
"top_events": top_events,
|
|
"candidate_count": len(members),
|
|
})
|
|
shares = _normalize_relative_support(raw_support)
|
|
for block, share in zip(blocks, shares):
|
|
block["relative_support"] = share
|
|
receipt = scored.get("decision_receipt") if isinstance(scored.get("decision_receipt"), dict) else {}
|
|
return {
|
|
"result_id": scored.get("result_id"),
|
|
"algorithm_version": scored.get("algorithm_version"),
|
|
"calculation_spec": scored.get("calculation_spec"),
|
|
"calculation_spec_hash": scored.get("calculation_spec_hash"),
|
|
"minute_step": step,
|
|
"candidate_count": len(rows),
|
|
"precision_stage": {"current": "block_scan"},
|
|
"blocks": blocks,
|
|
"discriminating_event_probes": [],
|
|
"guided_collect_windows": [],
|
|
"acceptance_allowed": False,
|
|
"selection_allowed": False,
|
|
"display_allowed": False,
|
|
"decision_receipt": {
|
|
**receipt,
|
|
"precision_stage": {"current": "block_scan"},
|
|
"discriminating_event_probes": [],
|
|
"guided_collect_windows": [],
|
|
"acceptance_allowed": False,
|
|
"selection_allowed": False,
|
|
},
|
|
}
|