diff --git a/frontend/src/mastra/index.ts b/frontend/src/mastra/index.ts index 28e7944e..c75929ec 100644 --- a/frontend/src/mastra/index.ts +++ b/frontend/src/mastra/index.ts @@ -113,6 +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: only status matched permits saying Vimshottari Mahadasha matches; different or not_compared must be described as such, and never imply Antardasha, Narayana, or 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 b573d3a7..b607698b 100644 --- a/frontend/tests/consultation-context.test.ts +++ b/frontend/tests/consultation-context.test.ts @@ -9,5 +9,6 @@ test("passes transparent public-case references into the agent context", () => { assert.match(source, /high_similarity_public_references_available/); assert.match(source, /requested_uncovered_domains/); assert.match(source, /public_context_only/); + assert.match(source, /timing_state/); assert.match(source, /exact_triggers as technical trigger points/); }); diff --git a/scripts/jyotish_api_server.py b/scripts/jyotish_api_server.py index 6a589912..6ee62ed4 100644 --- a/scripts/jyotish_api_server.py +++ b/scripts/jyotish_api_server.py @@ -720,6 +720,7 @@ def execute_consultation_workflow( chart, themes, timing=body.get('timing'), + reference_date=_consultation_reference_date(body).date().isoformat(), ) return result diff --git a/scripts/reference_transparency_contract.py b/scripts/reference_transparency_contract.py index 5d5e0960..90a19bc3 100644 --- a/scripts/reference_transparency_contract.py +++ b/scripts/reference_transparency_contract.py @@ -5,6 +5,7 @@ from __future__ import annotations import json import sys +from datetime import datetime from functools import lru_cache from pathlib import Path from typing import Any @@ -14,11 +15,11 @@ if str(SCRIPT_DIR) not in sys.path: sys.path.insert(0, str(SCRIPT_DIR)) try: - from scripts.domain_calculation_service import compute_chart + 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 except ModuleNotFoundError: # pragma: no cover - direct script execution - from domain_calculation_service import compute_chart + 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 @@ -41,6 +42,7 @@ FEATURE_WEIGHTS = { "d9_venus": 0.10, "d10_ascendant": 0.10, "d10_sun": 0.10, + "vimshottari_mahadasha": 0.15, } HIGH_SIMILARITY_THRESHOLD = 0.75 @@ -85,6 +87,43 @@ def _varga_sign(chart: dict[str, Any], key: str, division: int) -> str | None: return calc_varga(longitude, division)["sign"] if longitude is not None else None +def _birth_datetime(chart: dict[str, Any]) -> datetime | 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 + if not isinstance(birth, dict) or not isinstance(birth.get("date"), str): + return None + raw_time = birth.get("time") + if not isinstance(raw_time, str): + raw_time = f"{int(birth.get('hour', 0)):02d}:{int(birth.get('minute', 0)):02d}:{int(birth.get('second', 0)):02d}" + try: + return datetime.fromisoformat(f"{birth['date']}T{raw_time}") + except ValueError: + return None + + +def _vimshottari_mahadasha(chart: dict[str, Any], reference_date: str | None) -> str | None: + if not isinstance(reference_date, str): + return None + try: + target = datetime.fromisoformat(reference_date[:10]) + except ValueError: + return None + birth_dt, moon_lon = _birth_datetime(chart), _longitude(chart, "Moon") + if birth_dt is None or moon_lon is None: + return None + try: + periods = compute_vimshottari_timeline(birth_dt=birth_dt, moon_lon=moon_lon)["periods"] + for period in periods: + start = datetime.fromisoformat(period["start"]) + end = datetime.fromisoformat(period["end"]) + if start <= target <= end: + return period["lord"] + except (KeyError, TypeError, ValueError): + return None + 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") @@ -136,9 +175,26 @@ def _chart_for_case(case: dict[str, Any]) -> dict[str, Any] | None: ) -def _similarity(user_chart: dict[str, Any], case_chart: dict[str, Any], domain: str) -> dict[str, Any]: +def _similarity( + user_chart: dict[str, Any], case_chart: dict[str, Any], domain: str, + *, reference_date: str | None = None, case_event_date: str | None = None, +) -> dict[str, Any]: user = _features(user_chart, domain) candidate = _features(case_chart, domain) + user_md = _vimshottari_mahadasha(user_chart, reference_date) + case_md = _vimshottari_mahadasha(case_chart, case_event_date) + if user_md is not None and case_md is not None: + user["vimshottari_mahadasha"] = user_md + candidate["vimshottari_mahadasha"] = case_md + timing_state = { + "status": "matched" if user_md == case_md else "different", + "user_vimshottari_mahadasha": user_md, + "case_event_vimshottari_mahadasha": case_md, + "reference_date": reference_date, + "case_event_date": case_event_date, + } + else: + timing_state = {"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: @@ -156,7 +212,9 @@ def _similarity(user_chart: dict[str, Any], case_chart: dict[str, Any], domain: 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 = ["dasha_event_state", "transit_event_state"] + uncompared = ["vimshottari_antardasha", "narayana_dasha", "transit_event_state"] + if timing_state["status"] == "not_compared": + uncompared.insert(0, "vimshottari_mahadasha") if domain == "marriage" and "D9" not in compared_vargas: uncompared.insert(0, "D9") if domain == "career" and "D10" not in compared_vargas: @@ -165,8 +223,9 @@ def _similarity(user_chart: dict[str, Any], case_chart: dict[str, Any], domain: "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 ""), + "feature_scope": "D1 ascendant, Moon, theme-house lord, Rahu/Ketu axis" + (f", {'/'.join(compared_vargas)}" if compared_vargas else "") + (", Vimshottari MD" if timing_state["status"] != "not_compared" else ""), "uncompared_layers": uncompared, + "timing_state": timing_state, } @@ -207,6 +266,7 @@ def select_similar_public_cases( themes: list[str], *, cases: list[dict[str, Any]] | None = None, + reference_date: str | None = None, threshold: float = HIGH_SIMILARITY_THRESHOLD, max_cases: int = 3, ) -> dict[str, Any]: @@ -231,7 +291,13 @@ def select_similar_public_cases( for event in case.get("event_outcomes", []): if not isinstance(event, dict) or event.get("domain") not in themes: continue - similarity = _similarity(user_chart, case_chart, event["domain"]) + similarity = _similarity( + user_chart, + case_chart, + event["domain"], + reference_date=reference_date, + case_event_date=event.get("event_date"), + ) if similarity["score"] < threshold: continue source = case.get("source") if isinstance(case.get("source"), dict) else {} @@ -270,7 +336,7 @@ def select_similar_public_cases( def build_reference_transparency_contract( chart: dict[str, Any], themes: list[str], *, timing: dict[str, Any] | None = None, - cases: list[dict[str, Any]] | None = None, + cases: list[dict[str, Any]] | None = None, reference_date: str | None = None, ) -> dict[str, Any]: timing_contract = build_timing_precision_contract(timing) return { @@ -296,5 +362,10 @@ def build_reference_transparency_contract( "source": "references/oracle/xalen_formula_unit_attribution_2026_07_17.json", "boundary": "流派/公式差异并列展示;不以单一引擎多数投票决定真值。", }, - "similar_public_cases": select_similar_public_cases(chart, themes, cases=cases), + "similar_public_cases": select_similar_public_cases( + chart, + themes, + cases=cases, + reference_date=reference_date, + ), } diff --git a/tests/test_reference_transparency_contract.py b/tests/test_reference_transparency_contract.py index ef0c01e0..a0cda346 100644 --- a/tests/test_reference_transparency_contract.py +++ b/tests/test_reference_transparency_contract.py @@ -106,6 +106,27 @@ def test_default_public_manifest_can_surface_a_matching_replayed_case() -> None: assert selected["cases"][0]["reference_only"] is True +def test_same_event_date_compares_vimshottari_mahadasha() -> None: + from scripts.domain_calculation_service import compute_chart + + jobs_chart = compute_chart({ + "year": 1955, "month": 2, "day": 24, "hour": 19, "minute": 15, + "lat": 37.7833, "lon": -122.4167, "tz": -8.0, + "ayanamsa": "lahiri", "node_mode": "mean", + }) + + selected = select_similar_public_cases( + jobs_chart, + ["career"], + reference_date="2007-01-09", + ) + + similarity = selected["cases"][0]["similarity"] + assert "vimshottari_mahadasha" in similarity["matching_factors"] + assert similarity["timing_state"]["status"] == "matched" + assert "vimshottari_antardasha" in similarity["uncompared_layers"] + + def test_pending_health_case_is_context_only_not_calibration() -> None: user_chart = _chart("Leo", "Pisces", "Libra") cases = [{ @@ -203,3 +224,4 @@ def test_default_manifest_exposes_kahlo_health_as_context_only() -> None: def test_consultation_api_exposes_reference_transparency_contract() -> None: source = (ROOT / "scripts" / "jyotish_api_server.py").read_text(encoding="utf-8") assert "result['reference_transparency'] = build_reference_transparency_contract(" in source + assert "reference_date=_consultation_reference_date(body).date().isoformat()" in source