fix(rectification): exhaustion exit, explain layer, range reading, unknown-time scan (BUG-565–568)
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:
@@ -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,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@ _EVENT_PROVENANCE_FIELDS = frozenset({
|
||||
})
|
||||
_REQUEST_FIELDS = frozenset({
|
||||
"birth_date", "start_time", "end_time", "lat", "lon", "tz", "events",
|
||||
"ayanamsa", "node_mode", "asked_probe_keys",
|
||||
"ayanamsa", "node_mode", "asked_probe_keys", "minute_step",
|
||||
}) | _REQUEST_PROVENANCE_FIELDS
|
||||
_EVENT_FIELDS = frozenset({"id", "domain", "event_kind", "date_start", "date_end", "precision", "summary"}) | _EVENT_PROVENANCE_FIELDS
|
||||
_CLOCK = re.compile(r"(?:[01]\d|2[0-3]):[0-5]\d\Z")
|
||||
@@ -89,6 +89,7 @@ class RectificationRequest(TypedDict):
|
||||
timezone_source: NotRequired[str | None]
|
||||
local_time_status: NotRequired[str | None]
|
||||
asked_probe_keys: NotRequired[list[str]]
|
||||
minute_step: NotRequired[int]
|
||||
|
||||
|
||||
JsonObject = dict[str, Any]
|
||||
@@ -266,4 +267,10 @@ def normalize_rectification_request(body: Any, *, today: date | None = None) ->
|
||||
seen.add(key)
|
||||
cleaned_keys.append(key)
|
||||
cleaned_request["asked_probe_keys"] = cleaned_keys
|
||||
if "minute_step" in body:
|
||||
minute_step = body.get("minute_step")
|
||||
if isinstance(minute_step, bool) or not isinstance(minute_step, int) or not 1 <= minute_step <= 15:
|
||||
raise ValueError("minute_step must be an integer from 1 to 15")
|
||||
if minute_step != 1:
|
||||
cleaned_request["minute_step"] = minute_step
|
||||
return cast(RectificationRequest, cleaned_request)
|
||||
|
||||
@@ -563,6 +563,7 @@ def build_decision_receipt(
|
||||
representative_time=representative["time"] if representative else None,
|
||||
candidate_times=grid_times,
|
||||
cluster_width_minutes=width,
|
||||
include_discriminators=int(request.get("minute_step") or 1) <= 1,
|
||||
)
|
||||
if packet["dasha_agreement"]["status"] == "conflict":
|
||||
if overall_confidence == "high":
|
||||
|
||||
@@ -548,12 +548,33 @@ def build_refinement_packet(
|
||||
representative_time: str | None,
|
||||
candidate_times: Sequence[str],
|
||||
cluster_width_minutes: int | None = None,
|
||||
include_discriminators: bool = True,
|
||||
) -> dict[str, Any]:
|
||||
scan = window_scan(built)
|
||||
cluster = cluster_scan(built, candidate_times, representative_time, cluster_width_minutes)
|
||||
ledger = event_dasha_ledger(request, built, representative_time)
|
||||
agreement = dasha_agreement(built, candidate_times)
|
||||
stage = precision_stage(cluster, len(request.get("events") or []))
|
||||
if not include_discriminators:
|
||||
return {
|
||||
"window_scan": scan,
|
||||
"event_dasha_ledger": ledger,
|
||||
"event_fit_rate": event_fit_rate(ledger),
|
||||
"dasha_agreement": agreement,
|
||||
"lagna_contrast": lagna_contrast(built),
|
||||
"nakshatra_boundary": nakshatra_boundary(built, representative_time),
|
||||
"precision_stage": {"current": "block_scan"},
|
||||
"oos_blind_prompts": [],
|
||||
"discriminating_event_probes": [],
|
||||
"event_clarification_probes": [],
|
||||
"evidence_collection_probes": [],
|
||||
"candidate_contrast_opportunities": [],
|
||||
"holdout_validation_probes": [],
|
||||
"dropped_probes": [],
|
||||
"prospective_probes": [],
|
||||
"unique_minute_claim": False,
|
||||
"confirmation_allowed": False,
|
||||
}
|
||||
from scripts.rectification.candidate_contrast import PROBE_PHASE_HOLDOUT_VALIDATION, event_year
|
||||
from scripts.rectification.case_holdout import reserved_holdout_events
|
||||
from scripts.rectification.event_probes import (
|
||||
|
||||
@@ -116,7 +116,7 @@ def _legacy_request(request: RectificationRequest, event: LifeEvent, sampled_dat
|
||||
"date": sampled_date, "precision": "day", "summary": event.get("summary", ""),
|
||||
}],
|
||||
}
|
||||
for key in ("ayanamsa", "node_mode"):
|
||||
for key in ("ayanamsa", "node_mode", "minute_step"):
|
||||
if key in request:
|
||||
legacy_request[key] = request[key]
|
||||
return legacy_request
|
||||
@@ -355,7 +355,7 @@ def calculation_spec(request: RectificationRequest) -> dict[str, Any]:
|
||||
"timezoneOffsetHours": json_number(request["tz"]),
|
||||
"ayanamsa": request.get("ayanamsa", "raman"),
|
||||
"nodeMode": request.get("node_mode", "mean"),
|
||||
"minuteStep": 1,
|
||||
"minuteStep": int(request.get("minute_step") or 1),
|
||||
}
|
||||
for source, target in (
|
||||
("birth_time_source", "birthTimeSource"),
|
||||
|
||||
Reference in New Issue
Block a user