"""Auditable orchestration for the native tropical Western calculation layers.""" from __future__ import annotations try: # reached as a script: this directory is on the path, the root is not import _script_root # noqa: F401 except ModuleNotFoundError: # pragma: no cover - reached as scripts.x instead try: from scripts import _script_root # noqa: F401 except ModuleNotFoundError: pass import argparse import json from datetime import date from typing import Any, Callable try: import swisseph as swe except ImportError: # pragma: no cover - optional runtime dependency swe = None try: from scripts.classical_astrology import build_classical_astrology_packet, build_classical_usable_loop_audit_packet from scripts.western_chart_engine import build_tropical_natal_chart from scripts.western_chart_engine import build_draconic_chart, build_relocation_chart from scripts.western_timing_engine import ( calculate_annual_profection, calculate_converse_secondary_progressions, calculate_converse_solar_arc_directions, calculate_lunar_return, calculate_lunar_return_series, calculate_midpoints, calculate_parans_status, calculate_secondary_progressions, calculate_solar_arc_directions, calculate_solar_return, calculate_tertiary_progressed_moon, calculate_transit_duration_scan, calculate_transit_to_natal, calculate_zodiacal_release, ) except ModuleNotFoundError: # pragma: no cover - direct script execution from classical_astrology import build_classical_astrology_packet, build_classical_usable_loop_audit_packet from western_chart_engine import build_tropical_natal_chart from western_chart_engine import build_draconic_chart, build_relocation_chart from western_timing_engine import ( calculate_annual_profection, calculate_converse_secondary_progressions, calculate_converse_solar_arc_directions, calculate_lunar_return, calculate_lunar_return_series, calculate_midpoints, calculate_parans_status, calculate_secondary_progressions, calculate_solar_arc_directions, calculate_solar_return, calculate_tertiary_progressed_moon, calculate_transit_duration_scan, calculate_transit_to_natal, calculate_zodiacal_release, ) def _executed(inputs: dict[str, Any], calculation: Callable[[], dict[str, Any]]) -> dict[str, Any]: try: return {"status": "executed", "input": inputs, "raw_result": calculation()} except Exception as exc: # pragma: no cover - defensive orchestration boundary return { "status": "blocked", "input": inputs, "reason": f"{exc.__class__.__name__}: {exc}", } def _blocked(reason: str, inputs: dict[str, Any] | None = None) -> dict[str, Any]: result: dict[str, Any] = {"status": "blocked", "reason": reason} if inputs: result["input"] = inputs return result def _reference_layer( reference_date: str | None, birth: dict[str, Any], calculation: Callable[[str], dict[str, Any]], ) -> dict[str, Any]: if not reference_date: return _blocked("reference_date is required") return _executed({"reference_date": reference_date}, lambda: calculation(reference_date)) def _overall_status(layers: dict[str, dict[str, Any]]) -> str: return "complete" if all(layer["status"] == "executed" for layer in layers.values()) else "partial" def _eclipse_events(reference_date: str) -> dict[str, Any]: if swe is None: raise RuntimeError("swisseph is unavailable") start = date.fromisoformat(reference_date) jd = swe.julday(start.year, start.month, start.day, 0.0) events = [] for kind, finder in (("solar", swe.sol_eclipse_when_glob), ("lunar", swe.lun_eclipse_when)): flags, times = finder(jd) year, month, day, hour = swe.revjul(times[0]) events.append({"kind": kind, "maximum_utc": f"{year:04d}-{month:02d}-{day:02d}T{int(hour):02d}:{int((hour % 1) * 60):02d}:00Z", "swiss_event_flags": int(flags)}) return {"technique": "eclipse_triggers", "reference_date": reference_date, "events": events, "boundary": "Geometric eclipse triggers only; no predictive interpretation is inferred."} def build_unified_western_reading( *, reference_date: str | None = None, duration_scan_end_date: str | None = None, lunar_return_months: int = 1, relocation_latitude: float | None = None, relocation_longitude: float | None = None, relocation_timezone: str | float | int | None = None, zodiacal_release_lot: str = "spirit", zodiacal_release_levels: int = 2, include_classical_usable_loop: bool = False, classical_event_window_hours: int = 4, classical_event_step_hours: int = 2, classical_blind_scoring_manifest: str | None = None, classical_reviewer_manifest: str | None = None, **birth: Any, ) -> dict[str, Any]: """Run every available native Western layer and preserve raw evidence. ``reference_date`` drives the one-day timing techniques. A duration scan additionally needs an explicit end date, so a caller cannot unknowingly request an unbounded scan. Eclipse geometry uses the local Swiss Ephemeris calculation and does not infer an event interpretation. """ layers: dict[str, dict[str, Any]] = { "natal": _executed(dict(birth), lambda: build_tropical_natal_chart(**birth)), "classical": _executed( {"reference_date": reference_date}, lambda: build_classical_astrology_packet(reference_date=reference_date, **birth), ), "draconic": _executed(dict(birth), lambda: build_draconic_chart(**birth)), "transits": _reference_layer( reference_date, birth, lambda target: calculate_transit_to_natal(target_date=target, **birth) ), "solar_return": _reference_layer( reference_date, birth, lambda target: calculate_solar_return(target_year=date.fromisoformat(target).year, **birth), ), "secondary_progressions": _reference_layer( reference_date, birth, lambda target: calculate_secondary_progressions(target_date=target, **birth) ), "tertiary_moon": _reference_layer( reference_date, birth, lambda target: calculate_tertiary_progressed_moon(target_date=target, **birth) ), "solar_arc": _reference_layer( reference_date, birth, lambda target: calculate_solar_arc_directions(target_date=target, **birth) ), "midpoints": _reference_layer( reference_date, birth, lambda target: calculate_midpoints(target_date=target, **birth) ), "lunar_return": _reference_layer( reference_date, birth, lambda target: calculate_lunar_return_series( target_date=target, months=lunar_return_months, **birth ) ), "parans": _reference_layer( reference_date, birth, lambda target: calculate_parans_status(target_date=target, **birth) ), "profection": _reference_layer( reference_date, birth, lambda target: calculate_annual_profection(target_date=target, **birth) ), "zodiacal_release": _reference_layer( reference_date, birth, lambda target: calculate_zodiacal_release( target_date=target, lot=zodiacal_release_lot, levels=int(zodiacal_release_levels), **birth, ), ), } if relocation_latitude is None or relocation_longitude is None or relocation_timezone is None: layers["relocation"] = _blocked( "relocation_latitude, relocation_longitude, and relocation_timezone are required" ) else: layers["relocation"] = _executed( { "relocation_latitude": relocation_latitude, "relocation_longitude": relocation_longitude, "relocation_timezone": relocation_timezone, }, lambda: build_relocation_chart( relocation_latitude=float(relocation_latitude), relocation_longitude=float(relocation_longitude), relocation_timezone=relocation_timezone, **birth, ), ) if reference_date: layers["converse"] = _executed( {"reference_date": reference_date}, lambda: { "secondary_progressions": calculate_converse_secondary_progressions( target_date=reference_date, **birth ), "solar_arc_directions": calculate_converse_solar_arc_directions( target_date=reference_date, **birth ), }, ) else: layers["converse"] = _blocked("reference_date is required") if not reference_date: layers["transit_duration"] = _blocked("reference_date is required") elif not duration_scan_end_date: layers["transit_duration"] = _blocked("duration_scan_end_date is required") else: layers["transit_duration"] = _executed( {"start_date": reference_date, "end_date": duration_scan_end_date}, lambda: calculate_transit_duration_scan( start_date=reference_date, end_date=duration_scan_end_date, **birth ), ) layers["eclipses"] = _reference_layer(reference_date, birth, _eclipse_events) if include_classical_usable_loop: layers["classical_usable_loop"] = _executed( { "reference_date": reference_date, "event_window_hours": classical_event_window_hours, "event_step_hours": classical_event_step_hours, "blind_scoring_manifest": classical_blind_scoring_manifest, "reviewer_manifest": classical_reviewer_manifest, }, lambda: build_classical_usable_loop_audit_packet( reference_date=reference_date, event_window_hours=int(classical_event_window_hours), event_step_hours=int(classical_event_step_hours), blind_scoring_manifest_path=classical_blind_scoring_manifest, reviewer_manifest_path=classical_reviewer_manifest, **birth, ), ) return { "system": "western_astrology", "status": _overall_status(layers), "reference_date": reference_date, "layers": layers, "boundary": "Raw native calculations only; this entrypoint makes no predictive interpretation.", } def main(argv: list[str] | None = None) -> int: parser = argparse.ArgumentParser(description="Run the unified native Western calculation layers.") parser.add_argument("--input-json", required=True, help="Structured birth and timing input as JSON.") args = parser.parse_args(argv) payload = json.loads(args.input_json) if not isinstance(payload, dict): raise ValueError("--input-json must decode to an object") print(json.dumps(build_unified_western_reading(**payload), ensure_ascii=False, default=str)) return 0 if __name__ == "__main__": # pragma: no cover raise SystemExit(main())