#!/usr/bin/env python3 # -*- coding: utf-8 -*- """Punarphoo observation detector. Natal rule: Moon-Saturn conjunction with circular orb <= 10°. Timing observation: transiting Saturn returning to natal Moon. Claim boundary (hard): - This module is observation-only. - Punarphoo natal presence or Saturn-return-to-natal-Moon is not marriage. - Do not promote to truth / legal_marriage before negative holdout passes. """ from __future__ import annotations from typing import Any, Dict, Optional PUNARPHOO_ORB_DEG = 10.0 SATURN_RETURN_TO_MOON_ORB_DEG = 1.0 CLAIM_BOUNDARY = ( "Punarphoo return is not marriage. Observation only; " "negative holdout not passed; do not promote to truth." ) def circular_degree_gap(a: float, b: float) -> float: gap = abs(float(a) - float(b)) % 360.0 return min(gap, 360.0 - gap) def _longitude(pdata: Any) -> Optional[float]: if not isinstance(pdata, dict): return None for key in ("longitude", "lon", "degree"): value = pdata.get(key) if isinstance(value, (int, float)): return float(value) return None def detect_natal_punarphoo(planets: Dict[str, Any] | None, orb_limit: float = PUNARPHOO_ORB_DEG) -> Dict[str, Any]: planets = planets if isinstance(planets, dict) else {} moon = planets.get("Moon") if isinstance(planets.get("Moon"), dict) else {} saturn = planets.get("Saturn") if isinstance(planets.get("Saturn"), dict) else {} moon_lon = _longitude(moon) saturn_lon = _longitude(saturn) same_house = moon.get("house") is not None and moon.get("house") == saturn.get("house") orb = circular_degree_gap(moon_lon, saturn_lon) if moon_lon is not None and saturn_lon is not None else None present = orb is not None and orb <= orb_limit return { "status": "observation_only", "natal_present": present, "orb_deg": orb, "orb_limit_deg": orb_limit, "same_house": bool(same_house), "moon_house": moon.get("house"), "saturn_house": saturn.get("house"), "claim_boundary": CLAIM_BOUNDARY, "source": "punarphoo_observation_v1", "production_tuning_allowed": False, } def detect_saturn_return_to_natal_moon( natal_moon_longitude: Optional[float], transit_saturn_longitude: Optional[float], orb_limit: float = SATURN_RETURN_TO_MOON_ORB_DEG, ) -> Dict[str, Any]: if natal_moon_longitude is None or transit_saturn_longitude is None: return { "status": "observation_only", "active": False, "orb_deg": None, "orb_limit_deg": orb_limit, "claim_boundary": CLAIM_BOUNDARY, "source": "punarphoo_observation_v1", } orb = circular_degree_gap(natal_moon_longitude, transit_saturn_longitude) return { "status": "observation_only", "active": orb <= orb_limit, "orb_deg": orb, "orb_limit_deg": orb_limit, "claim_boundary": CLAIM_BOUNDARY, "source": "punarphoo_observation_v1", } def detect_punarphoo( planets: Dict[str, Any] | None, transit_saturn_longitude: Optional[float] = None, ) -> Dict[str, Any]: natal = detect_natal_punarphoo(planets) moon_lon = _longitude((planets or {}).get("Moon")) if isinstance(planets, dict) else None transit = detect_saturn_return_to_natal_moon(moon_lon, transit_saturn_longitude) return { **natal, "saturn_return_to_natal_moon": transit, "timing_observation_active": bool(transit.get("active")), "claim_boundary": CLAIM_BOUNDARY, }