From 810bd356a16565c6cca75cf880e9816a28d05c23 Mon Sep 17 00:00:00 2001 From: 732642856 <732642856@qq.com> Date: Sat, 18 Jul 2026 03:46:20 +0800 Subject: [PATCH] feat: compare Jupiter and Saturn case transits --- frontend/src/mastra/index.ts | 2 +- frontend/tests/consultation-context.test.ts | 2 + scripts/domain_calculation_service.py | 5 +- scripts/reference_transparency_contract.py | 87 +++++++++++++++++-- tests/test_calculation_p0_regressions.py | 13 +++ tests/test_reference_transparency_contract.py | 4 + 6 files changed, 106 insertions(+), 7 deletions(-) diff --git a/frontend/src/mastra/index.ts b/frontend/src/mastra/index.ts index ea2a4c12..dcbf07b0 100644 --- a/frontend/src/mastra/index.ts +++ b/frontend/src/mastra/index.ts @@ -113,7 +113,7 @@ When reference_transparency is present: - Present candidate_windows and exact_triggers when relevant, but describe exact_triggers as technical trigger points, never guaranteed events. - Share a public case only when similar_public_cases.status is high_similarity_public_references_available. State the listed matching factors, dissimilar factors, event source URL, and that the case is reference-only. - If a shared case has reference_status public_context_only, state that it has not been replayed for calibration and cannot increase timing confidence. -- Treat similarity.timing_state as authoritative: status=matched means Vimshottari MD and AD both match; partial_match means only Vimshottari MD matches. Read narayana_status separately and never infer it from Vimshottari status. Never imply transits also match. +- Treat similarity.timing_state as authoritative: status=matched means Vimshottari MD and AD both match; partial_match means only Vimshottari MD matches. Read narayana_status and transit_status separately; never infer either from Vimshottari status. A transit_status match means only Jupiter and Saturn relative houses match, not that every transit matches. - When similar_public_cases.coverage.requested_uncovered_domains is non-empty, say the current public-case catalog does not yet cover those themes; do not infer that no comparable real-world case exists. - When method_variants applies, present parallel methods and their source paths rather than silently picking one result as the only truth. - If should_lead_with_limitations is false, do not lead with limitations. If a limitation is relevant, put it in one short sentence at the end. diff --git a/frontend/tests/consultation-context.test.ts b/frontend/tests/consultation-context.test.ts index 51539493..eac83739 100644 --- a/frontend/tests/consultation-context.test.ts +++ b/frontend/tests/consultation-context.test.ts @@ -12,5 +12,7 @@ test("passes transparent public-case references into the agent context", () => { assert.match(source, /timing_state/); assert.match(source, /partial_match/); assert.match(source, /narayana_status/); + assert.match(source, /transit_status/); + assert.match(source, /Jupiter and Saturn relative houses/); assert.match(source, /exact_triggers as technical trigger points/); }); diff --git a/scripts/domain_calculation_service.py b/scripts/domain_calculation_service.py index f778cf91..08fbd49d 100644 --- a/scripts/domain_calculation_service.py +++ b/scripts/domain_calculation_service.py @@ -19,7 +19,10 @@ from sade_sati import calc_sade_sati_complete CONTRACT_VERSION = "1.0.0" _SWISSEPH_LOCK = threading.RLock() -_PLANET_IDS = {"Saturn": swe.SATURN} +_PLANET_IDS = { + "Jupiter": swe.JUPITER, + "Saturn": swe.SATURN, +} class CalculationError(ValueError): diff --git a/scripts/reference_transparency_contract.py b/scripts/reference_transparency_contract.py index 51ac70d4..89dc04ac 100644 --- a/scripts/reference_transparency_contract.py +++ b/scripts/reference_transparency_contract.py @@ -15,13 +15,13 @@ if str(SCRIPT_DIR) not in sys.path: sys.path.insert(0, str(SCRIPT_DIR)) try: - from scripts.domain_calculation_service import compute_chart, compute_vimshottari_timeline + from scripts.domain_calculation_service import compute_chart, compute_transit_longitude, compute_vimshottari_timeline from scripts.timing_precision_contract import build_timing_precision_contract from scripts.varga import SIGNS, SIGN_LORDS, calc_varga from scripts.dasha_analyzer import build_antardasha from scripts.narayana_dasha import narayana_dasha_full_report except ModuleNotFoundError: # pragma: no cover - direct script execution - from domain_calculation_service import compute_chart, compute_vimshottari_timeline + from domain_calculation_service import compute_chart, compute_transit_longitude, compute_vimshottari_timeline from timing_precision_contract import build_timing_precision_contract from varga import SIGNS, SIGN_LORDS, calc_varga from dasha_analyzer import build_antardasha @@ -50,6 +50,8 @@ FEATURE_WEIGHTS = { "vimshottari_antardasha": 0.10, "narayana_mahadasha_sign": 0.10, "narayana_antardasha_sign": 0.10, + "jupiter_transit_house": 0.10, + "saturn_transit_house": 0.10, } HIGH_SIMILARITY_THRESHOLD = 0.75 @@ -170,6 +172,59 @@ def _narayana_state(chart: dict[str, Any], reference_date: str | None) -> dict[s return None +def _timezone_offset(chart: dict[str, Any]) -> float | None: + birth = chart.get("birth_info") if isinstance(chart, dict) else None + if not isinstance(birth, dict): + birth = chart.get("birth") if isinstance(chart, dict) else None + raw = birth.get("tz") if isinstance(birth, dict) else None + try: + return float(str(raw).replace("UTC", "")) + except (TypeError, ValueError): + return None + + +@lru_cache(maxsize=256) +def _transit_sign_index(planet: str, reference_date: str, tz: float, ayanamsa: str) -> int | None: + try: + longitude = compute_transit_longitude( + planet=planet, + reference_date=reference_date, + tz=tz, + ayanamsa=ayanamsa, + )["longitude"] + return int(float(longitude) / 30) % 12 + except (KeyError, TypeError, ValueError): + return None + + +def _transit_state(chart: dict[str, Any], reference_date: str | None) -> dict[str, Any] | None: + if not isinstance(reference_date, str): + return None + try: + datetime.strptime(reference_date[:10], "%Y-%m-%d") + except ValueError: + return None + ascendant = chart.get("ascendant") if isinstance(chart, dict) else None + asc_sign = ascendant.get("sign") if isinstance(ascendant, dict) else None + tz = _timezone_offset(chart) + if asc_sign not in SIGNS or tz is None: + return None + birth = chart.get("birth_info") if isinstance(chart, dict) else None + if not isinstance(birth, dict): + birth = chart.get("birth") if isinstance(chart, dict) else None + ayanamsa = birth.get("ayanamsa_name", "lahiri") if isinstance(birth, dict) else "lahiri" + asc_idx = SIGNS.index(asc_sign) + jupiter = _transit_sign_index("Jupiter", reference_date[:10], tz, str(ayanamsa)) + saturn = _transit_sign_index("Saturn", reference_date[:10], tz, str(ayanamsa)) + if jupiter is None or saturn is None: + return None + return { + "jupiter_transit_house": (jupiter - asc_idx) % 12 + 1, + "saturn_transit_house": (saturn - asc_idx) % 12 + 1, + "reference_date": reference_date[:10], + } + + def _features(chart: dict[str, Any], domain: str) -> dict[str, Any]: ascendant = chart.get("ascendant") if isinstance(chart, dict) else None rahu, ketu = _sign(chart, "Rahu"), _sign(chart, "Ketu") @@ -231,6 +286,8 @@ def _similarity( case_dasha = _vimshottari_state(case_chart, case_event_date) user_narayana = _narayana_state(user_chart, reference_date) case_narayana = _narayana_state(case_chart, case_event_date) + user_transit = _transit_state(user_chart, reference_date) + case_transit = _transit_state(case_chart, case_event_date) if user_dasha is not None and case_dasha is not None: user["vimshottari_mahadasha"] = user_dasha["mahadasha"] candidate["vimshottari_mahadasha"] = case_dasha["mahadasha"] @@ -266,6 +323,24 @@ def _similarity( timing_state["case_event_narayana"] = case_narayana else: timing_state["narayana_status"] = "not_compared" + if user_transit is not None and case_transit is not None: + user["jupiter_transit_house"] = user_transit["jupiter_transit_house"] + user["saturn_transit_house"] = user_transit["saturn_transit_house"] + candidate["jupiter_transit_house"] = case_transit["jupiter_transit_house"] + candidate["saturn_transit_house"] = case_transit["saturn_transit_house"] + timing_state["transit_status"] = ( + "matched" + if user_transit["jupiter_transit_house"] == case_transit["jupiter_transit_house"] + and user_transit["saturn_transit_house"] == case_transit["saturn_transit_house"] + else "partial_match" + if user_transit["jupiter_transit_house"] == case_transit["jupiter_transit_house"] + or user_transit["saturn_transit_house"] == case_transit["saturn_transit_house"] + else "different" + ) + timing_state["user_transit"] = user_transit + timing_state["case_event_transit"] = case_transit + else: + timing_state["transit_status"] = "not_compared" matching, dissimilar, total = [], [], 0.0 for name, weight in FEATURE_WEIGHTS.items(): if name not in user or name not in candidate: @@ -283,12 +358,14 @@ def _similarity( compared_vargas.append("D9") if domain == "career" and all(user.get(name) is not None and candidate.get(name) is not None for name in ("d10_ascendant", "d10_sun")): compared_vargas.append("D10") - uncompared = ["transit_event_state"] + uncompared = [] if timing_state["status"] == "not_compared": uncompared.insert(0, "vimshottari_mahadasha") uncompared.insert(1, "vimshottari_antardasha") if timing_state["narayana_status"] == "not_compared": - uncompared.insert(-1, "narayana_dasha") + uncompared.append("narayana_dasha") + if timing_state["transit_status"] == "not_compared": + uncompared.append("transit_event_state") if domain == "marriage" and "D9" not in compared_vargas: uncompared.insert(0, "D9") if domain == "career" and "D10" not in compared_vargas: @@ -297,7 +374,7 @@ def _similarity( "score": score, "matching_factors": matching, "dissimilar_factors": dissimilar, - "feature_scope": "D1 ascendant, Moon, theme-house lord, Rahu/Ketu axis" + (f", {'/'.join(compared_vargas)}" if compared_vargas else "") + (", Vimshottari MD/AD" if timing_state["status"] != "not_compared" else "") + (", Narayana MD/AD" if timing_state["narayana_status"] != "not_compared" else ""), + "feature_scope": "D1 ascendant, Moon, theme-house lord, Rahu/Ketu axis" + (f", {'/'.join(compared_vargas)}" if compared_vargas else "") + (", Vimshottari MD/AD" if timing_state["status"] != "not_compared" else "") + (", Narayana MD/AD" if timing_state["narayana_status"] != "not_compared" else "") + (", Jupiter/Saturn transit houses" if timing_state["transit_status"] != "not_compared" else ""), "uncompared_layers": uncompared, "timing_state": timing_state, } diff --git a/tests/test_calculation_p0_regressions.py b/tests/test_calculation_p0_regressions.py index 05338e08..08de70b6 100644 --- a/tests/test_calculation_p0_regressions.py +++ b/tests/test_calculation_p0_regressions.py @@ -78,6 +78,19 @@ def test_sade_sati_uses_real_saturn_transit_for_reference_date() -> None: assert result["provenance"]["reference_date"] == "2026-07-11" +def test_jupiter_transit_is_available_from_the_native_oracle() -> None: + result = calculation_service.compute_transit_longitude( + planet="Jupiter", + reference_date="2026-07-11", + tz=5.5, + ayanamsa="lahiri", + ) + + assert result["planet"] == "Jupiter" + assert 0 <= result["longitude"] < 360 + assert result["data_layer"] == "true_transit_positions" + + def test_timezone_inference_fails_closed(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setattr( calculation_service, diff --git a/tests/test_reference_transparency_contract.py b/tests/test_reference_transparency_contract.py index 27ab4ebf..a5acd6ab 100644 --- a/tests/test_reference_transparency_contract.py +++ b/tests/test_reference_transparency_contract.py @@ -127,11 +127,15 @@ def test_same_event_date_compares_vimshottari_mahadasha() -> None: "vimshottari_antardasha", "narayana_mahadasha_sign", "narayana_antardasha_sign", + "jupiter_transit_house", + "saturn_transit_house", }.issubset(similarity["matching_factors"]) assert similarity["timing_state"]["status"] == "matched" assert similarity["timing_state"]["narayana_status"] == "matched" + assert similarity["timing_state"]["transit_status"] == "matched" assert "vimshottari_antardasha" not in similarity["uncompared_layers"] assert "narayana_dasha" not in similarity["uncompared_layers"] + assert "transit_event_state" not in similarity["uncompared_layers"] def test_pending_health_case_is_context_only_not_calibration() -> None: