fix(rectification): exhaustion exit, explain layer, range reading, unknown-time scan (BUG-565–568)
Independent Staging Quality Gate / validate (push) Successful in 9m20s
Independent Staging Quality Gate / publish (push) Successful in 6m51s

Keep askable cards after exhaustion, explain each probe, read the adopted credible range in reports and chat, and compare declared periods before the minute grid when the clock is unknown.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Jesse_Chen
2026-09-07 09:10:37 +08:00
parent 43a26a3ccb
commit 814c924e4a
91 changed files with 5362 additions and 224 deletions
+180 -1
View File
@@ -1,6 +1,6 @@
from __future__ import annotations
from typing import Any, Sequence
from typing import Any, Mapping, Sequence
from uuid import NAMESPACE_URL, uuid5
from scripts.active_rectification_events import build_candidate_result_summary
@@ -303,3 +303,182 @@ def diagnostics(request: RectificationRequest) -> dict[str, Any]:
"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 total <= 0:
return [20.0 for _ in floored]
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) -> dict[str, Any]:
"""Aggregate 24h event scores into the five declared birth-time periods."""
step = int(request.get("minute_step") or 10)
if step <= 1:
step = 10
scoring_request = {**request, "minute_step": step}
scored = score_candidates(scoring_request)
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]
]
raw_support: list[float] = []
blocks: list[dict[str, Any]] = []
for period, start_time, end_time in BLOCK_SCAN_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"),
})
raw_support.append(sum(float(row.get("score") or 0) for row in members))
blocks.append({
"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": [],
"acceptance_allowed": False,
"selection_allowed": False,
"display_allowed": False,
"decision_receipt": {
**receipt,
"precision_stage": {"current": "block_scan"},
"discriminating_event_probes": [],
"acceptance_allowed": False,
"selection_allowed": False,
},
}