The western comparison layer was invoked without its files, so it stayed blocked. Navamsha and Niryaana Shoola are filled from the native rashi dashas. Co-Authored-By: Grok (agent loop) Co-authored-by: Cursor <cursoragent@cursor.com>
269 lines
11 KiB
Python
269 lines
11 KiB
Python
"""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())
|