diff --git a/frontend/src/mastra/index.ts b/frontend/src/mastra/index.ts index 3c2208df..ea2a4c12 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: matched means Vimshottari MD and AD both match; partial_match means only MD matches; different or not_compared must be described as such. Never imply Narayana or 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 separately and never infer it from Vimshottari status. Never imply transits also match. - 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 3daa54b4..51539493 100644 --- a/frontend/tests/consultation-context.test.ts +++ b/frontend/tests/consultation-context.test.ts @@ -11,5 +11,6 @@ test("passes transparent public-case references into the agent context", () => { assert.match(source, /public_context_only/); assert.match(source, /timing_state/); assert.match(source, /partial_match/); + assert.match(source, /narayana_status/); assert.match(source, /exact_triggers as technical trigger points/); }); diff --git a/scripts/reference_transparency_contract.py b/scripts/reference_transparency_contract.py index 62b67283..51ac70d4 100644 --- a/scripts/reference_transparency_contract.py +++ b/scripts/reference_transparency_contract.py @@ -17,13 +17,15 @@ if str(SCRIPT_DIR) not in sys.path: try: from scripts.domain_calculation_service import compute_chart, compute_vimshottari_timeline from scripts.timing_precision_contract import build_timing_precision_contract - from scripts.varga import SIGN_LORDS, calc_varga + 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 timing_precision_contract import build_timing_precision_contract - from varga import SIGN_LORDS, calc_varga + from varga import SIGNS, SIGN_LORDS, calc_varga from dasha_analyzer import build_antardasha + from narayana_dasha import narayana_dasha_full_report ROOT = Path(__file__).resolve().parents[1] @@ -46,6 +48,8 @@ FEATURE_WEIGHTS = { "d10_sun": 0.10, "vimshottari_mahadasha": 0.15, "vimshottari_antardasha": 0.10, + "narayana_mahadasha_sign": 0.10, + "narayana_antardasha_sign": 0.10, } HIGH_SIMILARITY_THRESHOLD = 0.75 @@ -132,6 +136,40 @@ def _vimshottari_state(chart: dict[str, Any], reference_date: str | None) -> dic return None +def _narayana_state(chart: dict[str, Any], reference_date: str | None) -> dict[str, str] | None: + if not isinstance(reference_date, str): + return None + try: + target = datetime.fromisoformat(reference_date[:10]) + except ValueError: + return None + birth_dt = _birth_datetime(chart) + ascendant = chart.get("ascendant") if isinstance(chart, dict) else None + asc_sign = ascendant.get("sign") if isinstance(ascendant, dict) else None + planets = chart.get("planets") if isinstance(chart, dict) else None + if birth_dt is None or asc_sign not in SIGNS or not isinstance(planets, dict): + return None + try: + planet_lons = {name: float(value["lon"]) for name, value in planets.items() if isinstance(value, dict) and value.get("lon") is not None} + if not planet_lons: + return None + age = (target - birth_dt).total_seconds() / (365.25 * 86400) + if age <= 0: + return None + current = narayana_dasha_full_report( + lagna_sign_idx=SIGNS.index(asc_sign), + planet_lons=planet_lons, + current_age=age, + birth_year=birth_dt.year, + ).get("current_dasha", {}) + md, ad = current.get("md"), current.get("ad") + if not isinstance(md, dict) or not isinstance(ad, dict): + return None + return {"mahadasha_sign": md.get("sign"), "antardasha_sign": ad.get("sign")} + except (KeyError, TypeError, ValueError): + return None + + 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") @@ -191,6 +229,8 @@ def _similarity( candidate = _features(case_chart, domain) user_dasha = _vimshottari_state(user_chart, reference_date) 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) if user_dasha is not None and case_dasha is not None: user["vimshottari_mahadasha"] = user_dasha["mahadasha"] candidate["vimshottari_mahadasha"] = case_dasha["mahadasha"] @@ -210,6 +250,22 @@ def _similarity( } else: timing_state = {"status": "not_compared"} + if user_narayana is not None and case_narayana is not None: + user["narayana_mahadasha_sign"] = user_narayana["mahadasha_sign"] + candidate["narayana_mahadasha_sign"] = case_narayana["mahadasha_sign"] + user["narayana_antardasha_sign"] = user_narayana["antardasha_sign"] + candidate["narayana_antardasha_sign"] = case_narayana["antardasha_sign"] + timing_state["narayana_status"] = ( + "matched" + if user_narayana == case_narayana + else "partial_match" + if user_narayana["mahadasha_sign"] == case_narayana["mahadasha_sign"] + else "different" + ) + timing_state["user_narayana"] = user_narayana + timing_state["case_event_narayana"] = case_narayana + else: + timing_state["narayana_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: @@ -227,10 +283,12 @@ 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 = ["narayana_dasha", "transit_event_state"] + uncompared = ["transit_event_state"] 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") if domain == "marriage" and "D9" not in compared_vargas: uncompared.insert(0, "D9") if domain == "career" and "D10" not in compared_vargas: @@ -239,7 +297,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 ""), + "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 ""), "uncompared_layers": uncompared, "timing_state": timing_state, } diff --git a/tests/test_reference_transparency_contract.py b/tests/test_reference_transparency_contract.py index c6331a84..27ab4ebf 100644 --- a/tests/test_reference_transparency_contract.py +++ b/tests/test_reference_transparency_contract.py @@ -122,9 +122,16 @@ def test_same_event_date_compares_vimshottari_mahadasha() -> None: ) similarity = selected["cases"][0]["similarity"] - assert {"vimshottari_mahadasha", "vimshottari_antardasha"}.issubset(similarity["matching_factors"]) + assert { + "vimshottari_mahadasha", + "vimshottari_antardasha", + "narayana_mahadasha_sign", + "narayana_antardasha_sign", + }.issubset(similarity["matching_factors"]) assert similarity["timing_state"]["status"] == "matched" + assert similarity["timing_state"]["narayana_status"] == "matched" assert "vimshottari_antardasha" not in similarity["uncompared_layers"] + assert "narayana_dasha" not in similarity["uncompared_layers"] def test_pending_health_case_is_context_only_not_calibration() -> None: