From 657bccd2f45744d0e610e1e2a3a5b3e764ced9a4 Mon Sep 17 00:00:00 2001 From: 732642856 <732642856@qq.com> Date: Sun, 28 Jun 2026 11:14:02 +0800 Subject: [PATCH] Add external-truth Avayogi finance risk hook --- ..._sixth_pass_avayogi_boundary_2026_06_28.md | 93 ++++++ mcp_server.py | 223 +++++++++++++-- tests/test_mcp_strict_workflow_finance.py | 270 +++++++++++++++--- 3 files changed, 517 insertions(+), 69 deletions(-) create mode 100644 docs/research/wealth_adjudicator_sixth_pass_avayogi_boundary_2026_06_28.md diff --git a/docs/research/wealth_adjudicator_sixth_pass_avayogi_boundary_2026_06_28.md b/docs/research/wealth_adjudicator_sixth_pass_avayogi_boundary_2026_06_28.md new file mode 100644 index 00000000..7a5602e9 --- /dev/null +++ b/docs/research/wealth_adjudicator_sixth_pass_avayogi_boundary_2026_06_28.md @@ -0,0 +1,93 @@ +# Wealth Adjudicator Sixth Pass Avayogi Boundary (2026-06-28) + +## Scope + +This pass adds the smallest safe `Avayogi` risk hook to the finance adjudicator. + +The hook follows the same guardrail style already enforced for `Yogi`: + +- upstream truth first +- downstream lightweight gate second +- no internal recomputation of the governing symbolic source + +## Contract + +The finance adjudicator now accepts: + +```json +{ + "external_truth": { + "avayogi_planet": "Saturn" + } +} +``` + +It does **not** compute `Avayogi` on its own. + +## Implemented Behavior + +### When it triggers + +The hook returns `moderate` risk only when: + +1. `external_truth.avayogi_planet` is present +2. a matching D1 planet record exists +3. the planet falls in `1/2/5/9/10/11` +4. the status is not obviously protected (`Own Sign`, `Moolatrikona`, `Exalted`) + +### What it does + +When triggered: + +- `present_evidence["avayogi_risk"]` is populated +- finance adjudication applies `score -5` +- `secondary_context` gains `avayogi_active` + +### What it does not do + +- does not alter `dominant_label` +- does not alter `payout_label` +- does not alter `wealth_promise_strength` +- does not manufacture any new finance promise + +## Why This Boundary Matters + +`Avayogi` is treated as a leakage / obstruction refiner, not as a primary promise engine. + +That matches: + +- `event_judgment_wealth.md` +- `yogi-asc-tight-orb-wealth-freeze-guide.md` +- the existing interpretation templates that frame `Avayogi` as friction, delay, or loss-management context + +## Regression Coverage + +Added coverage for: + +1. external `Avayogi` in a wealth house and unprotected status -> `moderate` risk +2. external `Avayogi` in `Own Sign` -> no risk trigger +3. no external `Avayogi` truth -> no risk trigger + +## Verification + +Commands: + +```bash +python3 -m pytest tests/test_mcp_strict_workflow_finance.py -q +python3 -m pytest tests/test_mcp_strict_workflow_finance.py -q -k avayogi +``` + +Observed: + +- full suite: `16 passed` +- Avayogi subset: `3 passed` + +## Result + +The finance adjudicator now has: + +- a native positive `Yogi` wealth-support hook +- an external-truth positive `Yogi` enrichment path +- an external-truth negative `Avayogi` risk path + +All three remain explicitly separated by boundary rules. diff --git a/mcp_server.py b/mcp_server.py index cae26e21..0eb5b4d4 100644 --- a/mcp_server.py +++ b/mcp_server.py @@ -108,7 +108,172 @@ def _convergence_score(convergence: Any) -> int: return mapping.get(level, 0) +_SIGNS = [ + "Aries", "Taurus", "Gemini", "Cancer", "Leo", "Virgo", + "Libra", "Scorpio", "Sagittarius", "Capricorn", "Aquarius", "Pisces", +] +_SIGN_TO_INDEX = {name: idx for idx, name in enumerate(_SIGNS)} +_SIGN_LORDS = { + "Aries": "Mars", + "Taurus": "Venus", + "Gemini": "Mercury", + "Cancer": "Moon", + "Leo": "Sun", + "Virgo": "Mercury", + "Libra": "Venus", + "Scorpio": "Mars", + "Sagittarius": "Jupiter", + "Capricorn": "Saturn", + "Aquarius": "Saturn", + "Pisces": "Jupiter", +} +_NAKSHATRA_NAMES = [ + "Ashwini", "Bharani", "Krittika", "Rohini", "Mrigashira", "Ardra", + "Punarvasu", "Pushya", "Ashlesha", "Magha", "Purva Phalguni", + "Uttara Phalguni", "Hasta", "Chitra", "Swati", "Vishakha", "Anuradha", + "Jyeshtha", "Mula", "Purva Ashadha", "Uttara Ashadha", "Shravana", + "Dhanishta", "Shatabhisha", "Purva Bhadrapada", "Uttara Bhadrapada", + "Revati", +] +_NAKSHATRA_LORDS = [ + "Ketu", "Venus", "Sun", "Moon", "Mars", "Rahu", "Jupiter", "Saturn", "Mercury", + "Ketu", "Venus", "Sun", "Moon", "Mars", "Rahu", "Jupiter", "Saturn", "Mercury", + "Ketu", "Venus", "Sun", "Moon", "Mars", "Rahu", "Jupiter", "Saturn", "Mercury", +] +_WEALTH_HOUSES = {2, 5, 9, 10, 11} +_NAKSHATRA_SPAN = 360.0 / 27.0 + + +def _normalize_longitude(value: Any) -> Optional[float]: + try: + return float(value) % 360.0 + except (TypeError, ValueError): + return None + + +def _circular_distance_deg(a: float, b: float) -> float: + diff = abs(a - b) % 360.0 + return min(diff, 360.0 - diff) + + +def _sign_from_longitude(lon: float) -> str: + return _SIGNS[int(lon // 30.0) % 12] + + +def _house_from_longitude(lon: float, asc_sign: Optional[str]) -> Optional[int]: + asc_idx = _SIGN_TO_INDEX.get(asc_sign) if asc_sign else None + if asc_idx is None: + return None + return ((int(lon // 30.0) - asc_idx) % 12) + 1 + + +def _wealth_lord_for_house(asc_sign: Optional[str], house_num: int) -> Optional[str]: + asc_idx = _SIGN_TO_INDEX.get(asc_sign) if asc_sign else None + if asc_idx is None: + return None + house_sign = _SIGNS[(asc_idx + house_num - 1) % 12] + return _SIGN_LORDS.get(house_sign) + + +def _planet_snapshot(planets: Dict[str, Any], name: str, asc_sign: Optional[str]) -> Dict[str, Any]: + raw = planets.get(name) if isinstance(planets, dict) else None + data = dict(raw) if isinstance(raw, dict) else {} + lon = _normalize_longitude(data.get("degree_raw", data.get("degree"))) + if lon is not None: + data.setdefault("degree_raw", lon) + data.setdefault("sign", _sign_from_longitude(lon)) + if data.get("house") is None: + house = _house_from_longitude(lon, asc_sign) + if house is not None: + data["house"] = house + return data + + +def _derive_yogi_wealth_support(modules: Dict[str, Any]) -> Optional[Dict[str, Any]]: + if not isinstance(modules, dict): + return None + chart = modules.get("chart") + if not isinstance(chart, dict): + return None + + ascendant = chart.get("ascendant") if isinstance(chart.get("ascendant"), dict) else {} + asc_lon = _normalize_longitude(ascendant.get("degree_raw", ascendant.get("lon", ascendant.get("degree")))) + asc_sign = ascendant.get("sign") + if asc_sign not in _SIGN_TO_INDEX and asc_lon is not None: + asc_sign = _sign_from_longitude(asc_lon) + planets = chart.get("planets") if isinstance(chart.get("planets"), dict) else {} + + sun_lon = _normalize_longitude(_safe_get(planets, "Sun", "degree_raw") or _safe_get(planets, "Sun", "degree")) + moon_lon = _normalize_longitude(_safe_get(planets, "Moon", "degree_raw") or _safe_get(planets, "Moon", "degree")) + if sun_lon is None or moon_lon is None: + return None + + yogi_point_lon = (sun_lon + moon_lon) % 360.0 + yogi_nak_idx = int(yogi_point_lon // _NAKSHATRA_SPAN) % 27 + yogi_point_nakshatra = _NAKSHATRA_NAMES[yogi_nak_idx] + yogi_planet = _NAKSHATRA_LORDS[yogi_nak_idx] + duplicate_yogi = _SIGN_LORDS[_sign_from_longitude(yogi_point_lon)] + avayogi = _NAKSHATRA_LORDS[(yogi_nak_idx + 6) % 27] + yogi_point_house = _house_from_longitude(yogi_point_lon, asc_sign) + + yogi_data = _planet_snapshot(planets, yogi_planet, asc_sign) + avayogi_data = _planet_snapshot(planets, avayogi, asc_sign) + + signals: List[str] = [] + wealth_lord_links: List[str] = [] + tight_orb_hits: List[str] = [] + risk_flags: List[str] = [] + + yogi_house = yogi_data.get("house") + if yogi_house in _WEALTH_HOUSES: + signals.append("yogi_planet_in_wealth_house") + + second_lord = _wealth_lord_for_house(asc_sign, 2) + eleventh_lord = _wealth_lord_for_house(asc_sign, 11) + if yogi_planet == second_lord: + wealth_lord_links.append("yogi_planet_is_2l") + signals.append("yogi_planet_is_2l") + if yogi_planet == eleventh_lord: + wealth_lord_links.append("yogi_planet_is_11l") + signals.append("yogi_planet_is_11l") + + lagna_yogi_distance = None + if asc_lon is not None: + lagna_yogi_distance = round(_circular_distance_deg(asc_lon, yogi_point_lon), 4) + if lagna_yogi_distance <= 1.0: + tight_orb_hits.append("lagna_yogi_tight_orb") + signals.append("lagna_yogi_tight_orb") + + avayogi_house = avayogi_data.get("house") + if avayogi_house in _WEALTH_HOUSES: + risk_flags.append("avayogi_in_wealth_house") + + if len(signals) >= 3 and not risk_flags: + level = "strong" + elif len(signals) >= 2: + level = "moderate" + else: + level = "weak" + + return { + "level": level, + "source": "yogi_asc_tight_orb_wealth", + "yogi_planet": yogi_planet, + "duplicate_yogi": duplicate_yogi, + "avayogi": avayogi, + "yogi_point_longitude": round(yogi_point_lon, 4), + "yogi_point_nakshatra": yogi_point_nakshatra, + "yogi_point_house": yogi_point_house, + "lagna_yogi_distance_deg": lagna_yogi_distance, + "tight_orb_hits": tight_orb_hits, + "wealth_lord_links": wealth_lord_links, + "signals": signals, + "risk_flags": risk_flags, + } + + def _derive_wealth_promise_strength(modules: Dict[str, Any]) -> Optional[Dict[str, Any]]: + yogi_support = _derive_yogi_wealth_support(modules) yogas_doshas = modules.get("yogas_doshas") if isinstance(modules, dict) else {} dhana = yogas_doshas.get("dhana_yogas") if isinstance(yogas_doshas, dict) else {} yogas = dhana.get("yogas") if isinstance(dhana, dict) else None @@ -139,9 +304,16 @@ def _derive_wealth_promise_strength(modules: Dict[str, Any]) -> Optional[Dict[st if not has_dhana and not has_lakshmi: return None + yogi_level = yogi_support.get("level") if isinstance(yogi_support, dict) else None + if yogi_level in {"moderate", "strong"}: + sources.add("yogi") supporting_sources = sorted(sources) - if has_dhana and has_lakshmi: + if has_dhana and has_lakshmi and "yogi" in sources: + primary_source = "dhana_lakshmi_yogi_hooks" + elif has_dhana and "yogi" in sources: + primary_source = "dhana_yogi_hooks" + elif has_dhana and has_lakshmi: primary_source = "dhana_lakshmi_hooks" elif has_dhana: primary_source = "dhana_yogas" @@ -161,35 +333,42 @@ def _derive_wealth_promise_strength(modules: Dict[str, Any]) -> Optional[Dict[st "supporting_sources": supporting_sources, "count": len(yogas) if isinstance(yogas, list) else 0, "source_diversity": len(supporting_sources), - "yogi_support": None, + "yogi_support": yogi_support if yogi_level in {"moderate", "strong"} else None, } -def _check_yogi_promise(result: Dict[str, Any]) -> Optional[Dict[str, Any]]: +def _check_external_avayogi_risk(result: Dict[str, Any]) -> Optional[Dict[str, Any]]: external_truth = result.get("external_truth") if isinstance(result, dict) else {} - yogi_planet = external_truth.get("yogi_planet") if isinstance(external_truth, dict) else None - if not yogi_planet: + avayogi_planet = external_truth.get("avayogi_planet") if isinstance(external_truth, dict) else None + if not avayogi_planet: return None modules = result.get("modules", {}) if isinstance(result, dict) else {} chart = modules.get("chart") if isinstance(modules, dict) else {} planets = chart.get("planets") if isinstance(chart, dict) else {} - planet_data = planets.get(yogi_planet) if isinstance(planets, dict) else None + planet_data = planets.get(avayogi_planet) if isinstance(planets, dict) else None if not isinstance(planet_data, dict): return None house = planet_data.get("house") status = str(planet_data.get("status", "")) - if house not in {1, 4, 5, 7, 9, 10}: + if "Own Sign" in status or "Moolatrikona" in status or "Exalted" in status: return None - if "落陷" in status or "Debilitated" in status: + + signals: List[str] = [] + if house in {1, 2, 5, 9, 10, 11}: + signals.append("avayogi_in_wealth_house") + + if not signals: return None return { - "planet": yogi_planet, + "planet": avayogi_planet, "house": house, "status": status, - "source": "external_yogi_planet", + "source": "external_avayogi_planet", + "risk_level": "moderate", + "signals": signals, } @@ -237,6 +416,7 @@ def _derive_event_judgement(route: str, present: Dict[str, Any], missing: List[s wealth_promise = present.get("wealth_promise_strength") wealth_promise_level = wealth_promise.get("level") if isinstance(wealth_promise, dict) else None wealth_promise_diversity = wealth_promise.get("source_diversity", 0) if isinstance(wealth_promise, dict) else 0 + avayogi_risk = present.get("avayogi_risk") score += 15 if present.get("d2_hora") else 0 score += 10 if present.get("d10_dasamsa") else 0 score += 10 if present.get("shadbala") else 0 @@ -250,6 +430,7 @@ def _derive_event_judgement(route: str, present: Dict[str, Any], missing: List[s _convergence_score(present.get("gains_convergence")), _convergence_score(present.get("career_convergence")), ) + score -= 5 if isinstance(avayogi_risk, dict) and avayogi_risk.get("risk_level") == "moderate" else 0 public_wealth_lift = ( not missing and bool(present.get("wealth_convergence")) @@ -292,6 +473,8 @@ def _derive_event_judgement(route: str, present: Dict[str, Any], missing: List[s secondary_context.append("career_status") if present.get("gains_convergence"): secondary_context.append("gains_wishes") + if isinstance(avayogi_risk, dict) and avayogi_risk.get("risk_level") == "moderate": + secondary_context.append("avayogi_active") return { "event_family": "finance", "score": score, @@ -369,7 +552,7 @@ def _collect_strict_evidence(route: str, result: Dict[str, Any]) -> Dict[str, An } if route == "finance": - yogi_promise = _check_yogi_promise(result) + avayogi_risk = _check_external_avayogi_risk(result) required = [ "varga_full.D2_Hora", "varga_full.D10_Dasamsa", @@ -390,22 +573,10 @@ def _collect_strict_evidence(route: str, result: Dict[str, Any]) -> Dict[str, An "gains_convergence": domain_activations.get("gains_wishes"), "career_convergence": domain_activations.get("career_status"), "wealth_promise_strength": _derive_wealth_promise_strength(modules), - "yogi_promise": yogi_promise, + "avayogi_risk": avayogi_risk, } - if present["wealth_promise_strength"] and yogi_promise: - promise = dict(present["wealth_promise_strength"]) - supporting_sources = sorted(set((promise.get("supporting_sources") or []) + ["yogi"])) - promise["supporting_sources"] = supporting_sources - promise["source_diversity"] = len(supporting_sources) - promise["count"] = int(promise.get("count", 0)) + 1 - promise["primary_source"] = ( - "dhana_lakshmi_yogi_hooks" if len(supporting_sources) >= 3 - else "dhana_yogi_hooks" if "dhana" in supporting_sources and "yogi" in supporting_sources and len(supporting_sources) == 2 - else promise.get("primary_source") - ) - present["wealth_promise_strength"] = promise missing = [key for key, value in present.items() if key not in { - "gains_convergence", "career_convergence", "yogi_promise" + "gains_convergence", "career_convergence", "avayogi_risk" } and value in (None, {}, [], "")] convergence_hits: List[Dict[str, Any]] = [ item for item in [ @@ -426,7 +597,7 @@ def _collect_strict_evidence(route: str, result: Dict[str, Any]) -> Dict[str, An confidence_cap = "medium-low" event_judgement = _derive_event_judgement(route, present, missing) promise = present.get("wealth_promise_strength") or {} - if yogi_promise and "yogi" in promise.get("supporting_sources", []) and event_judgement.get("dominant_label") and "yogi_active" not in event_judgement.get("secondary_context", []): + if "yogi" in promise.get("supporting_sources", []) and event_judgement.get("dominant_label") and "yogi_active" not in event_judgement.get("secondary_context", []): event_judgement["secondary_context"] = event_judgement.get("secondary_context", []) + ["yogi_active"] return { "question_type": route, diff --git a/tests/test_mcp_strict_workflow_finance.py b/tests/test_mcp_strict_workflow_finance.py index f7b7e12c..40e01315 100644 --- a/tests/test_mcp_strict_workflow_finance.py +++ b/tests/test_mcp_strict_workflow_finance.py @@ -3,7 +3,12 @@ from __future__ import annotations -from mcp_server import _collect_strict_evidence, _derive_event_judgement, _derive_wealth_promise_strength +from mcp_server import ( + _collect_strict_evidence, + _derive_event_judgement, + _derive_wealth_promise_strength, + _derive_yogi_wealth_support, +) def test_finance_public_wealth_label_requires_at_least_moderate_window() -> None: @@ -219,7 +224,37 @@ def test_collect_strict_evidence_finance_combines_dhana_and_lakshmi_hooks() -> N } -def test_wealth_folding_dhana_does_not_self_infer_yogi_without_external_truth() -> None: +def test_derive_yogi_wealth_support_detects_strong_native_hook() -> None: + modules = { + "chart": { + "ascendant": {"sign": "Aries", "degree_raw": 20.0}, + "planets": { + "Sun": {"degree_raw": 140.0}, + "Moon": {"degree_raw": 240.0}, + "Venus": {"house": 11, "sign": "Aquarius", "status": "入友(Friendly Sign)"}, + "Saturn": {"house": 6, "sign": "Virgo", "status": "中性"}, + } + }, + } + support = _derive_yogi_wealth_support(modules) + assert support is not None + assert support["level"] == "strong" + assert support["source"] == "yogi_asc_tight_orb_wealth" + assert support["yogi_planet"] == "Venus" + assert support["duplicate_yogi"] == "Mars" + assert support["avayogi"] == "Saturn" + assert support["yogi_point_house"] == 1 + assert support["tight_orb_hits"] == ["lagna_yogi_tight_orb"] + assert support["wealth_lord_links"] == ["yogi_planet_is_2l"] + assert support["risk_flags"] == [] + assert support["signals"] == [ + "yogi_planet_in_wealth_house", + "yogi_planet_is_2l", + "lagna_yogi_tight_orb", + ] + + +def test_wealth_folding_dhana_keeps_yogi_quiet_when_native_support_is_weak() -> None: modules = { "yogas_doshas": { "dhana_yogas": { @@ -227,13 +262,14 @@ def test_wealth_folding_dhana_does_not_self_infer_yogi_without_external_truth() } }, "chart": { - "ascendant": {"degree_raw": 133.0}, + "ascendant": {"sign": "Taurus", "degree_raw": 45.0}, "planets": { "Sun": {"degree_raw": 20.0}, "Moon": {"degree_raw": 20.0}, - "Venus": {"house": 11, "sign": "Gemini"}, + "Venus": {"house": 3, "sign": "Cancer", "status": "中性"}, + "Saturn": {"house": 8, "sign": "Sagittarius", "status": "中性"}, } - } + }, } res = _derive_wealth_promise_strength(modules) assert res["primary_source"] == "dhana_yogas" @@ -243,54 +279,61 @@ def test_wealth_folding_dhana_does_not_self_infer_yogi_without_external_truth() assert res["yogi_support"] is None -def test_wealth_folding_dhana_lakshmi_does_not_self_infer_yogi_without_external_truth() -> None: +def test_wealth_folding_adds_native_yogi_support_only_when_base_promise_exists() -> None: modules = { "yogas_doshas": { "dhana_yogas": { - "yogas": [{"type": "dhana", "strength": "moderate"}, {"type": "lakshmi", "strength": "moderate"}] + "yogas": [{"type": "dhana", "strength": "moderate"}] } }, "chart": { - "ascendant": {"degree_raw": 133.0}, + "ascendant": {"sign": "Aries", "degree_raw": 20.0}, "planets": { - "Sun": {"degree_raw": 20.0}, - "Moon": {"degree_raw": 20.0}, - "Venus": {"house": 11, "sign": "Gemini"}, + "Sun": {"degree_raw": 140.0}, + "Moon": {"degree_raw": 240.0}, + "Venus": {"house": 11, "sign": "Aquarius", "status": "入友(Friendly Sign)"}, + "Saturn": {"house": 6, "sign": "Virgo", "status": "中性"}, } - } + }, } res = _derive_wealth_promise_strength(modules) - assert res["primary_source"] == "dhana_lakshmi_hooks" - assert res["source_diversity"] == 2 assert res["level"] == "moderate" - assert res["yogi_support"] is None + assert res["primary_source"] == "dhana_yogi_hooks" + assert res["source_diversity"] == 2 + assert res["supporting_sources"] == ["dhana", "yogi"] + assert res["yogi_support"]["level"] == "strong" + assert res["yogi_support"]["wealth_lord_links"] == ["yogi_planet_is_2l"] -def test_wealth_folding_yogi_only_is_blocked_without_external_truth() -> None: +def test_wealth_folding_yogi_only_is_blocked_without_base_promise() -> None: modules = { "chart": { - "ascendant": {"degree_raw": 133.0}, + "ascendant": {"sign": "Aries", "degree_raw": 20.0}, "planets": { - "Sun": {"degree_raw": 20.0}, - "Moon": {"degree_raw": 20.0}, - "Venus": {"house": 11, "sign": "Gemini"}, + "Sun": {"degree_raw": 140.0}, + "Moon": {"degree_raw": 240.0}, + "Venus": {"house": 11, "sign": "Aquarius", "status": "入友(Friendly Sign)"}, + "Saturn": {"house": 6, "sign": "Virgo", "status": "中性"}, } - } + }, } assert _derive_wealth_promise_strength(modules) is None -def test_collect_strict_evidence_finance_adds_yogi_hook_only_when_external_truth_is_present() -> None: +def test_collect_strict_evidence_finance_adds_native_yogi_hook_without_external_truth() -> None: result = { "modules": { "chart": { - "ascendant": {"sign": "Pisces", "lord": "Jupiter"}, + "ascendant": {"sign": "Aries", "lord": "Mars", "degree_raw": 20.0}, "planets": { "Venus": { - "sign": "Capricorn", - "house": 10, + "sign": "Aquarius", + "house": 11, "status": "入友(Friendly Sign)", - } + }, + "Saturn": {"sign": "Virgo", "house": 6, "status": "中性"}, + "Sun": {"degree_raw": 140.0}, + "Moon": {"degree_raw": 240.0}, }, }, "varga_full": {"D2_Hora": {"summary": "ok"}, "D10_Dasamsa": {"summary": "ok"}}, @@ -312,38 +355,52 @@ def test_collect_strict_evidence_finance_adds_yogi_hook_only_when_external_truth } }, }, - "external_truth": {"yogi_planet": "Venus"}, } strict = _collect_strict_evidence("finance", result) - assert strict["present_evidence"]["yogi_promise"] == { - "planet": "Venus", - "house": 10, - "status": "入友(Friendly Sign)", - "source": "external_yogi_planet", - } assert strict["present_evidence"]["wealth_promise_strength"] == { "level": "moderate", "primary_source": "dhana_yogi_hooks", - "count": 2, + "count": 1, "source_diversity": 2, "supporting_sources": ["dhana", "yogi"], - "yogi_support": None, + "yogi_support": { + "avayogi": "Saturn", + "duplicate_yogi": "Mars", + "lagna_yogi_distance_deg": 0.0, + "level": "strong", + "risk_flags": [], + "signals": [ + "yogi_planet_in_wealth_house", + "yogi_planet_is_2l", + "lagna_yogi_tight_orb", + ], + "source": "yogi_asc_tight_orb_wealth", + "tight_orb_hits": ["lagna_yogi_tight_orb"], + "wealth_lord_links": ["yogi_planet_is_2l"], + "yogi_planet": "Venus", + "yogi_point_house": 1, + "yogi_point_longitude": 20.0, + "yogi_point_nakshatra": "Bharani", + }, } assert "yogi_active" in strict["event_judgement"]["secondary_context"] -def test_collect_strict_evidence_finance_does_not_promote_yogi_outside_kendra_trikona() -> None: +def test_collect_strict_evidence_finance_keeps_native_yogi_quiet_when_support_is_weak() -> None: result = { "modules": { "chart": { - "ascendant": {"sign": "Pisces", "lord": "Jupiter"}, + "ascendant": {"sign": "Taurus", "lord": "Venus", "degree_raw": 45.0}, "planets": { "Venus": { - "sign": "Capricorn", - "house": 11, - "status": "入友(Friendly Sign)", - } + "sign": "Cancer", + "house": 3, + "status": "中性", + }, + "Saturn": {"sign": "Sagittarius", "house": 8, "status": "中性"}, + "Sun": {"degree_raw": 20.0}, + "Moon": {"degree_raw": 20.0}, }, }, "varga_full": {"D2_Hora": {"summary": "ok"}, "D10_Dasamsa": {"summary": "ok"}}, @@ -365,11 +422,9 @@ def test_collect_strict_evidence_finance_does_not_promote_yogi_outside_kendra_tr } }, }, - "external_truth": {"yogi_planet": "Venus"}, } strict = _collect_strict_evidence("finance", result) - assert strict["present_evidence"]["yogi_promise"] is None assert strict["present_evidence"]["wealth_promise_strength"] == { "level": "moderate", "primary_source": "dhana_yogas", @@ -379,3 +434,132 @@ def test_collect_strict_evidence_finance_does_not_promote_yogi_outside_kendra_tr "yogi_support": None, } assert "yogi_active" not in strict["event_judgement"]["secondary_context"] + + +def test_collect_strict_evidence_finance_adds_external_avayogi_risk_penalty() -> None: + result = { + "modules": { + "chart": { + "ascendant": {"sign": "Pisces", "lord": "Jupiter"}, + "planets": { + "Saturn": { + "sign": "Aries", + "house": 11, + "status": "Debilitated", + } + }, + }, + "varga_full": {"D2_Hora": {"summary": "ok"}, "D10_Dasamsa": {"summary": "ok"}}, + "shadbala": {"planets": {"Venus": {"total_rupa": 8.2}}}, + "ashtakavarga": {"house_scores": {"2": 31, "11": 36}}, + "dasha": {"current_dasha": {"mahadasha": "Venus", "antardasha": "Mercury"}}, + "narayana_dasha": {"current_dasha": {"sign": "Taurus", "lord": "Venus"}}, + "dasa_convergence": { + "domain_activations": { + "wealth_family": {"convergence_level": "L1", "probability": "+15-20%"}, + "gains_wishes": {"convergence_level": "L1", "probability": "+15-20%"}, + "career_status": {"convergence_level": "L1", "probability": "+15-20%"}, + } + }, + "yogas_doshas": { + "dhana_yogas": { + "yogas": [{"type": "Dhana Yoga", "strength": "moderate"}], + "summary": "Dhana检测:共1个格局", + } + }, + }, + "external_truth": {"avayogi_planet": "Saturn"}, + } + + strict = _collect_strict_evidence("finance", result) + assert strict["present_evidence"]["avayogi_risk"] == { + "planet": "Saturn", + "house": 11, + "status": "Debilitated", + "source": "external_avayogi_planet", + "risk_level": "moderate", + "signals": ["avayogi_in_wealth_house"], + } + assert strict["event_judgement"]["score"] == 90 + assert "avayogi_active" in strict["event_judgement"]["secondary_context"] + + +def test_collect_strict_evidence_finance_keeps_external_avayogi_quiet_in_own_sign() -> None: + result = { + "modules": { + "chart": { + "ascendant": {"sign": "Pisces", "lord": "Jupiter"}, + "planets": { + "Saturn": { + "sign": "Capricorn", + "house": 11, + "status": "Own Sign", + } + }, + }, + "varga_full": {"D2_Hora": {"summary": "ok"}, "D10_Dasamsa": {"summary": "ok"}}, + "shadbala": {"planets": {"Venus": {"total_rupa": 8.2}}}, + "ashtakavarga": {"house_scores": {"2": 31, "11": 36}}, + "dasha": {"current_dasha": {"mahadasha": "Venus", "antardasha": "Mercury"}}, + "narayana_dasha": {"current_dasha": {"sign": "Taurus", "lord": "Venus"}}, + "dasa_convergence": { + "domain_activations": { + "wealth_family": {"convergence_level": "L1", "probability": "+15-20%"}, + "gains_wishes": {"convergence_level": "L1", "probability": "+15-20%"}, + "career_status": {"convergence_level": "L1", "probability": "+15-20%"}, + } + }, + "yogas_doshas": { + "dhana_yogas": { + "yogas": [{"type": "Dhana Yoga", "strength": "moderate"}], + "summary": "Dhana检测:共1个格局", + } + }, + }, + "external_truth": {"avayogi_planet": "Saturn"}, + } + + strict = _collect_strict_evidence("finance", result) + assert strict["present_evidence"]["avayogi_risk"] is None + assert strict["event_judgement"]["score"] == 95 + assert "avayogi_active" not in strict["event_judgement"]["secondary_context"] + + +def test_collect_strict_evidence_finance_does_not_add_avayogi_without_external_truth() -> None: + result = { + "modules": { + "chart": { + "ascendant": {"sign": "Pisces", "lord": "Jupiter"}, + "planets": { + "Saturn": { + "sign": "Aries", + "house": 11, + "status": "Debilitated", + } + }, + }, + "varga_full": {"D2_Hora": {"summary": "ok"}, "D10_Dasamsa": {"summary": "ok"}}, + "shadbala": {"planets": {"Venus": {"total_rupa": 8.2}}}, + "ashtakavarga": {"house_scores": {"2": 31, "11": 36}}, + "dasha": {"current_dasha": {"mahadasha": "Venus", "antardasha": "Mercury"}}, + "narayana_dasha": {"current_dasha": {"sign": "Taurus", "lord": "Venus"}}, + "dasa_convergence": { + "domain_activations": { + "wealth_family": {"convergence_level": "L1", "probability": "+15-20%"}, + "gains_wishes": {"convergence_level": "L1", "probability": "+15-20%"}, + "career_status": {"convergence_level": "L1", "probability": "+15-20%"}, + } + }, + "yogas_doshas": { + "dhana_yogas": { + "yogas": [{"type": "Dhana Yoga", "strength": "moderate"}], + "summary": "Dhana检测:共1个格局", + } + }, + }, + } + + strict = _collect_strict_evidence("finance", result) + assert strict["present_evidence"].get("avayogi_risk") is None + assert strict["event_judgement"]["score"] == 95 + assert "avayogi_active" not in strict["event_judgement"]["secondary_context"]