feat(upstream): merge a6f47abd engine, MCP, and orchestrator
Independent Staging Quality Gate / validate (push) Has been cancelled
Independent Staging Quality Gate / publish (push) Has been cancelled

Three-way-merge calculation modules and pl9-export into the product fork while keeping commercial API routes, Raman ayanamsa, and the consultation contract as a keypath superset.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Jesse_Chen
2026-09-03 20:28:57 +08:00
co-authored by Cursor
parent 45d132588f
commit f224146348
30 changed files with 17151 additions and 248 deletions
+137 -5
View File
@@ -11,6 +11,10 @@ try:
from saham_daynight import determine_daytime
except ImportError:
from scripts.saham_daynight import determine_daytime
try:
from ayanamsa_utils import ayanamsa_display_name, normalize_ayanamsa_name, sidereal_flags
except ImportError:
from scripts.ayanamsa_utils import ayanamsa_display_name, normalize_ayanamsa_name, sidereal_flags
# Monday=0, matching datetime.weekday(). Values are the end of Saturn's share
@@ -37,10 +41,40 @@ SATURN_PART_START = {
6: {"day": 6, "night": 2},
}
# Planet indices follow the local seven-graha ordering: Sun through Saturn.
# Each row assigns the eight daylight/nighttime shares for a weekday where
# Python's datetime.weekday() is Monday=0.
DAY_RULERS = (
(1, 2, 3, 4, 5, 6, -1, 0),
(2, 3, 4, 5, 6, -1, 0, 1),
(3, 4, 5, 6, -1, 0, 1, 2),
(4, 5, 6, -1, 0, 1, 2, 3),
(5, 6, -1, 0, 1, 2, 3, 4),
(6, -1, 0, 1, 2, 3, 4, 5),
(0, 1, 2, 3, 4, 5, 6, -1),
)
NIGHT_RULERS = (
(5, 6, -1, 0, 1, 2, 3, 4),
(6, -1, 0, 1, 2, 3, 4, 5),
(-1, 0, 1, 2, 3, 4, 5, 6),
(0, 1, 2, 3, 4, 5, 6, -1),
(1, 2, 3, 4, 5, 6, -1, 0),
(2, 3, 4, 5, 6, -1, 0, 1),
(4, 5, 6, -1, 0, 1, 2, 3),
)
TEMPORAL_UPAGRAHAS = {
"Kaala": (0, "middle"),
"Mrityu": (2, "middle"),
"Artha_Praharaka": (3, "middle"),
"Yama_Ghantaka": (4, "middle"),
"Gulika": (6, "begin"),
"Maandi": (6, "middle"),
}
def _sidereal_ascendant(jd_ut: float, lat: float, lon: float) -> float:
swe.set_sid_mode(swe.SIDM_LAHIRI)
cusps, ascmc = swe.houses_ex(jd_ut, lat, lon, b"P", swe.FLG_SIDEREAL)
def _sidereal_ascendant(jd_ut: float, lat: float, lon: float, ayanamsa: str = "raman") -> float:
flags = sidereal_flags(swe, ayanamsa)
cusps, ascmc = swe.houses_ex(jd_ut, lat, lon, b"P", flags)
return float(ascmc[0]) % 360
@@ -51,8 +85,10 @@ def calculate_gulika(
lon: float,
tz: float,
method: str = "saturn_part_start",
ayanamsa: str = "raman",
) -> dict[str, Any]:
"""Return Gulika from local moment/location using Swiss sunrise and sunset."""
ayanamsa_name = normalize_ayanamsa_name(ayanamsa)
daynight = determine_daytime(moment, lat=lat, lon=lon, tz=tz)
is_day = bool(daynight["is_daytime"])
period = "day" if is_day else "night"
@@ -73,7 +109,7 @@ def calculate_gulika(
else:
raise ValueError("method must be saturn_part_start or legacy_ghatika_end")
segment_jd = start_jd + (end_jd - start_jd) * segment_fraction
longitude = _sidereal_ascendant(segment_jd, float(lat), float(lon))
longitude = _sidereal_ascendant(segment_jd, float(lat), float(lon), ayanamsa_name)
return {
"scope": "gulika_prasna_marga",
"status": "partial",
@@ -89,7 +125,103 @@ def calculate_gulika(
"ghatika_end": ghatika_end,
"segment_jd_ut": segment_jd,
"daynight_evidence": daynight,
"ayanamsa": "lahiri",
"ayanamsa": ayanamsa_name,
"ayanamsa_display": ayanamsa_display_name(ayanamsa_name),
"rule_source": "references/prashna-complete-guide.md#3.5",
"boundary": "PyJHora-aligned Saturn-part-start variant. Numeric parity is evidence only and does not enable Prashna verdict layers.",
}
def _temporal_upagraha(
moment: datetime,
*,
lat: float,
lon: float,
tz: float,
planet_index: int,
segment_part: str,
ayanamsa: str = "raman",
) -> dict[str, Any]:
ayanamsa_name = normalize_ayanamsa_name(ayanamsa)
daynight = determine_daytime(moment, lat=lat, lon=lon, tz=tz)
is_day = bool(daynight["is_daytime"])
period = "day" if is_day else "night"
start_jd = daynight["sunrise_jd_ut"] if is_day else daynight["sunset_jd_ut"]
end_jd = daynight["sunset_jd_ut"] if is_day else daynight["sunrise_jd_ut"] + 1.0
if end_jd <= start_jd:
end_jd += 1.0
start_year, start_month, start_day, _ = swe.revjul(start_jd + float(tz) / 24.0)
weekday = datetime(int(start_year), int(start_month), int(start_day)).weekday()
ruler_table = DAY_RULERS if is_day else NIGHT_RULERS
part_index = ruler_table[weekday].index(planet_index)
fraction = (part_index + (0.5 if segment_part == "middle" else 0.0)) / 8.0
segment_jd = start_jd + (end_jd - start_jd) * fraction
longitude = _sidereal_ascendant(segment_jd, float(lat), float(lon), ayanamsa_name)
return {
"longitude": round(longitude, 6),
"sign_idx": int(longitude / 30) % 12,
"degree_in_sign": round(longitude % 30, 6),
"period": period,
"weekday": weekday,
"planet_index": planet_index,
"part_index": part_index,
"segment_part": segment_part,
"segment_fraction": fraction,
"segment_jd_ut": segment_jd,
}
def _solar_upagrahas(sun_longitude: float) -> dict[str, dict[str, Any]]:
sun = float(sun_longitude) % 360.0
longitudes = {
"Dhuma": (sun + 133.0 + 20.0 / 60.0) % 360.0,
"Vyatipata": (360.0 - ((sun + 133.0 + 20.0 / 60.0) % 360.0)) % 360.0,
}
longitudes["Parivesha"] = (longitudes["Vyatipata"] + 180.0) % 360.0
longitudes["Indrachapa"] = (360.0 - longitudes["Parivesha"]) % 360.0
longitudes["Upaketu"] = (sun - 30.0) % 360.0
return {
name: {
"longitude": round(longitude, 6),
"sign_idx": int(longitude / 30) % 12,
"degree_in_sign": round(longitude % 30, 6),
"source": "solar_longitude_formula",
}
for name, longitude in longitudes.items()
}
def calculate_upagrahas(
moment: datetime,
*,
lat: float,
lon: float,
tz: float,
sun_longitude: float,
ayanamsa: str = "raman",
) -> dict[str, Any]:
"""Calculate the D1 Upagraha packet using local Swiss Ephemeris inputs."""
ayanamsa_name = normalize_ayanamsa_name(ayanamsa)
temporal = {
name: _temporal_upagraha(
moment,
lat=lat,
lon=lon,
tz=tz,
planet_index=planet_index,
segment_part=segment_part,
ayanamsa=ayanamsa_name,
)
for name, (planet_index, segment_part) in TEMPORAL_UPAGRAHAS.items()
}
return {
"status": "computed_pending_validation",
"raw": {**temporal, **_solar_upagrahas(sun_longitude)},
"ayanamsa": ayanamsa_name,
"ayanamsa_display": ayanamsa_display_name(ayanamsa_name),
"scope": "d1_only",
"boundary": (
"Local D1 formulas are retained with segment evidence. Public PyJHora observations are "
"used only for calibration; D-N Upagraha mapping and external numeric closure remain pending."
),
}