feat: compare Vimshottari case timing states

This commit is contained in:
732642856
2026-07-18 03:17:45 +08:00
parent c9ce5cbc14
commit 77d2d30df6
5 changed files with 104 additions and 8 deletions
+1
View File
@@ -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.
@@ -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/);
});
+1
View File
@@ -720,6 +720,7 @@ def execute_consultation_workflow(
chart,
themes,
timing=body.get('timing'),
reference_date=_consultation_reference_date(body).date().isoformat(),
)
return result
+79 -8
View File
@@ -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,
),
}
@@ -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