fix: feed current dasha into web marriage path and keep chart fences on failure (BUG-609, BUG-610, BUG-611)
Consultation was passing the birth mahadasha into event-class splits, so marriage hits stayed empty and timing still said 缔结. Read live MD/AD from dasha_sub_periods, derive activation copy from the split, and isolate fence failures so one bad varga cannot 500 the whole report. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,229 @@
|
||||
{
|
||||
"note": "Fictional 1990-06-15 10:30 Beijing; Leo lagna; 5L=Jupiter, 7L=Saturn, Moon in 7H. Current MD Saturn / AD Jupiter. Trimmed from consultation-shaped chart modules.",
|
||||
"birth": {
|
||||
"year": 1990,
|
||||
"month": 6,
|
||||
"day": 15,
|
||||
"hour": 10,
|
||||
"minute": 30,
|
||||
"lat": 39.9042,
|
||||
"lon": 116.4074,
|
||||
"tz": 8,
|
||||
"today": "2026-09-09"
|
||||
},
|
||||
"chart": {
|
||||
"ascendant": {
|
||||
"sign": "Leo",
|
||||
"sign_idx": 4,
|
||||
"lon": 131.4482,
|
||||
"degree": 11.45,
|
||||
"degree_in_sign": 11.45
|
||||
},
|
||||
"planets": {
|
||||
"Jupiter": {
|
||||
"sign": "Gemini",
|
||||
"house": 11,
|
||||
"lon": 83.52630829337346,
|
||||
"degree": 23.5263,
|
||||
"degree_in_sign": 23.5263,
|
||||
"sign_idx": 2,
|
||||
"longitude": 83.52630829337346
|
||||
},
|
||||
"Ketu": {
|
||||
"sign": "Cancer",
|
||||
"house": 12,
|
||||
"lon": 107.4414312800169,
|
||||
"degree": 17.4414,
|
||||
"degree_in_sign": 17.4414,
|
||||
"sign_idx": 3,
|
||||
"longitude": 107.4414312800169
|
||||
},
|
||||
"Mars": {
|
||||
"sign": "Pisces",
|
||||
"house": 8,
|
||||
"lon": 348.4792230303051,
|
||||
"degree": 18.4792,
|
||||
"degree_in_sign": 18.4792,
|
||||
"sign_idx": 11,
|
||||
"longitude": 348.4792230303051
|
||||
},
|
||||
"Mercury": {
|
||||
"sign": "Taurus",
|
||||
"house": 10,
|
||||
"lon": 42.73765469811987,
|
||||
"degree": 12.7377,
|
||||
"degree_in_sign": 12.7377,
|
||||
"sign_idx": 1,
|
||||
"longitude": 42.73765469811987
|
||||
},
|
||||
"Moon": {
|
||||
"sign": "Aquarius",
|
||||
"house": 7,
|
||||
"lon": 317.8190269192091,
|
||||
"degree": 17.819,
|
||||
"degree_in_sign": 17.819,
|
||||
"sign_idx": 10,
|
||||
"longitude": 317.8190269192091
|
||||
},
|
||||
"Rahu": {
|
||||
"sign": "Capricorn",
|
||||
"house": 6,
|
||||
"lon": 287.4414312800169,
|
||||
"degree": 17.4414,
|
||||
"degree_in_sign": 17.4414,
|
||||
"sign_idx": 9,
|
||||
"longitude": 287.4414312800169
|
||||
},
|
||||
"Saturn": {
|
||||
"sign": "Capricorn",
|
||||
"house": 6,
|
||||
"lon": 271.7776735771559,
|
||||
"degree": 1.7777,
|
||||
"degree_in_sign": 1.7777,
|
||||
"sign_idx": 9,
|
||||
"longitude": 271.7776735771559
|
||||
},
|
||||
"Sun": {
|
||||
"sign": "Gemini",
|
||||
"house": 11,
|
||||
"lon": 61.47407825034283,
|
||||
"degree": 1.4741,
|
||||
"degree_in_sign": 1.4741,
|
||||
"sign_idx": 2,
|
||||
"longitude": 61.47407825034283
|
||||
},
|
||||
"Venus": {
|
||||
"sign": "Aries",
|
||||
"house": 9,
|
||||
"lon": 26.034770273615777,
|
||||
"degree": 26.0348,
|
||||
"degree_in_sign": 26.0348,
|
||||
"sign_idx": 0,
|
||||
"longitude": 26.034770273615777
|
||||
}
|
||||
},
|
||||
"modules": {
|
||||
"dasha": {
|
||||
"current_md": "Rahu",
|
||||
"start_date": "1975-05-25",
|
||||
"remaining_years": 2.94,
|
||||
"total_years": 18,
|
||||
"birth_balance": {
|
||||
"elapsed_years": 15.06,
|
||||
"lord": "Rahu",
|
||||
"remaining_years": 2.94
|
||||
},
|
||||
"periods": [
|
||||
{
|
||||
"lord": "Rahu",
|
||||
"start": "1975-05-25",
|
||||
"end": "1993-05-25"
|
||||
},
|
||||
{
|
||||
"lord": "Jupiter",
|
||||
"start": "1993-05-25",
|
||||
"end": "2009-05-25"
|
||||
},
|
||||
{
|
||||
"lord": "Saturn",
|
||||
"start": "2009-05-25",
|
||||
"end": "2028-05-25"
|
||||
},
|
||||
{
|
||||
"lord": "Mercury",
|
||||
"start": "2028-05-25",
|
||||
"end": "2045-05-25"
|
||||
},
|
||||
{
|
||||
"lord": "Ketu",
|
||||
"start": "2045-05-25",
|
||||
"end": "2052-05-25"
|
||||
},
|
||||
{
|
||||
"lord": "Venus",
|
||||
"start": "2052-05-25",
|
||||
"end": "2072-05-25"
|
||||
},
|
||||
{
|
||||
"lord": "Sun",
|
||||
"start": "2072-05-25",
|
||||
"end": "2078-05-25"
|
||||
},
|
||||
{
|
||||
"lord": "Moon",
|
||||
"start": "2078-05-25",
|
||||
"end": "2088-05-25"
|
||||
},
|
||||
{
|
||||
"lord": "Mars",
|
||||
"start": "2088-05-25",
|
||||
"end": "2095-05-25"
|
||||
}
|
||||
]
|
||||
},
|
||||
"dasha_sub_periods": {
|
||||
"status": "ready",
|
||||
"current": {
|
||||
"mahadasha": {
|
||||
"lord": "Saturn",
|
||||
"start": "2009-05-25",
|
||||
"end": "2028-05-25"
|
||||
},
|
||||
"antardasha": {
|
||||
"lord": "Jupiter",
|
||||
"start": "2025-11-11",
|
||||
"end": "2028-05-25"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"thematic_report": {
|
||||
"themes": {
|
||||
"marriage": {
|
||||
"timing": {
|
||||
"dasha_period": "Saturn-Jupiter",
|
||||
"start_year": 2009,
|
||||
"end_year": 2028,
|
||||
"activation_description": "感情被激活的观察期,不等于领证",
|
||||
"is_current": true
|
||||
},
|
||||
"evidence": [
|
||||
{
|
||||
"technique": "D1-7th-house",
|
||||
"details": {}
|
||||
},
|
||||
{
|
||||
"technique": "Spouse-status-yoga",
|
||||
"details": {}
|
||||
},
|
||||
{
|
||||
"technique": "DK-UL-Dasha timing",
|
||||
"details": {}
|
||||
},
|
||||
{
|
||||
"technique": "Romantic-activation",
|
||||
"details": {
|
||||
"hits": [
|
||||
"5L_dasha"
|
||||
],
|
||||
"natal_structure": [
|
||||
"7H_Moon"
|
||||
],
|
||||
"event_class": "romantic_activation"
|
||||
}
|
||||
},
|
||||
{
|
||||
"technique": "Relationship-formation",
|
||||
"details": {
|
||||
"hits": [
|
||||
"7L_dasha"
|
||||
],
|
||||
"event_class": "relationship_formation"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -214,6 +214,7 @@ def test_health_endpoint_exposes_runtime_accuracy_metadata() -> None:
|
||||
assert handler.status_code == 200
|
||||
payload = handler.payload()
|
||||
assert payload['status'] == 'ok'
|
||||
assert payload['version'] == jyotish_api_server.__version__
|
||||
assert payload['ayanamsa_default'] == 'raman'
|
||||
assert 'swisseph_available' in payload
|
||||
assert 'swisseph_version' in payload
|
||||
|
||||
@@ -2,12 +2,14 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT / "scripts"))
|
||||
|
||||
from jyotish_api_server import JyotishAPIHandler # noqa: E402
|
||||
from relationship_analysis import analyze_relationship # noqa: E402
|
||||
from relationship_event_class_evidence import build_event_class_evidence # noqa: E402
|
||||
|
||||
@@ -63,3 +65,36 @@ def test_pratyantar_venus_does_not_emit_legal_marriage_item() -> None:
|
||||
assert "Venus_dasha" not in split["legal_marriage"]["hits"]
|
||||
items = build_event_class_evidence(relationship, _theme_evidence)
|
||||
assert all(item["technique"] != "Legal-marriage" for item in items)
|
||||
|
||||
|
||||
def test_consultation_shaped_1990_chart_feeds_nonempty_event_class_hits() -> None:
|
||||
golden = json.loads(
|
||||
(ROOT / "tests/golden/upstream_sync2/consultation_marriage_1990_fictional.json").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
)
|
||||
chart = golden["chart"]
|
||||
handler = JyotishAPIHandler.__new__(JyotishAPIHandler)
|
||||
dasha = (chart.get("modules") or {}).get("dasha") or {}
|
||||
dasha_info = handler._thematic_dasha_info(chart, dasha)
|
||||
assert dasha_info["maha_dasha"] == "Saturn"
|
||||
assert dasha_info["antar_dasha"] == "Jupiter"
|
||||
timing = (((golden.get("thematic_report") or {}).get("themes") or {}).get("marriage") or {}).get("timing") or {}
|
||||
assert "Unknown" not in str(timing.get("dasha_period") or "")
|
||||
assert timing.get("dasha_period") == "Saturn-Jupiter"
|
||||
relationship = analyze_relationship(
|
||||
chart["planets"],
|
||||
asc_sign=chart["ascendant"]["sign"],
|
||||
dasha_info=dasha_info,
|
||||
)
|
||||
items = handler._derived_marriage_evidence(
|
||||
chart,
|
||||
{"relationship": relationship, "full_modules": chart.get("modules") or {}},
|
||||
)
|
||||
by_name = {item["technique"]: item for item in items}
|
||||
nonempty = [
|
||||
name
|
||||
for name in ("Romantic-activation", "Relationship-formation")
|
||||
if (by_name.get(name) or {}).get("details", {}).get("hits")
|
||||
]
|
||||
assert nonempty, {name: (by_name.get(name) or {}).get("details") for name in by_name}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import json
|
||||
from functools import lru_cache
|
||||
from types import SimpleNamespace
|
||||
@@ -195,3 +196,14 @@ def test_chart_block_suite_is_on_the_quick_quality_gate() -> None:
|
||||
from scripts.run_quality_gate import CORE_PYTEST_TARGETS
|
||||
|
||||
assert "tests/test_report_chart_block.py" in CORE_PYTEST_TARGETS
|
||||
|
||||
|
||||
def test_chinese_ascendant_sign_keeps_svg_and_drops_fence() -> None:
|
||||
packet = copy.deepcopy(_golden()["packet"])
|
||||
packet["core_chart"]["ascendant"]["sign"] = "白羊"
|
||||
markdown = render_pl9_markdown(packet)
|
||||
start = markdown.index("#### D1 — Rashi Chart(本命盘)")
|
||||
end = markdown.index("#### D9 — Navamsa(婚盘)")
|
||||
section = markdown[start:end]
|
||||
assert "<svg" in section
|
||||
assert "```jyotish-chart" not in section
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
"""Marriage timing copy follows event_class_split; missing AD is not spelled Unknown."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from scripts.report_orchestrator import BirthChartData, ThemeName, TimingAnchorBuilder
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
ORCHESTRATOR = ROOT / "scripts" / "report_orchestrator.py"
|
||||
|
||||
|
||||
def _builder(split: dict, *, antardasha: str = "Unknown") -> TimingAnchorBuilder:
|
||||
chart = BirthChartData(
|
||||
dasha_timeline=[
|
||||
{"mahadasha": "Saturn", "antardasha": antardasha, "start": 2025, "end": 2028},
|
||||
],
|
||||
event_class_split=split,
|
||||
)
|
||||
return TimingAnchorBuilder(chart)
|
||||
|
||||
|
||||
def test_romantic_only_says_not_legal_marriage() -> None:
|
||||
timing = _builder({"romantic_activation": {"hits": ["5L_dasha"]}}).build_for_theme(ThemeName.MARRIAGE)
|
||||
assert timing is not None
|
||||
assert "不等于领证" in timing.activation_description
|
||||
assert "缔结" not in timing.activation_description
|
||||
assert "关键期" not in timing.activation_description
|
||||
|
||||
|
||||
def test_legal_md_ad_hit_may_say_solemnize() -> None:
|
||||
timing = _builder(
|
||||
{"legal_marriage": {"hits": ["Venus_dasha"]}},
|
||||
antardasha="Venus",
|
||||
).build_for_theme(ThemeName.MARRIAGE)
|
||||
assert timing is not None
|
||||
assert "缔结" in timing.activation_description
|
||||
assert timing.dasha_period == "Saturn-Venus"
|
||||
|
||||
|
||||
def test_no_hits_omits_key_period_and_solemnize() -> None:
|
||||
timing = _builder({}).build_for_theme(ThemeName.MARRIAGE)
|
||||
assert timing is not None
|
||||
assert "关键期" not in timing.activation_description
|
||||
assert "缔结" not in timing.activation_description
|
||||
assert timing.activation_description == "本期无婚恋事件类命中"
|
||||
assert "Unknown" not in timing.dasha_period
|
||||
assert timing.dasha_period == "Saturn"
|
||||
|
||||
|
||||
def test_legal_phrase_only_lives_on_the_legal_branch() -> None:
|
||||
source = ORCHESTRATOR.read_text(encoding="utf-8")
|
||||
assert source.count("缔结或调整伴侣关系的关键期") == 1
|
||||
legal_fn = source[source.index("def _marriage_activation_text") :]
|
||||
legal_fn = legal_fn[: legal_fn.index("class ThematicReportOrchestrator")]
|
||||
assert "if legal_hits:" in legal_fn
|
||||
before_legal, after_legal = legal_fn.split("if legal_hits:", 1)
|
||||
assert "缔结或调整伴侣关系的关键期" not in before_legal
|
||||
assert "缔结或调整伴侣关系的关键期" in after_legal.split("return", 1)[1].split("\n", 1)[0]
|
||||
@@ -0,0 +1,77 @@
|
||||
"""Current MD/AD for thematic reports come from dasha_sub_periods, not birth MD."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from scripts.jyotish_api_server import JyotishAPIHandler
|
||||
|
||||
|
||||
def _handler() -> JyotishAPIHandler:
|
||||
return JyotishAPIHandler.__new__(JyotishAPIHandler)
|
||||
|
||||
|
||||
def test_prefers_dasha_sub_periods_current_over_birth_md() -> None:
|
||||
info = _handler()._thematic_dasha_info(
|
||||
{
|
||||
"dasha": {"current_md": "Rahu"},
|
||||
"modules": {
|
||||
"dasha_sub_periods": {
|
||||
"current": {
|
||||
"mahadasha": {"lord": "Saturn"},
|
||||
"antardasha": {"lord": "Jupiter"},
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
{"current_md": "Rahu", "periods": []},
|
||||
)
|
||||
assert info["maha_dasha"] == "Saturn"
|
||||
assert info["antar_dasha"] == "Jupiter"
|
||||
|
||||
|
||||
def test_falls_back_to_vimshottari_analysis_when_sub_periods_missing() -> None:
|
||||
info = _handler()._thematic_dasha_info(
|
||||
{"dasha": {"current_md": "Rahu"}},
|
||||
{
|
||||
"vimshottari_analysis": {
|
||||
"current": {
|
||||
"mahadasha": {"lord": "Venus"},
|
||||
"antardasha": {"lord": "Moon"},
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
assert info["maha_dasha"] == "Venus"
|
||||
assert info["antar_dasha"] == "Moon"
|
||||
|
||||
|
||||
def test_omits_maha_dasha_when_neither_source_has_current_lords() -> None:
|
||||
info = _handler()._thematic_dasha_info({"dasha": {"current_md": "Rahu"}}, {"periods": []})
|
||||
assert "maha_dasha" not in info
|
||||
assert "antar_dasha" not in info
|
||||
|
||||
|
||||
def test_timeline_fills_current_antardasha_from_sub_periods() -> None:
|
||||
timeline = _handler()._normalize_thematic_dasha_timeline(
|
||||
{
|
||||
"periods": [
|
||||
{"lord": "Saturn", "start": "2009-05-25", "end": "2028-05-25"},
|
||||
],
|
||||
"modules": {
|
||||
"dasha_sub_periods": {
|
||||
"current": {
|
||||
"mahadasha": {"lord": "Saturn"},
|
||||
"antardasha": {"lord": "Jupiter"},
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
)
|
||||
assert timeline[0]["mahadasha"] == "Saturn"
|
||||
assert timeline[0]["antardasha"] == "Jupiter"
|
||||
|
||||
|
||||
def test_source_tests_are_on_the_quick_quality_gate() -> None:
|
||||
from scripts.run_quality_gate import CORE_PYTEST_TARGETS
|
||||
|
||||
assert "tests/test_thematic_dasha_info_source.py" in CORE_PYTEST_TARGETS
|
||||
assert "tests/test_report_orchestrator_marriage_timing.py" in CORE_PYTEST_TARGETS
|
||||
Reference in New Issue
Block a user