feat(reading): fill the five full-reading modules missing from upstream

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>
This commit is contained in:
jesse-ux
2026-10-18 19:32:38 +08:00
co-authored by Grok (agent loop) Cursor
parent b3ef2d8158
commit 900581068e
11 changed files with 13869 additions and 1 deletions
+109
View File
@@ -153,6 +153,21 @@ def _birth_zone(value: str | float | int):
return fixed_timezone(timedelta(hours=offset)), f"UTC{offset:+g}"
def _birth_times(
*,
year: int,
month: int,
day: int,
hour: int,
minute: int,
second: int,
timezone: str | float | int,
) -> tuple[datetime, datetime, str]:
zone, timezone_label = _birth_zone(timezone)
local = datetime(year, month, day, hour, minute, second, tzinfo=zone)
utc = local.astimezone(ZoneInfo("UTC"))
return local, utc, timezone_label
def build_tropical_natal_chart(
*,
year: int,
@@ -217,6 +232,100 @@ def build_tropical_natal_chart(
}
def build_draconic_chart(**birth: Any) -> dict[str, Any]:
"""Derive a draconic chart by rotating tropical longitudes to the true node Aries point."""
natal_chart = build_tropical_natal_chart(**birth)
natal = natal_chart["natal"]
node_longitude = natal["planets"]["true_node"]["longitude"]
def _rotate_point(point: dict[str, Any]) -> dict[str, Any]:
rotated = _point(point["longitude"] - node_longitude)
if "house" in point:
rotated["house"] = point["house"]
if "speed_longitude" in point:
rotated["speed_longitude"] = point["speed_longitude"]
rotated["retrograde"] = point.get("retrograde", False)
return rotated
rotated_planets = {name: _rotate_point(point) for name, point in natal["planets"].items()}
rotated_angles = {name: _rotate_point(point) for name, point in natal["angles"].items()}
rotated_houses = [
{"house": row["house"], "cusp": _rotate_point(row["cusp"])}
for row in natal["houses"]
]
aspect_points = {**rotated_planets, "ascendant": rotated_angles["ascendant"], "mc": rotated_angles["mc"]}
return {
"technique": "draconic_chart",
"status": "used",
"source_engine": natal_chart["source_engine"],
"base_zodiac": natal_chart["zodiac"],
"derived_zodiac": "draconic",
"node_basis": "true_node",
"node_longitude": node_longitude,
"draconic": {
"ascendant": rotated_angles["ascendant"],
"mc": rotated_angles["mc"],
"angles": rotated_angles,
"planets": rotated_planets,
"houses": rotated_houses,
"aspects": _aspects(aspect_points),
"distribution": _distribution(rotated_planets),
"house_ruler_chains": _ruler_chains(
[row["cusp"]["longitude"] for row in rotated_houses],
rotated_planets,
),
},
"boundary": "Longitude rotation to the true-node Aries point only; interpretation and timing remain separate audited layers.",
}
def build_relocation_chart(
*,
relocation_latitude: float,
relocation_longitude: float,
relocation_timezone: str | float | int,
house_system: str = "P",
**birth: Any,
) -> dict[str, Any]:
"""Calculate a relocated chart for the same UTC birth instant at a new location."""
local, utc, _ = _birth_times(
year=int(birth["year"]),
month=int(birth["month"]),
day=int(birth["day"]),
hour=int(birth["hour"]),
minute=int(birth["minute"]),
second=int(birth.get("second", 0)),
timezone=birth["timezone"],
)
relocation_zone, relocation_label = _birth_zone(relocation_timezone)
relocation_local = utc.astimezone(relocation_zone)
relocated_chart = build_tropical_natal_chart(
year=relocation_local.year,
month=relocation_local.month,
day=relocation_local.day,
hour=relocation_local.hour,
minute=relocation_local.minute,
second=relocation_local.second,
latitude=float(relocation_latitude),
longitude=float(relocation_longitude),
timezone=relocation_timezone,
house_system=house_system,
)
return {
"technique": "relocation_chart",
"status": "used",
"source_engine": relocated_chart["source_engine"],
"same_birth_utc": utc.isoformat(),
"source_local_time": local.isoformat(),
"relocation_local_time": relocation_local.isoformat(),
"relocation_timezone": relocation_label,
"relocation_coordinates": {
"latitude": float(relocation_latitude),
"longitude": float(relocation_longitude),
},
"relocated_chart": relocated_chart,
"boundary": "Same UTC birth instant with relocated coordinates and timezone; interpretation and relocation outcome judgment remain separate audited layers.",
}
def _request_field(body: dict[str, Any], *keys: str) -> Any:
for key in keys:
if key in body and body[key] not in (None, ""):