feat: compare Narayana case timing states

This commit is contained in:
732642856
2026-07-18 03:32:18 +08:00
parent f288bb8eeb
commit d014bb67e2
4 changed files with 72 additions and 6 deletions
+1 -1
View File
@@ -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.
@@ -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/);
});
+62 -4
View File
@@ -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,
}
@@ -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: