Files
Jyotisha/scripts/annual_pyjhora_replay.py
T
Jesse_Chen bab0718700
Independent Staging Quality Gate / validate (push) Failing after 9m41s
Independent Staging Quality Gate / publish (push) Has been skipped
feat(report): ship full-mode longform appendix beside the five-chapter report
Web export now calls the same full pack as the long skill report and caches an owner-only Markdown download. Appendix failure stays unavailable and does not change the main report status.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-05 11:10:44 +08:00

987 lines
40 KiB
Python
Executable File

"""PyJHora annual replay boundary.
PyJHora/JHora is treated as an external reference engine. This wrapper imports
it only inside `replay_pyjhora_annual`, preserves raw return shapes, and never
vendors or normalizes it into the native engine as a hard dependency.
"""
from __future__ import annotations
import contextlib
import hashlib
import json
import subprocess
import sys
from datetime import datetime, timedelta
from importlib.metadata import PackageNotFoundError, version as _distribution_version
from importlib import import_module as _import_module
from inspect import signature
from pathlib import Path
from typing import Any, Callable
ENGINE = "PyJHora/JHora"
REPO_ROOT = Path(__file__).resolve().parents[1]
_TAJAKA_YEAR_LORD_MODULE = "jhora.horoscope.transit.tajaka"
_YEAR_LORD_MODULES = (
"jhora.horoscope.dhasa.annual.mudda",
"jhora.horoscope.dhasa.annual.patyayini",
)
_YEAR_LORD_CALLABLES = (
"panchadhikari_year_lord",
"get_panchadhikari_year_lord",
"get_year_lord",
"year_lord",
)
_ANNUAL_SURFACE_KEYS = (
"annual_chart_snapshot",
"annual_chart_speed_snapshot",
"mudda_dasha",
"patyayini_dasha",
"sahams",
"solar_return_boundary",
"tajika_yogas",
)
_PYJHORA_SAHAM_CALLABLES = (
"apamrithyu_saham", "artha_saham", "asha_saham", "bandhana_saham", "bandhu_saham",
"bhratri_saham", "gaurava_saham", "jadya_saham", "jalapatna_saham", "jeeva_saham",
"kali_saham", "karma_saham", "karyasiddhi_saham", "laabha_saham", "maathri_saham",
"mahatmaya_saham", "mitra_saham", "mrithyu_saham", "paradara_saham", "paradesa_saham",
"pithri_saham", "preethi_saham", "punya_saham", "puthra_saham", "rajya_saham",
"roga_sagam_1", "roga_saham", "samartha_saham", "santapa_saham", "sastra_saham",
"sathru_saham", "sraddha_saham", "vanika_saham", "vidya_saham", "vivaha_saham",
"vyaapaara_saham", "yasas_saham",
)
def probe_pyjhora_year_lord(
profile: dict[str, Any],
*,
birth_julian_day: float,
place: Any,
age: int | float,
import_module: Callable[[str], Any] = _import_module,
distribution_version: Callable[[str], str] = _distribution_version,
isolated_replay: Callable[[dict[str, Any], float, Any, int | float], dict[str, Any]] | None = None,
) -> dict[str, Any]:
"""Probe PyJHora for an annual Tajaka/Panchadhikari Year Lord API.
This is deliberately an external-engine observation. PyJHora's annual
modules are not assumed to implement Panchadhikari simply because they
expose other Varshaphala calculations. The public Tajaka callable is
preferred when present; annual-dasha modules remain a legacy fallback.
"""
replay = {
"status": "blocked",
"reason": "pyjhora_panchadhikari_year_lord_api_missing",
"missing_api": "Panchadhikari/Year Lord callable",
"probed_modules": [_TAJAKA_YEAR_LORD_MODULE, *_YEAR_LORD_MODULES],
"available_annual_callables": [],
"raw": None,
"normalized_candidates": [],
}
result = {
"engine": ENGINE,
"input_profile_id": profile.get("profile_id"),
"pyjhora_version": _pyjhora_version(distribution_version),
"license_boundary": {
"mode": "external_reference_only",
"dependency_required": False,
},
"evidence_scope": "pyjhora_behavior_only",
"parity_status": "not_multiengine_parity",
"year_lord_replay": replay,
}
replay_runner = isolated_replay or _run_isolated_tajaka_year_lord
isolated = replay_runner(profile, birth_julian_day, place, age)
if isolated.get("reason") != "pyjhora_tajaka_api_missing":
replay.update(isolated)
replay["normalized_candidates"] = _normalize_tajaka_candidates(replay.get("raw_candidates"))
if isolated.get("pyjhora_version"):
result["pyjhora_version"] = isolated["pyjhora_version"]
return result
modules: list[Any] = []
for module_name in _YEAR_LORD_MODULES:
try:
modules.append(import_module(module_name))
except Exception as exc:
replay["status"] = "blocked"
replay["reason"] = f"pyjhora_annual_module_import_failed:{module_name}:{exc.__class__.__name__}"
replay["missing_api"] = module_name
return result
for module in modules:
replay["available_annual_callables"].extend(
sorted(name for name in _YEAR_LORD_CALLABLES if callable(getattr(module, name, None)))
)
for name in _YEAR_LORD_CALLABLES:
candidate = getattr(module, name, None)
if not callable(candidate):
continue
return _call_year_lord_candidate(result, replay, candidate, name, birth_julian_day, place, age)
return result
def _run_isolated_tajaka_year_lord(
profile: dict[str, Any],
birth_julian_day: float,
place: Any,
age: int | float,
) -> dict[str, Any]:
settings = _replay_settings(profile)
request = {
"birth_julian_day": birth_julian_day,
"place": _place_payload(place),
"age": age,
"settings": settings,
}
request_hash = _request_hash(request)
command = [
sys.executable,
"-c",
(
"import json, sys; "
"from scripts.annual_pyjhora_replay import _isolated_tajaka_worker; "
"print(json.dumps(_isolated_tajaka_worker(json.load(sys.stdin)), sort_keys=True))"
),
]
try:
completed = subprocess.run(
command,
input=json.dumps(request, sort_keys=True),
capture_output=True,
text=True,
timeout=30,
check=False,
cwd=str(REPO_ROOT),
)
except subprocess.TimeoutExpired:
return _isolated_blocked(
"pyjhora_isolated_replay_timeout", request_hash, settings
)
except Exception as exc:
return _isolated_blocked(
f"pyjhora_isolated_replay_start_failed:{exc.__class__.__name__}", request_hash, settings
)
if completed.returncode != 0:
return _isolated_blocked(
f"pyjhora_isolated_replay_failed:exit_{completed.returncode}", request_hash, settings
)
try:
replay = json.loads(completed.stdout)
except json.JSONDecodeError:
return _isolated_blocked("pyjhora_isolated_replay_invalid_json", request_hash, settings)
if not isinstance(replay, dict):
return _isolated_blocked("pyjhora_isolated_replay_invalid_payload", request_hash, settings)
replay["request_hash"] = request_hash
return replay
def _isolated_tajaka_worker(request: dict[str, Any]) -> dict[str, Any]:
"""Run in a child process so PyJHora global Ayanamsa state cannot leak."""
settings = dict(request.get("settings") or {})
node_mode = {
"requested": settings.get("node_mode"),
"status": "unsupported_by_replay_adapter",
}
ayanamsa = settings.get("ayanamsa")
if not isinstance(ayanamsa, str) or not ayanamsa.strip():
return {
"status": "blocked",
"reason": "pyjhora_ayanamsa_configuration_failed:missing_ayanamsa",
"settings": settings,
"node_mode": node_mode,
}
try:
# PyJHora can print import diagnostics; reserve stdout for the JSON reply.
with contextlib.redirect_stdout(sys.stderr):
from jhora import const
from jhora.panchanga import drik
from jhora.horoscope.transit import tajaka
drik.set_ayanamsa_mode(ayanamsa.upper())
effective_ayanamsa = const._DEFAULT_AYANAMSA_MODE
if str(effective_ayanamsa).upper() != ayanamsa.upper():
return {
"status": "blocked",
"reason": "pyjhora_ayanamsa_configuration_failed:effective_mode_mismatch",
"settings": settings,
"effective_ayanamsa": effective_ayanamsa,
"node_mode": node_mode,
}
except Exception as exc:
return {
"status": "blocked",
"reason": f"pyjhora_ayanamsa_configuration_failed:{exc.__class__.__name__}",
"settings": settings,
"node_mode": node_mode,
}
try:
place_data = dict(request["place"])
place = drik.Place(
place_data["name"],
float(place_data["latitude"]),
float(place_data["longitude"]),
float(place_data["timezone"]),
place_data.get("elevation"),
)
raw_candidates, candidate_trace_status = _tajaka_candidate_trace(
tajaka, float(request["birth_julian_day"]), place, request["age"]
)
raw = tajaka.lord_of_the_year(float(request["birth_julian_day"]), place, request["age"])
except Exception as exc:
return {
"status": "blocked",
"reason": f"pyjhora_tajaka_year_lord_failed:{exc.__class__.__name__}",
"callable": f"{_TAJAKA_YEAR_LORD_MODULE}.lord_of_the_year",
"settings": settings,
"effective_ayanamsa": effective_ayanamsa,
"node_mode": node_mode,
"raw": None,
"raw_candidates": None,
}
return {
"status": "partial_verified",
"reason": "pyjhora_tajaka_year_lord_callable_observed",
"callable": f"{_TAJAKA_YEAR_LORD_MODULE}.lord_of_the_year",
"pyjhora_version": _pyjhora_version(_distribution_version),
"settings": settings,
"effective_ayanamsa": effective_ayanamsa,
"node_mode": node_mode,
"year_lord": raw,
"raw": raw,
"raw_candidates": raw_candidates,
"candidate_trace_status": candidate_trace_status,
}
def _run_isolated_annual_replay(
profile: dict[str, Any],
birth_julian_day: float,
place: Any,
age: int | float,
) -> dict[str, Any]:
"""Call optional PyJHora annual surfaces with a child-process Place."""
settings = _replay_settings(profile)
request = {
"birth_julian_day": birth_julian_day,
"place": _place_payload(place),
"age": age,
"settings": settings,
}
request_hash = _request_hash(request)
command = [
sys.executable,
"-c",
(
"import json, sys; "
"from scripts.annual_pyjhora_replay import _isolated_annual_worker; "
"print(json.dumps(_isolated_annual_worker(json.load(sys.stdin)), sort_keys=True, default=str))"
),
]
try:
completed = subprocess.run(
command,
input=json.dumps(request, sort_keys=True),
capture_output=True,
text=True,
timeout=30,
check=False,
cwd=str(REPO_ROOT),
)
except subprocess.TimeoutExpired:
return _annual_replay_blocked("pyjhora_isolated_annual_replay_timeout", request_hash, settings)
except Exception as exc:
return _annual_replay_blocked(
f"pyjhora_isolated_annual_replay_start_failed:{exc.__class__.__name__}", request_hash, settings
)
if completed.returncode != 0:
return _annual_replay_blocked(
f"pyjhora_isolated_annual_replay_failed:exit_{completed.returncode}", request_hash, settings
)
try:
replay = json.loads(completed.stdout)
except json.JSONDecodeError:
return _annual_replay_blocked("pyjhora_isolated_annual_replay_invalid_json", request_hash, settings)
if not isinstance(replay, dict):
return _annual_replay_blocked("pyjhora_isolated_annual_replay_invalid_payload", request_hash, settings)
replay["request_hash"] = request_hash
return replay
def _isolated_annual_worker(request: dict[str, Any]) -> dict[str, Any]:
"""Construct ``drik.Place`` after setting Ayanamsa in the child process."""
settings = dict(request.get("settings") or {})
node_mode = {"requested": settings.get("node_mode"), "status": "unsupported_by_replay_adapter"}
ayanamsa = settings.get("ayanamsa")
if not isinstance(ayanamsa, str) or not ayanamsa.strip():
return _annual_worker_result(
{key: {"status": "blocked", "reason": "pyjhora_ayanamsa_configuration_failed:missing_ayanamsa"} for key in _ANNUAL_SURFACE_KEYS},
settings=settings,
node_mode=node_mode,
)
try:
with contextlib.redirect_stdout(sys.stderr):
from jhora import const
from jhora.panchanga import drik
from jhora.horoscope.dhasa.annual import mudda, patyayini
from jhora.horoscope.transit import saham
drik.set_ayanamsa_mode(ayanamsa.upper())
effective_ayanamsa = const._DEFAULT_AYANAMSA_MODE
if str(effective_ayanamsa).upper() != ayanamsa.upper():
return _annual_worker_result(
{key: {"status": "blocked", "reason": "pyjhora_ayanamsa_configuration_failed:effective_mode_mismatch"} for key in _ANNUAL_SURFACE_KEYS},
settings=settings,
node_mode=node_mode,
effective_ayanamsa=effective_ayanamsa,
)
place_data = dict(request["place"])
pyjhora_place = drik.Place(
place_data["name"], float(place_data["latitude"]), float(place_data["longitude"]),
float(place_data["timezone"]), place_data.get("elevation"),
)
except Exception as exc:
return _annual_worker_result(
{key: {"status": "blocked", "reason": f"pyjhora_annual_adapter_setup_failed:{exc.__class__.__name__}"} for key in _ANNUAL_SURFACE_KEYS},
settings=settings,
node_mode=node_mode,
)
jd = float(request["birth_julian_day"])
years = request["age"]
surfaces = {
"annual_chart_snapshot": _call_annual_chart_snapshot(jd, pyjhora_place, years),
"annual_chart_speed_snapshot": _call_annual_chart_speed_snapshot(jd, pyjhora_place, years),
"mudda_dasha": _call_mudda(mudda, jd, pyjhora_place, years),
"patyayini_dasha": _call_patyayini(patyayini, jd, pyjhora_place, years),
"sahams": _call_sahams(saham, jd, pyjhora_place, years),
"solar_return_boundary": _call_solar_return_boundary(saham, jd, pyjhora_place, years),
"tajika_yogas": _probe_tajika_yogas(jd, pyjhora_place, years),
}
return _annual_worker_result(
surfaces,
settings=settings,
node_mode=node_mode,
effective_ayanamsa=effective_ayanamsa,
)
def _annual_worker_result(
surfaces: dict[str, dict[str, Any]],
*,
settings: dict[str, Any],
node_mode: dict[str, Any],
effective_ayanamsa: Any = None,
) -> dict[str, Any]:
return {
**{key: surfaces.get(key, {"status": "blocked", "reason": "not_executed"}) for key in _ANNUAL_SURFACE_KEYS},
"settings": settings,
"effective_ayanamsa": effective_ayanamsa,
"node_mode": node_mode,
"evidence_scope": "pyjhora_behavior_only",
"parity_status": "not_multiengine_parity",
}
def _annual_replay_blocked(reason: str, request_hash: str, settings: dict[str, Any]) -> dict[str, Any]:
return _annual_worker_result(
{key: {"status": "blocked", "reason": reason} for key in _ANNUAL_SURFACE_KEYS},
settings=settings,
node_mode={"requested": settings.get("node_mode"), "status": "unsupported_by_replay_adapter"},
) | {"request_hash": request_hash}
def _place_payload(place: Any) -> dict[str, Any]:
return {
"name": getattr(place, "name", "External replay place"),
"latitude": getattr(place, "latitude"),
"longitude": getattr(place, "longitude"),
"timezone": getattr(place, "timezone"),
"elevation": getattr(place, "elevation", None),
}
def _request_hash(request: dict[str, Any]) -> str:
payload = json.dumps(request, sort_keys=True, separators=(",", ":"), ensure_ascii=True)
return hashlib.sha256(payload.encode("utf-8")).hexdigest()
def _isolated_blocked(reason: str, request_hash: str, settings: dict[str, Any]) -> dict[str, Any]:
return {
"status": "blocked",
"reason": reason,
"request_hash": request_hash,
"settings": settings,
"node_mode": {
"requested": settings.get("node_mode"),
"status": "unsupported_by_replay_adapter",
},
}
def _replay_settings(profile: dict[str, Any]) -> dict[str, Any]:
settings = profile.get("settings")
source = settings if isinstance(settings, dict) else profile
return {
key: source[key]
for key in ("ayanamsa", "node_mode")
if source.get(key) is not None
}
def _call_tajaka_year_lord(
result: dict[str, Any],
replay: dict[str, Any],
tajaka: Any,
*,
birth_julian_day: float,
place: Any,
age: int | float,
settings: dict[str, Any],
) -> dict[str, Any]:
callable_name = f"{_TAJAKA_YEAR_LORD_MODULE}.lord_of_the_year"
try:
raw_candidates, candidate_trace_status = _tajaka_candidate_trace(
tajaka, birth_julian_day, place, age
)
raw = tajaka.lord_of_the_year(birth_julian_day, place, age)
except Exception as exc:
replay.update(
{
"status": "blocked",
"reason": f"pyjhora_tajaka_year_lord_failed:{exc.__class__.__name__}",
"missing_api": None,
"callable": callable_name,
"settings": settings,
"raw": None,
"raw_candidates": None,
}
)
return result
replay.update(
{
"status": "partial_verified",
"reason": "pyjhora_tajaka_year_lord_callable_observed",
"missing_api": None,
"callable": callable_name,
"settings": settings,
"year_lord": raw,
"raw": raw,
"raw_candidates": raw_candidates,
"candidate_trace_status": candidate_trace_status,
"normalized_candidates": _normalize_tajaka_candidates(raw_candidates),
}
)
return result
def _tajaka_candidate_trace(
tajaka: Any,
birth_julian_day: float,
place: Any,
age: int | float,
) -> tuple[Any, str]:
"""Collect the source module's candidate list without reimplementing its rule."""
candidate_function = getattr(tajaka, "_get_lord_candidates", None)
if not callable(candidate_function):
return None, "unavailable"
try:
# Lightweight injected test modules can expose a direct trace helper.
return candidate_function(), "observed"
except TypeError:
pass
try:
rasi_chart = tajaka.charts.divisional_chart(
birth_julian_day, place, divisional_chart_factor=1
)
natal_planet_to_house = tajaka.utils.get_planet_house_dictionary_from_planet_positions(rasi_chart)
natal_lagna_house = natal_planet_to_house[tajaka.const._ascendant_symbol]
annual_jd = birth_julian_day + float(age) * tajaka.year_value
annual_hour = tajaka.drik.jd_to_gregorian(annual_jd)[3]
sunrise = tajaka.utils.from_dms_str_to_dms(tajaka.drik.sunrise(annual_jd, place)[1])
sunset = tajaka.utils.from_dms_str_to_dms(tajaka.drik.sunset(annual_jd, place)[1])
sunrise_hour = sunrise[0] + sunrise[1] / 60 + sunrise[2] / 3600
sunset_hour = sunset[0] + sunset[1] / 60 + sunset[2] / 3600
night_time_birth = annual_hour > sunset_hour or annual_hour < sunrise_hour
annual_chart = tajaka.charts.divisional_chart(annual_jd, place, divisional_chart_factor=1)
return candidate_function(annual_chart, age, natal_lagna_house, night_time_birth), "observed"
except Exception as exc:
return None, f"blocked:{exc.__class__.__name__}"
def _normalize_tajaka_candidates(raw_candidates: Any) -> list[dict[str, Any]]:
if not isinstance(raw_candidates, (list, tuple)):
return []
return [
{"planet_index": candidate, "selection_state": "raw_tajaka_candidate"}
for candidate in raw_candidates
if isinstance(candidate, int)
]
def _pyjhora_version(distribution_version: Callable[[str], str]) -> str | None:
try:
return distribution_version("PyJHora")
except PackageNotFoundError:
return None
def _call_year_lord_candidate(
result: dict[str, Any],
replay: dict[str, Any],
candidate: Callable[..., Any],
callable_name: str,
birth_julian_day: float,
place: Any,
age: int | float,
) -> dict[str, Any]:
values = {
"jd": birth_julian_day,
"birth_julian_day": birth_julian_day,
"place": place,
"age": age,
"years": age,
}
try:
params = signature(candidate).parameters
kwargs = {name: values[name] for name in params if name in values}
required = [
name for name, parameter in params.items()
if parameter.default is parameter.empty
and parameter.kind in (parameter.POSITIONAL_ONLY, parameter.POSITIONAL_OR_KEYWORD, parameter.KEYWORD_ONLY)
and name not in kwargs
]
if required:
replay.update(
{
"reason": "pyjhora_year_lord_callable_signature_unsupported",
"missing_api": ", ".join(required),
"callable": callable_name,
}
)
return result
raw = candidate(**kwargs)
except Exception as exc:
replay.update(
{
"reason": f"pyjhora_year_lord_callable_failed:{callable_name}:{exc.__class__.__name__}",
"missing_api": None,
"callable": callable_name,
}
)
return result
replay.update(
{
"status": "partial_verified",
"reason": "pyjhora_year_lord_callable_observed",
"missing_api": None,
"callable": callable_name,
"year_lord": raw.get("year_lord") if isinstance(raw, dict) else None,
"raw": raw,
"normalized_candidates": _normalize_year_lord_candidates(raw),
}
)
return result
def _normalize_year_lord_candidates(raw: Any) -> list[dict[str, Any]]:
if not isinstance(raw, dict):
return []
candidates = raw.get("candidates") or raw.get("candidate_planets")
if not isinstance(candidates, list):
return []
return [candidate for candidate in candidates if isinstance(candidate, dict)]
def replay_pyjhora_annual(
profile: dict[str, Any],
*,
birth_julian_day: float,
place: Any,
age: int | float,
import_module: Callable[[str], Any] = _import_module,
year_lord_probe: Callable[..., dict[str, Any]] | None = None,
isolated_annual_replay: Callable[[dict[str, Any], float, Any, int | float], dict[str, Any]] | None = None,
) -> dict[str, Any]:
"""Replay annual PyJHora methods for the same profile, if available."""
year_lord_observer = year_lord_probe or probe_pyjhora_year_lord
year_lord_result = year_lord_observer(
profile,
birth_julian_day=birth_julian_day,
place=place,
age=age,
)
year_lord_replay = year_lord_result.get("year_lord_replay") if isinstance(year_lord_result, dict) else None
blocked = _base(profile)
blocked["year_lord_replay"] = year_lord_replay if isinstance(year_lord_replay, dict) else {
"status": "blocked",
"reason": "pyjhora_year_lord_probe_invalid_payload",
}
blocked["evidence_scope"] = "pyjhora_behavior_only"
blocked["parity_status"] = "not_multiengine_parity"
if isolated_annual_replay is not None or (import_module is _import_module and year_lord_probe is None):
observer = isolated_annual_replay or _run_isolated_annual_replay
observed = observer(profile, birth_julian_day, place, age)
if not isinstance(observed, dict):
observed = _annual_replay_blocked(
"pyjhora_isolated_annual_replay_invalid_payload",
_request_hash({"birth_julian_day": birth_julian_day, "place": _place_payload(place), "age": age}),
_replay_settings(profile),
)
result = _base(profile)
for key in _ANNUAL_SURFACE_KEYS:
value = observed.get(key)
result[key] = value if isinstance(value, dict) else {"status": "blocked", "reason": f"{key}_missing_from_isolated_replay"}
result["year_lord_replay"] = blocked["year_lord_replay"]
for key in ("effective_ayanamsa", "node_mode", "request_hash"):
if key in observed:
result[key] = observed[key]
result["evidence_scope"] = "pyjhora_behavior_only"
result["parity_status"] = "not_multiengine_parity"
result["status"] = _aggregate_status(result)
return result
try:
mudda = import_module("jhora.horoscope.dhasa.annual.mudda")
patyayini = import_module("jhora.horoscope.dhasa.annual.patyayini")
saham = import_module("jhora.horoscope.transit.saham")
except Exception as exc:
reason = f"pyjhora_import_failed:{exc.__class__.__name__}:{exc}"
for key in _ANNUAL_SURFACE_KEYS:
blocked[key] = {"status": "blocked", "reason": reason}
blocked["status"] = "blocked"
return blocked
result = _base(profile)
result["annual_chart_snapshot"] = _call_annual_chart_snapshot(birth_julian_day, place, age)
result["mudda_dasha"] = _call_mudda(mudda, birth_julian_day, place, age)
result["patyayini_dasha"] = _call_patyayini(patyayini, birth_julian_day, place, age)
result["sahams"] = _call_sahams(saham, birth_julian_day, place, age)
result["solar_return_boundary"] = _call_solar_return_boundary(saham, birth_julian_day, place, age)
result["tajika_yogas"] = {"status": "blocked", "reason": "pyjhora_tajika_yogas_requires_isolated_replay"}
result["year_lord_replay"] = year_lord_replay if isinstance(year_lord_replay, dict) else {
"status": "blocked",
"reason": "pyjhora_year_lord_probe_invalid_payload",
}
result["evidence_scope"] = "pyjhora_behavior_only"
result["parity_status"] = "not_multiengine_parity"
result["status"] = _aggregate_status(result)
return result
def _base(profile: dict[str, Any]) -> dict[str, Any]:
return {
"engine": ENGINE,
"license_boundary": {
"mode": "external_reference_only",
"dependency_required": False,
"notes": "PyJHora/JHora remains an optional external oracle/replay boundary.",
},
"input_profile_id": profile.get("profile_id"),
"annual_chart_snapshot": {"status": "blocked", "reason": "not_executed"},
"annual_chart_speed_snapshot": {"status": "blocked", "reason": "not_executed"},
"mudda_dasha": {"status": "blocked", "reason": "not_executed"},
"patyayini_dasha": {"status": "blocked", "reason": "not_executed"},
"sahams": {"status": "blocked", "reason": "not_executed"},
"solar_return_boundary": {"status": "blocked", "reason": "not_executed"},
"tajika_yogas": {"status": "blocked", "reason": "not_executed"},
"year_lord_replay": {"status": "blocked", "reason": "not_executed"},
"evidence_scope": "pyjhora_behavior_only",
"parity_status": "not_multiengine_parity",
"status": "blocked",
}
def _call_mudda(module: Any, birth_julian_day: float, place: Any, age: int | float) -> dict[str, Any]:
payload: dict[str, Any] = {}
try:
if hasattr(module, "varsha_vimsottari_dasha_start_date"):
payload["varsha_vimsottari_start"] = module.varsha_vimsottari_dasha_start_date(
birth_julian_day, place, age
)
if hasattr(module, "mudda_dhasa_bhukthi"):
payload["periods"] = module.mudda_dhasa_bhukthi(birth_julian_day, place, age)
return {"status": "partial_verified", "raw": payload}
except Exception as exc:
return {"status": "blocked", "reason": f"pyjhora_mudda_failed:{exc}"}
def _call_patyayini(module: Any, birth_julian_day: float, place: Any, age: int | float) -> dict[str, Any]:
try:
jd_for_year = birth_julian_day + float(age) * 365.256364
return {"status": "partial_verified", "raw": {"periods": module.get_dhasa_bhukthi(jd_for_year, place)}}
except Exception as exc:
return {"status": "blocked", "reason": f"pyjhora_patyayini_failed:{exc}"}
def _call_sahams(module: Any, birth_julian_day: float, place: Any, age: int | float) -> dict[str, Any]:
"""Replay the current PyJHora per-Saham API against its annual chart.
Older PyJHora releases exposed an aggregate ``sahams`` function. Current
releases expose individual functions which all consume annual-chart planet
positions. Keep both paths external-reference-only and preserve the exact
source API used for audit.
"""
try:
individual = [name for name in _PYJHORA_SAHAM_CALLABLES if callable(getattr(module, name, None))]
if not individual:
if hasattr(module, "sahams"):
return {
"status": "partial_verified",
"callable": "jhora.horoscope.transit.saham.sahams",
"raw": module.sahams(birth_julian_day, place, age),
"reason": "legacy_pyjhora_aggregate_sahams_api",
}
return {"status": "blocked", "reason": "pyjhora_sahams_callable_missing"}
tajaka = _import_module("jhora.horoscope.transit.tajaka")
annual_chart = getattr(tajaka, "annual_chart", None)
if not callable(annual_chart):
return {"status": "blocked", "reason": "pyjhora_annual_chart_callable_missing"}
chart_payload = annual_chart(birth_julian_day, place, years=age)
if not isinstance(chart_payload, tuple) or len(chart_payload) < 2 or not isinstance(chart_payload[0], list):
return {"status": "blocked", "reason": "pyjhora_annual_chart_payload_invalid"}
positions, return_marker = chart_payload[0], chart_payload[1]
annual_moment = _annual_return_datetime(return_marker)
if annual_moment is None:
return {"status": "blocked", "reason": "pyjhora_annual_chart_return_time_unavailable"}
try:
try:
from scripts.saham_daynight import determine_daytime
except ImportError: # pragma: no cover - direct script execution
from saham_daynight import determine_daytime
daynight = determine_daytime(
annual_moment,
lat=float(place.latitude),
lon=float(place.longitude),
tz=float(place.timezone),
)
except Exception as exc:
return {"status": "blocked", "reason": f"pyjhora_sahams_daynight_failed:{exc.__class__.__name__}"}
values: dict[str, Any] = {}
failures: dict[str, str] = {}
for name in individual:
candidate = getattr(module, name)
try:
parameters = signature(candidate).parameters
if "night_time_birth" in parameters:
values[name] = candidate(positions, night_time_birth=not bool(daynight["is_daytime"]))
else:
values[name] = candidate(positions)
except Exception as exc: # External API variations must remain visible.
failures[name] = exc.__class__.__name__
if not values:
return {
"status": "blocked",
"reason": "pyjhora_individual_sahams_all_failed",
"failed_callables": failures,
}
return {
"status": "partial_verified",
"callable": "jhora.horoscope.transit.tajaka.annual_chart",
"reason": "pyjhora_individual_sahams_from_annual_chart",
"daynight": daynight,
"raw": {
"annual_return": _annual_return_marker(return_marker),
"sahams": values,
"failed_callables": failures,
"available_callables": individual,
},
}
except Exception as exc:
return {"status": "blocked", "reason": f"pyjhora_sahams_failed:{exc}"}
def _call_annual_chart_snapshot(birth_julian_day: float, place: Any, age: int | float) -> dict[str, Any]:
"""Capture PyJHora's annual chart without deriving a local interpretation.
This is an external-reference snapshot for profile diagnosis. It exists so
annual Tajika comparisons can distinguish a chart-input difference from a
candidate-rule difference. It is not a report authority or a Yoga result.
"""
try:
tajaka = _import_module("jhora.horoscope.transit.tajaka")
annual_chart = getattr(tajaka, "annual_chart", None)
if not callable(annual_chart):
return {"status": "blocked", "reason": "pyjhora_annual_chart_callable_missing"}
payload = annual_chart(birth_julian_day, place, years=age)
if not isinstance(payload, tuple) or len(payload) < 2 or not isinstance(payload[0], list):
return {"status": "blocked", "reason": "pyjhora_annual_chart_payload_invalid"}
positions, return_marker = payload[0], payload[1]
return {
"status": "partial_verified",
"callable": "jhora.horoscope.transit.tajaka.annual_chart",
"reason": "pyjhora_annual_chart_snapshot_for_profile_comparison",
"raw": {
"annual_return": _annual_return_marker(return_marker),
"positions": positions,
},
"claim_boundary": "external_annual_chart_snapshot_is_not_tajika_yoga_or_report_authority",
}
except Exception as exc:
return {"status": "blocked", "reason": f"pyjhora_annual_chart_snapshot_failed:{exc.__class__.__name__}"}
def _signed_longitude_delta(previous: float, following: float) -> float:
return (float(following) - float(previous) + 180.0) % 360.0 - 180.0
def _planet_longitudes_from_positions(positions: Any) -> dict[int, float]:
values: dict[int, float] = {}
for row in positions if isinstance(positions, list) else []:
if not isinstance(row, (list, tuple)) or len(row) < 2 or not isinstance(row[0], int):
continue
planet, position = row[0], row[1]
if planet not in range(7) or not isinstance(position, (list, tuple)) or len(position) < 2:
continue
sign, degree = position[0], position[1]
if isinstance(sign, (int, float)) and isinstance(degree, (int, float)):
values[planet] = (float(sign) * 30.0 + float(degree)) % 360.0
return values
def _call_annual_chart_speed_snapshot(birth_julian_day: float, place: Any, age: int | float) -> dict[str, Any]:
"""Observe PyJHora annual-chart motion by finite difference.
The upstream annual-chart snapshot exposes longitudes but not speeds. This
child-process-only probe samples one minute on either side of the external
return marker so a later diagnostic can distinguish chart coordinates from
applying/separating semantics. It is not a native speed source or Tajika
Yoga authority.
"""
try:
from jhora import utils
from jhora.panchanga import drik
tajaka = _import_module("jhora.horoscope.transit.tajaka")
annual_chart = getattr(tajaka, "annual_chart", None)
if not callable(annual_chart):
return {"status": "blocked", "reason": "pyjhora_annual_chart_callable_missing"}
payload = annual_chart(birth_julian_day, place, years=age)
if not isinstance(payload, tuple) or len(payload) < 2:
return {"status": "blocked", "reason": "pyjhora_annual_chart_payload_invalid"}
annual_moment = _annual_return_datetime(payload[1])
if annual_moment is None:
return {"status": "blocked", "reason": "pyjhora_annual_chart_return_time_unavailable"}
offset_seconds = 60
before = annual_moment - timedelta(seconds=offset_seconds)
after = annual_moment + timedelta(seconds=offset_seconds)
before_jd = utils.julian_day_number(
(before.year, before.month, before.day), (before.hour, before.minute, before.second)
)
after_jd = utils.julian_day_number(
(after.year, after.month, after.day), (after.hour, after.minute, after.second)
)
before_positions = _planet_longitudes_from_positions(drik.dhasavarga(before_jd, place))
after_positions = _planet_longitudes_from_positions(drik.dhasavarga(after_jd, place))
if set(before_positions) != set(range(7)) or set(after_positions) != set(range(7)):
return {
"status": "blocked",
"reason": "pyjhora_annual_chart_speed_positions_incomplete",
"before_planets": sorted(before_positions),
"after_planets": sorted(after_positions),
}
interval_days = (2.0 * offset_seconds) / 86400.0
rates = {
planet: round(_signed_longitude_delta(before_positions[planet], after_positions[planet]) / interval_days, 8)
for planet in range(7)
}
return {
"status": "partial_verified",
"callable": "jhora.panchanga.drik.dhasavarga",
"reason": "pyjhora_annual_chart_finite_difference_speed_snapshot",
"raw": {
"annual_return": _annual_return_marker(payload[1]),
"sample_offset_seconds": offset_seconds,
"planet_rates_deg_per_day": rates,
},
"claim_boundary": "external_annual_speed_snapshot_is_comparison_only_not_tajika_yoga_or_report_authority",
}
except Exception as exc:
return {"status": "blocked", "reason": f"pyjhora_annual_chart_speed_snapshot_failed:{exc.__class__.__name__}"}
def _annual_return_marker(value: Any) -> dict[str, Any]:
if not isinstance(value, (list, tuple)) or len(value) < 2:
return {"raw": value}
date_value, time_value = value[0], value[1]
return {
"date": list(date_value) if isinstance(date_value, (list, tuple)) else date_value,
"time": str(time_value),
}
def _annual_return_datetime(value: Any) -> datetime | None:
marker = _annual_return_marker(value)
date_value = marker.get("date")
time_value = marker.get("time")
if not isinstance(date_value, list) or len(date_value) != 3 or not isinstance(time_value, str):
return None
try:
hour, minute, second = [int(part) for part in time_value.split(":")]
return datetime(int(date_value[0]), int(date_value[1]), int(date_value[2]), hour, minute, second)
except (TypeError, ValueError):
return None
def _call_solar_return_boundary(module: Any, birth_julian_day: float, place: Any, age: int | float) -> dict[str, Any]:
try:
if not hasattr(module, "solar_return_chart"):
return {"status": "blocked", "reason": "pyjhora_solar_return_chart_missing"}
return {
"status": "partial_verified",
"raw": module.solar_return_chart(birth_julian_day, place, age),
}
except Exception as exc:
return {"status": "blocked", "reason": f"pyjhora_solar_return_failed:{exc}"}
def _probe_tajika_yogas(birth_julian_day: float, place: Any, age: int | float) -> dict[str, Any]:
"""Probe callable availability without recreating PyJHora yoga rules."""
try:
module = _import_module("jhora.horoscope.transit.tajaka_yoga")
except Exception as exc:
return {"status": "blocked", "reason": f"pyjhora_tajika_yogas_module_missing:{exc.__class__.__name__}"}
for name in ("tajaka_yoga", "tajika_yoga", "get_tajaka_yogas", "get_tajika_yogas"):
candidate = getattr(module, name, None)
if not callable(candidate):
continue
values = {"jd": birth_julian_day, "birth_julian_day": birth_julian_day, "place": place, "age": age, "years": age}
try:
params = signature(candidate).parameters
kwargs = {key: values[key] for key in params if key in values}
missing = [key for key, parameter in params.items() if parameter.default is parameter.empty and key not in kwargs]
if missing:
return {"status": "blocked", "reason": "pyjhora_tajika_yogas_callable_signature_unsupported", "callable": name, "missing": missing}
return {"status": "partial_verified", "callable": name, "raw": candidate(**kwargs)}
except Exception as exc:
return {"status": "blocked", "reason": f"pyjhora_tajika_yogas_callable_failed:{exc.__class__.__name__}", "callable": name}
return {"status": "blocked", "reason": "pyjhora_tajika_yogas_callable_missing"}
def _aggregate_status(result: dict[str, Any]) -> str:
statuses = {
result[key].get("status")
for key in (*_ANNUAL_SURFACE_KEYS, "year_lord_replay")
if isinstance(result.get(key), dict)
}
if statuses == {"partial_verified"}:
return "partial_verified"
if "partial_verified" in statuses:
return "partial_verified"
return "blocked"