feat(qizheng): vendor stem-branch and expose three read-only chart APIs

Add Apache-2.0 @4n6h4x0r/stem-branch 0.8.0, a seven-governors adapter,
and POST /api/qizheng, /api/western, /api/ephemeris_events.

BUG-700 remains blocked (do not call --pillars). BUG-701 and BUG-702 are
resolved. BUG-703 is investigating (panchanga Lahiri). Skill is not bumped.
This commit is contained in:
jesse-ux
2026-09-15 16:10:21 +08:00
parent 3e58e85f8f
commit 25851dd338
28 changed files with 94708 additions and 2 deletions
+48
View File
@@ -21,6 +21,10 @@ try:
from western_evidence_packet import build_western_evidence_packet
except ImportError: # pragma: no cover - package import path
from scripts.western_evidence_packet import build_western_evidence_packet
try:
from ayanamsa_utils import DEFAULT_AYANAMSA_NAME, ayanamsa_display_name, temporary_ayanamsa
except ImportError: # pragma: no cover - package import path
from scripts.ayanamsa_utils import DEFAULT_AYANAMSA_NAME, ayanamsa_display_name, temporary_ayanamsa
_PLANETS = {
@@ -213,6 +217,50 @@ def build_tropical_natal_chart(
}
def _request_field(body: dict[str, Any], *keys: str) -> Any:
for key in keys:
if key in body and body[key] not in (None, ""):
return body[key]
raise ValueError(f"{keys[0]} is required")
def build_tropical_natal_from_request(body: dict[str, Any]) -> dict[str, Any]:
"""Read-only tropical natal chart for POST /api/western."""
if not isinstance(body, dict):
raise ValueError("JSON body must be an object")
house_system = str(body.get("house_system") or body.get("houseSystem") or "P").strip() or "P"
chart = build_tropical_natal_chart(
year=int(_request_field(body, "year")),
month=int(_request_field(body, "month")),
day=int(_request_field(body, "day")),
hour=int(_request_field(body, "hour")),
minute=int(_request_field(body, "minute")),
second=int(body.get("second") or 0),
latitude=float(_request_field(body, "lat", "latitude")),
longitude=float(_request_field(body, "lon", "longitude", "lng")),
timezone=_request_field(body, "tz", "timezone"),
house_system=house_system,
)
jd_ut = float(chart["calculation_contract"]["julian_day_ut"])
with temporary_ayanamsa(DEFAULT_AYANAMSA_NAME):
ayanamsa_degrees = round(float(swe.get_ayanamsa_ut(jd_ut)), 6)
chart["coordinate_system"] = "tropical_ecliptic"
chart["sidereal_offset_degrees"] = {
"reference": DEFAULT_AYANAMSA_NAME,
"display": ayanamsa_display_name(DEFAULT_AYANAMSA_NAME),
"degrees": ayanamsa_degrees,
}
chart["boundary"] = (
"这是回归黄道(分点黄经),不是恒星黄道,也不是七政宿度。"
f"{ayanamsa_display_name(DEFAULT_AYANAMSA_NAME)} 岁差在此刻约 {ayanamsa_degrees:.4f}°。"
"三套坐标系不得互相换算,也不得叠加成双重印证。"
" Natal tropical calculation only; it does not calculate transits, progressions, solar arcs, returns, or interpretation."
)
chart["success"] = True
chart["endpoint"] = "western"
return chart
def build_tropical_western_evidence_packet(*, route_packet: dict[str, Any], **birth: Any) -> dict[str, Any]:
"""Wrap direct natal calculation in the existing cross-system packet contract."""
chart = build_tropical_natal_chart(**birth)