Files
Jyotisha/scripts/kp_monthly_report_packet.py
T
Jesse_Chen bab0718700
Independent Staging Quality Gate / validate (push) Failing after 9m41s
Independent Staging Quality Gate / publish (push) Has been skipped
feat(report): ship full-mode longform appendix beside the five-chapter report
Web export now calls the same full pack as the long skill report and caches an owner-only Markdown download. Appendix failure stays unavailable and does not change the main report status.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-05 11:10:44 +08:00

220 lines
8.5 KiB
Python
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""Assembler for the KP three-year monthly report packet."""
from __future__ import annotations
import sys
from datetime import datetime
from pathlib import Path
from types import SimpleNamespace
try: # pragma: no cover - import path differs under CLI vs pytest
from domain_calculation_service import compute_chart
from kp_monthly_report_contract import build_kp_monthly_report_contract
from kp_monthly_theme_support import build_kp_monthly_theme_support
from kp_monthly_transits import build_monthly_transit_snapshot
from kp_monthly_vimshottari import build_monthly_vimshottari_snapshot
from kp_system import build_kp_western_support_surface, kp_maturity_profile
except ImportError: # pragma: no cover
from scripts.domain_calculation_service import compute_chart
from scripts.kp_monthly_report_contract import build_kp_monthly_report_contract
from scripts.kp_monthly_theme_support import build_kp_monthly_theme_support
from scripts.kp_monthly_transits import build_monthly_transit_snapshot
from scripts.kp_monthly_vimshottari import build_monthly_vimshottari_snapshot
from scripts.kp_system import build_kp_western_support_surface, kp_maturity_profile
_HUMAN_LABELS = {
"Jupiter": "木星",
"Saturn": "土星",
"Rahu": "北交点",
"Ketu": "南交点",
"Aries": "白羊座",
"Taurus": "金牛座",
"Gemini": "双子座",
"Cancer": "巨蟹座",
"Leo": "狮子座",
"Virgo": "处女座",
"Libra": "天秤座",
"Scorpio": "天蝎座",
"Sagittarius": "射手座",
"Capricorn": "摩羯座",
"Aquarius": "水瓶座",
"Pisces": "双鱼座",
}
def _add_months(start_year: int, start_month: int, offset: int) -> tuple[int, int]:
month_index = (start_month - 1) + offset
return start_year + (month_index // 12), (month_index % 12) + 1
def _label(value: str | None) -> str:
if not value:
return "-"
return _HUMAN_LABELS.get(value, value)
def _build_slow_planet_signals(
current_transits: dict,
next_transits: dict | None,
) -> list[str]:
signals = []
current_planets = current_transits.get("planets") if isinstance(current_transits.get("planets"), dict) else {}
next_planets = next_transits.get("planets") if isinstance(next_transits, dict) and isinstance(next_transits.get("planets"), dict) else {}
for planet in ("Jupiter", "Saturn", "Rahu", "Ketu"):
current = current_planets.get(planet) if isinstance(current_planets.get(planet), dict) else {}
nxt = next_planets.get(planet) if isinstance(next_planets.get(planet), dict) else {}
current_sign = current.get("sign")
next_sign = nxt.get("sign")
if current_sign and next_sign and current_sign != next_sign:
signals.append(f"{_label(planet)}换座:{_label(current_sign)}{_label(next_sign)}")
elif current.get("retrograde"):
signals.append(f"{_label(planet)}逆行强调")
return signals
def _highlight_reason(row: dict) -> str:
signals = row.get("slow_planet_signals") if isinstance(row.get("slow_planet_signals"), list) else []
if signals:
return "".join(str(item) for item in signals[:2])
levels = row.get("vimshottari_five_levels") if isinstance(row.get("vimshottari_five_levels"), dict) else {}
level_rows = levels.get("levels") if isinstance(levels.get("levels"), dict) else {}
md = level_rows.get("mahadasha") if isinstance(level_rows.get("mahadasha"), dict) else {}
ad = level_rows.get("antardasha") if isinstance(level_rows.get("antardasha"), dict) else {}
return f"主运/次运焦点:{_label(md.get('lord'))} / {_label(ad.get('lord'))}"
def _build_yearly_highlights(month_rows: list[dict]) -> list[dict]:
years: dict[int, list[dict]] = {}
for row in month_rows:
month = str(row.get("month") or "")
if len(month) < 4:
continue
year = int(month[:4])
years.setdefault(year, []).append(row)
yearly = []
for year in sorted(years):
ranked = sorted(
years[year],
key=lambda item: (
len(item.get("slow_planet_signals") or []),
1 if any("换座" in str(signal) for signal in (item.get("slow_planet_signals") or [])) else 0,
),
reverse=True,
)
picks = []
for row in ranked:
picks.append({"month": row.get("month"), "reason": _highlight_reason(row), "status": "parameter_sensitive"})
if len(picks) == 4:
break
yearly.append({"year": year, "months": picks, "status": "parameter_sensitive"})
return yearly
def _resolve_cmd_kp():
try: # pragma: no cover - script execution path
from __main__ import cmd_kp as resolved
return resolved
except ImportError:
pass
scripts_dir = Path(__file__).resolve().parent
if str(scripts_dir) not in sys.path:
sys.path.insert(0, str(scripts_dir))
try: # pragma: no cover - pytest/module import path
from jyotish_engine import cmd_kp as resolved
return resolved
except ImportError: # pragma: no cover
from scripts.jyotish_engine import cmd_kp as resolved
return resolved
def build_kp_monthly_report_packet(
*,
birth_payload: dict,
start_month: str,
month_count: int,
western_support: dict | None = None,
) -> dict:
start_year, start_month_number = [int(part) for part in start_month.split("-", 1)]
natal_chart = compute_chart({**birth_payload, "ayanamsa": "lahiri", "node_mode": "mean"})
moon_lon = natal_chart.get("planets", {}).get("Moon", {}).get("degree_raw")
kp_args = SimpleNamespace(
year=birth_payload["year"],
month=birth_payload["month"],
day=birth_payload["day"],
hour=birth_payload["hour"],
minute=birth_payload["minute"],
second=birth_payload.get("second", 0),
lat=birth_payload["lat"],
lon=birth_payload["lon"],
tz=birth_payload["tz"],
node_mode="mean",
ayanamsa="lahiri",
)
natal_kp = _resolve_cmd_kp()(kp_args)
packet = build_kp_monthly_report_contract(
start_month=start_month,
month_count=month_count,
timezone_offset=float(birth_payload["tz"]),
)
maturity_profile = kp_maturity_profile()
western_support_surface = build_kp_western_support_surface(
western_support,
maturity_profile=maturity_profile,
)
packet["supporting_systems"]["western_kp_support"] = western_support_surface
packet["western_support"] = western_support_surface
packet["months"] = []
month_rows = []
for offset in range(month_count):
year, month = _add_months(start_year, start_month_number, offset)
anchor_dt = datetime(year, month, 1, 12, 0, 0)
monthly_levels = build_monthly_vimshottari_snapshot(
birth_dt=datetime(
birth_payload["year"],
birth_payload["month"],
birth_payload["day"],
birth_payload["hour"],
birth_payload["minute"],
birth_payload.get("second", 0),
),
moon_lon=moon_lon,
anchor_dt=anchor_dt,
)
monthly_transits = build_monthly_transit_snapshot(
year=year,
month=month,
lat=float(birth_payload["lat"]),
lon=float(birth_payload["lon"]),
tz=float(birth_payload["tz"]),
)
theme_support = build_kp_monthly_theme_support(
natal_kp=natal_kp,
monthly_levels=monthly_levels.get("levels") or {},
monthly_transits=monthly_transits,
)
month_rows.append(
{
"month": f"{year:04d}-{month:02d}",
"anchor_local": monthly_transits["anchor_local"],
"vimshottari_five_levels": monthly_levels,
"monthly_transits": monthly_transits,
"theme_support": theme_support,
"status": "parameter_sensitive",
"must_not_claim": packet["must_not_claim"],
}
)
for index, row in enumerate(month_rows):
next_transits = None
if index + 1 < len(month_rows):
next_transits = month_rows[index + 1].get("monthly_transits")
row["slow_planet_signals"] = _build_slow_planet_signals(
row.get("monthly_transits") or {},
next_transits,
)
packet["months"] = month_rows
packet["yearly_highlights"] = _build_yearly_highlights(month_rows)
packet["natal_kp"] = natal_kp
return packet