#!/usr/bin/env python3 """Annual Mudda Dasha from the natal Moon nakshatra (BUG-1216). Astrologer round-5 ruling 2026-10-03, Q7: the first Mudda lord is (completed years + natal Moon nakshatra number 1..27 - 2) mod 9, remainder 1 Sun, 2 Moon, 3 Mars, 4 Rahu, 5 Jupiter, 6 Saturn, 7 Mercury, 8 Ketu, 0 Venus. It does not depend on the year lord. The rule itself is `tajika.calc_mudda_with_dasha_balance`, ported verbatim from upstream yinduzhanxing c7117f92. This module is the single product entry both annual producers (`solar_return.solar_return_full_report` and `jyotish_engine.cmd_tajika`) call, so `annual_tajika_pack` compares two runs of the same rule. What it adds on top of the upstream function: - the balance comes from the natal Moon (`moon_at_birth_time`); - the year length is named: `solar_year_365_25` (product default, the upstream default) or `savana_360` (360-day year, research only); - `periods` are cropped to the annual window [annual_start, annual_end) and carry `months` (share of 12) for the existing month-window consumers; the upstream ten-group surface (the last group runs past the year end) stays in `periods_full`. Upstream's sub-periods are not carried (no product surface shows them). The legacy `tajika.calc_mudda_dasha` (first lord = year lord) is no longer called from any product path. """ from __future__ import annotations from datetime import datetime from typing import Any, Dict, Optional try: # scripts/ on sys.path from tajika import calc_mudda_with_dasha_balance except ImportError: # pragma: no cover - package import from scripts.tajika import calc_mudda_with_dasha_balance BALANCE_PROFILE = "moon_at_birth_time" YEAR_LENGTHS = {"solar_year_365_25": 365.25, "savana_360": 360.0} DEFAULT_YEAR_LENGTH = "solar_year_365_25" SOURCE = "tajika.calc_mudda_with_dasha_balance (upstream yinduzhanxing c7117f92)" RULE = "astrologer_round5_q7_completed_years_plus_natal_nakshatra_minus_two_mod_nine" RULE_TEXT = "起运星 =(已满年数 + 出生月宿序号 − 2)mod 9,与年主无关" RULE_TEXT_EN = "First lord = (completed years + natal Moon nakshatra number - 2) mod 9; independent of the year lord" def _parse(value: str) -> datetime: return datetime.fromisoformat(str(value)) def annual_mudda_dasha( *, annual_start: Optional[datetime], completed_years: Optional[int], natal_moon_longitude: Optional[float], year_length: str = DEFAULT_YEAR_LENGTH, ) -> Dict[str, Any]: """Mudda Dasha for one Varsha year, anchored at the solar-return instant.""" if year_length not in YEAR_LENGTHS: return {"status": "blocked", "reason": "unknown_year_length", "year_length_basis": year_length} if completed_years is None or natal_moon_longitude is None: return {"status": "blocked", "reason": "completed_years_and_natal_moon_required_for_mudda", "balance_profile": BALANCE_PROFILE} raw = calc_mudda_with_dasha_balance( annual_start=annual_start, completed_years=int(completed_years), natal_moon_longitude=natal_moon_longitude, balance_moon_longitude=natal_moon_longitude, balance_profile=BALANCE_PROFILE, annual_duration_days=YEAR_LENGTHS[year_length], ) if raw.get("status") == "blocked": return {**raw, "year_length_basis": year_length, "source": SOURCE} start = _parse(raw["annual_start"]) end = _parse(raw["annual_end"]) year_days = float(raw["annual_duration_days"]) periods = [] for row in raw.get("periods") or []: p_start = max(_parse(row["start"]), start) p_end = min(_parse(row["end"]), end) if p_start >= p_end: continue days = (p_end - p_start).total_seconds() / 86400.0 periods.append({ "order": len(periods) + 1, "lord": row["lord"], "months": round(days / year_days * 12.0, 2), "duration_days": days, "full_duration_days": row.get("full_duration_days"), "start": p_start.isoformat(sep=" "), "end": p_end.isoformat(sep=" "), }) return { # The first-lord rule is the astrologer's round-5 ruling; dates are # not externally replayed (upstream keeps its own parameter_sensitive). "status": "partial_verified", "upstream_status": raw.get("status"), "starting_lord": raw.get("starting_lord"), "starting_lord_basis": raw.get("starting_lord_basis"), "rule": RULE, "rule_text": RULE_TEXT, "rule_text_en": RULE_TEXT_EN, "annual_start": raw.get("annual_start"), "annual_end": raw.get("annual_end"), "annual_duration_days": year_days, "year_length_basis": year_length, "completed_years": raw.get("completed_years"), "natal_moon_longitude": raw.get("natal_moon_longitude"), "natal_moon_nakshatra_number": raw.get("natal_moon_nakshatra_number"), "balance_profile": raw.get("balance_profile"), "balance_fraction_remaining": raw.get("balance_fraction_remaining"), "periods": periods, "periods_full": raw.get("periods"), "independent_of_year_lord": True, "source": SOURCE, }