refactor: rebuild birth time rectification agent

This commit is contained in:
Jesse_Chen
2026-07-28 13:04:17 +08:00
parent ac5aef5f89
commit 8ade6ed5c8
59 changed files with 4423 additions and 1142 deletions
+111 -12
View File
@@ -14,7 +14,7 @@ import sys
from datetime import date, datetime, time, timedelta
from pathlib import Path
from collections.abc import Sequence
from typing import Final, assert_never
from typing import Any, Final, assert_never
from scripts.active_rectification_events import (
CandidateEvidence,
@@ -316,10 +316,22 @@ def _shadbala_verified_components_auxiliary(natal_chart: dict, birth_hour: float
return [], 0.0
def _candidate_row(
def _feature_hash(value: Any) -> str:
normalized = json.dumps(value, ensure_ascii=True, sort_keys=True, separators=(",", ":"), default=str)
return hashlib.sha256(normalized.encode("utf-8")).hexdigest()
def _arudha_sign(arudha_padas: dict, key: str) -> int | None:
value = arudha_padas.get(key) or {}
sign_index = value.get("sign_idx")
return int(sign_index) if isinstance(sign_index, int) and 0 <= sign_index <= 11 else None
def build_candidate_static_context(
request: RectificationEventRequest,
candidate_at: datetime,
) -> CandidateScoreRow:
) -> dict[str, Any]:
"""Compute every candidate-minute natal layer once for scoring and diagnostics."""
chart = domain_calculation_service.compute_chart({
"year": candidate_at.year,
"month": candidate_at.month,
@@ -340,16 +352,102 @@ def _candidate_row(
ascendant_longitude = float(chart["ascendant"]["lon"])
ascendant_index = int(ascendant_longitude // 30)
arudha = jaimini.calc_arudha_padas(ascendant_index, planet_longitudes)
arudha_padas = {
**(arudha.get("padas") or {}),
"UL": arudha.get("upapada") or {},
}
arudha_padas = {**(arudha.get("padas") or {}), "UL": arudha.get("upapada") or {}}
charts = varga.calc_all_vargas(
planet_longitudes,
ascendant_longitude,
divisions=[2, 4, 9, 10, 24, 30],
)
d11_chart = _d11_chart(planet_longitudes, ascendant_longitude)
varga_charts = {
prefix: d11_chart if prefix == "D11" else _varga_chart(charts, prefix)
for prefix in ("D2", "D4", "D9", "D10", "D11", "D24", "D30")
}
available_layers = ["D1"]
blocked_layers = ["KP_cusps"]
varga_ascendants: dict[str, int] = {}
for prefix, value in varga_charts.items():
ascendant = (value or {}).get("Ascendant") or {}
sign_index = ascendant.get("sign_idx")
if isinstance(sign_index, int) and 0 <= sign_index <= 11:
varga_ascendants[prefix] = sign_index
available_layers.append(prefix)
else:
blocked_layers.append(prefix)
arudha_signs = {key: _arudha_sign(arudha_padas, key) for key in ("A7", "A10", "UL")}
for key, sign_index in arudha_signs.items():
(available_layers if sign_index is not None else blocked_layers).append(key)
ashtakavarga_result = None
try:
ashtakavarga_result = ashtakavarga.calc_ashtakavarga(chart.get("planets", {}), ascendant_index)
available_layers.append("Ashtakavarga")
except (KeyError, TypeError, ValueError):
blocked_layers.append("Ashtakavarga")
shadbala_result = None
try:
shadbala_result = shadbala.calc_shadbala(
chart.get("planets", {}),
str(chart["ascendant"].get("sign")),
candidate_at.hour + candidate_at.minute / 60,
planet_longitudes["Sun"],
planet_longitudes["Moon"],
birth_minute=float(candidate_at.minute),
)
available_layers.append("Shadbala")
except (KeyError, TypeError, ValueError):
blocked_layers.append("Shadbala")
feature_payload = {
"time": candidate_at.strftime("%H:%M"),
"ascendant_degree": ascendant_longitude,
"ascendant_sign_index": ascendant_index,
"varga_ascendants": varga_ascendants,
"arudha_signs": arudha_signs,
"available_layers": sorted(set(available_layers)),
"blocked_layers": sorted(set(blocked_layers)),
"fingerprints": {
"natal": str(chart.get("result_hash") or _feature_hash({"ascendant": chart.get("ascendant"), "planets": chart.get("planets")})),
"vargas": _feature_hash(varga_ascendants),
"arudha": _feature_hash(arudha_signs),
"ashtakavarga": _feature_hash(ashtakavarga_result) if ashtakavarga_result is not None else "blocked",
"shadbala": _feature_hash(shadbala_result) if shadbala_result is not None else "blocked",
},
}
feature_payload["fingerprints"]["static"] = _feature_hash(feature_payload)
return {
"candidate_at": candidate_at,
"chart": chart,
"planet_longitudes": planet_longitudes,
"ascendant_longitude": ascendant_longitude,
"ascendant_index": ascendant_index,
"arudha_padas": arudha_padas,
"varga_charts": varga_charts,
"feature": feature_payload,
}
def compute_candidate_static_contexts(
request: RectificationEventRequest,
*,
candidates: Sequence[datetime] | None = None,
) -> list[dict[str, Any]]:
candidate_datetimes = list(candidates) if candidates is not None else _candidate_datetimes(request)
return [build_candidate_static_context(request, candidate) for candidate in candidate_datetimes]
def _candidate_row(
request: RectificationEventRequest,
context: dict[str, Any],
) -> CandidateScoreRow:
candidate_at = context["candidate_at"]
chart = context["chart"]
planet_longitudes = context["planet_longitudes"]
ascendant_index = context["ascendant_index"]
arudha_padas = context["arudha_padas"]
varga_charts = context["varga_charts"]
moon_longitude = planet_longitudes["Moon"]
evidence: list[CandidateEvidence] = []
missing_layers: list[str] = []
@@ -357,7 +455,7 @@ def _candidate_row(
for event in request["events"]:
event_at = _event_datetime(event)
prefixes, _ = DOMAIN_CONFIG[event["domain"]]
domain_vargas = [d11_chart if prefix == "D11" else _varga_chart(charts, prefix) for prefix in prefixes]
domain_vargas = [varga_charts[prefix] for prefix in prefixes]
if any(chart is None for chart in domain_vargas):
missing_layers.extend(prefixes)
continue
@@ -407,7 +505,7 @@ def _candidate_row(
"time": candidate_at.strftime("%H:%M"),
"score": round(sum(item["points"] for item in evidence), 4),
"evidence": evidence,
"missing_layers": sorted(set(missing_layers)),
"missing_layers": sorted(set(missing_layers + context["feature"]["blocked_layers"])),
}
@@ -501,7 +599,8 @@ def compute_event_candidate_rows(
request: RectificationEventRequest,
*,
candidates: Sequence[datetime] | None = None,
static_contexts: Sequence[dict[str, Any]] | None = None,
) -> list[CandidateScoreRow]:
"""Return every computed minute row without performing release adjudication."""
candidate_datetimes = list(candidates) if candidates is not None else _candidate_datetimes(request)
return [_candidate_row(request, candidate) for candidate in candidate_datetimes]
"""Return every computed minute row while reusing one static chart scan per candidate."""
contexts = list(static_contexts) if static_contexts is not None else compute_candidate_static_contexts(request, candidates=candidates)
return [_candidate_row(request, context) for context in contexts]