bab0718700
Web export now calls the same full pack as the long skill report and caches an owner-only Markdown download. Appendix failure stays unavailable and does not change the main report status. Co-authored-by: Cursor <cursoragent@cursor.com>
254 lines
9.8 KiB
Python
254 lines
9.8 KiB
Python
"""Call-surface and assembly locks for full-mode longform export."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from types import SimpleNamespace
|
|
|
|
from ayanamsa_utils import ACTIVE_AYANAMSA_NAME, apply_ayanamsa
|
|
|
|
from scripts.jyotish_engine import (
|
|
_attach_report_governance_contracts,
|
|
_build_natal_foundation_modules,
|
|
_compute_chart_from_args,
|
|
_native_dasha_master_families,
|
|
_render_finished_reading_navigation,
|
|
cmd_kp,
|
|
render_pl9_markdown,
|
|
)
|
|
from scripts.professional_report_reference import _export_args
|
|
|
|
|
|
BEIJING = {
|
|
"year": 1990,
|
|
"month": 1,
|
|
"day": 1,
|
|
"hour": 12,
|
|
"minute": 0,
|
|
"second": 0,
|
|
"lat": 39.9042,
|
|
"lon": 116.4074,
|
|
"tz": 8,
|
|
"ayanamsa": "raman",
|
|
}
|
|
|
|
|
|
def test_export_args_default_today_target_year_and_age() -> None:
|
|
args = _export_args({**BEIJING, "today": "2026-09-05"})
|
|
assert args.today == "2026-09-05"
|
|
assert args.target_year == 2026
|
|
assert args.age == 36
|
|
assert args.hour == 12
|
|
assert args.minute == 0
|
|
|
|
|
|
def test_export_args_keeps_explicit_age_and_does_not_duplicate_kwargs() -> None:
|
|
args = _export_args({**BEIJING, "today": "2026-09-05", "target_year": 2027, "age": 40})
|
|
assert args.target_year == 2027
|
|
assert args.age == 40
|
|
|
|
|
|
def test_reading_navigation_emits_required_headings() -> None:
|
|
markdown = "\n".join(_render_finished_reading_navigation({
|
|
"report_quality_gate": {"status": "review_required", "checks": []},
|
|
"worksheets": {
|
|
"timing_and_predictive_systems": {
|
|
"annual_tajika_pack": {"status": "partial_verified"},
|
|
"annual_tajika_series": {"years": {"2026": {}, "2027": {}, "2028": {}}},
|
|
},
|
|
"advanced_systems": {"kp_monthly_report": {"status": "parameter_sensitive", "months": [{}] * 36}},
|
|
"strengths_and_scores": {"functional_benefic_malefic": {"status": "partial_verified"}},
|
|
"divisional_and_special_charts": {"upagrahas": {"Gulika": {}}},
|
|
},
|
|
"birth_time_sensitivity": {"status": "not_rectified"},
|
|
}))
|
|
for heading in (
|
|
"## 成品阅读导航",
|
|
"### 先读什么",
|
|
"### 结论等级规则",
|
|
"### 专题判读协议",
|
|
"### 质量验收矩阵",
|
|
"### 对照覆盖表",
|
|
):
|
|
assert heading in markdown
|
|
assert "| 三年年度展开 | present |" in markdown
|
|
assert "| KP 三年流月支持 | present |" in markdown
|
|
|
|
|
|
def test_markdown_without_candidate_window_says_not_rectified() -> None:
|
|
packet = {
|
|
"schema": "pl9_style_professional_export_v1",
|
|
"birth_info": BEIJING,
|
|
"worksheets": {},
|
|
"raw_full_reading": {"modules": {}},
|
|
"personal_report_producer": {"main_body": {"status": "partial_verified"}},
|
|
"reader_engine_boundary_notice": {
|
|
"primary_text_zh": "多引擎口径说明",
|
|
"display_rule": "keep_blocked_labels",
|
|
},
|
|
}
|
|
packet = _attach_report_governance_contracts(packet, SimpleNamespace(
|
|
**BEIJING,
|
|
birth_time_accuracy="confirmed",
|
|
))
|
|
markdown = render_pl9_markdown(packet)
|
|
assert "## 成品阅读导航" in markdown
|
|
assert "### 出生时间敏感度" in markdown
|
|
assert "未做校时" in markdown
|
|
assert packet["birth_time_sensitivity"]["status"] == "not_rectified"
|
|
|
|
|
|
def test_candidate_range_keeps_minute_matrix_status() -> None:
|
|
args = SimpleNamespace(
|
|
**BEIJING,
|
|
birth_time_accuracy="provisional",
|
|
candidate_range={"start_time": "11:50", "end_time": "12:10", "representative_time": "12:00"},
|
|
)
|
|
packet = _attach_report_governance_contracts(
|
|
{"worksheets": {}, "raw_full_reading": {"modules": {}}},
|
|
args,
|
|
)
|
|
assert packet["birth_time_sensitivity"]["status"] == "candidate_window_only"
|
|
assert packet["birth_time_sensitivity"]["window"]["candidate_count"] >= 2
|
|
|
|
|
|
def _producer_packet(worksheets: dict) -> dict:
|
|
return {
|
|
"schema": "pl9_style_professional_export_v1",
|
|
"birth_info": BEIJING,
|
|
"worksheets": worksheets,
|
|
"full_report_pack": {"sections": {}},
|
|
"personal_report_producer": {"main_body": {"status": "partial_verified"}},
|
|
"reader_engine_boundary_notice": {
|
|
"primary_text_zh": "多引擎口径说明",
|
|
"display_rule": "keep_blocked_labels",
|
|
},
|
|
}
|
|
|
|
|
|
def test_native_dasha_families_mark_present_modules_executed() -> None:
|
|
families = _native_dasha_master_families({
|
|
"dasha": {"current_dasha": {"lord": "Saturn"}},
|
|
"narayana_dasha": {"status": "computed"},
|
|
"yogini_dasha": {"error": "missing"},
|
|
"ashtottari_dasha": {},
|
|
"kalachakra_dasha": {"status": "blocked", "reason": "not_applicable"},
|
|
})
|
|
assert families["vimshottari"]["execution_status"] == "executed"
|
|
assert families["narayana"]["execution_status"] == "executed"
|
|
assert families["yogini"]["execution_status"] == "blocked"
|
|
assert families["ashtottari"]["execution_status"] == "blocked"
|
|
assert families["kala_chakra"]["execution_status"] == "blocked"
|
|
|
|
|
|
def test_timing_mainline_renders_from_native_dasha_when_master_pack_has_no_families() -> None:
|
|
markdown = render_pl9_markdown(_producer_packet({
|
|
"timing_and_predictive_systems": {
|
|
"dasha": {"current_dasha": {"lord": "Saturn"}},
|
|
"narayana_dasha": {"status": "computed"},
|
|
"dasha_master_pack": {"schema": "dasha_master_report_pack_v1", "status": "blocked"},
|
|
"annual_tajika_pack": {"year_lord": {"status": "partial_verified"}},
|
|
},
|
|
}))
|
|
assert "### 大运与时间主线" in markdown
|
|
assert "#### 时间系统证据状态" in markdown
|
|
assert "| Vimshottari |" in markdown
|
|
assert "| Narayana |" in markdown
|
|
assert "| Year Lord |" in markdown
|
|
|
|
|
|
def test_kp_upagraha_and_yogakaraka_headings_render_when_worksheets_present() -> None:
|
|
markdown = render_pl9_markdown(_producer_packet({
|
|
"divisional_and_special_charts": {
|
|
"upagrahas": {
|
|
"raw": {
|
|
"Gulika": {"sign_idx": 0, "degree_in_sign": 12.5, "source": "local_formula"},
|
|
"Maandi": {"sign_idx": 1, "degree_in_sign": 3.0, "source": "local_formula"},
|
|
},
|
|
},
|
|
},
|
|
"advanced_systems": {
|
|
"kp": {
|
|
"house_basis": "explicit_cusps",
|
|
"planets": {
|
|
"Sun": {
|
|
"kp_lords": {
|
|
"sign": "Capricorn",
|
|
"nakshatra": "Uttara Ashadha",
|
|
"nakshatra_lord": "Sun",
|
|
"sub_lord": "Sun",
|
|
"sub_sub_lord": "Venus",
|
|
},
|
|
"significators": {"A": ["10"], "B": ["10"], "C": [], "D": ["10"]},
|
|
},
|
|
},
|
|
"houses": {
|
|
10: {
|
|
"sign": "Capricorn",
|
|
"cusp_longitude": 280.1,
|
|
"kp_lords": {
|
|
"sign": "Capricorn",
|
|
"nakshatra": "Uttara Ashadha",
|
|
"nakshatra_lord": "Sun",
|
|
"sub_lord": "Moon",
|
|
"sub_sub_lord": "Mars",
|
|
},
|
|
"significators": {"A": ["Sun"], "B": ["Mercury"], "C": [], "D": []},
|
|
},
|
|
2: {"significators": {"A": ["Venus"], "B": ["Jupiter"], "C": [], "D": []}},
|
|
6: {"significators": {"A": ["Saturn"], "B": ["Mars"], "C": [], "D": []}},
|
|
11: {"significators": {"A": ["Moon"], "B": ["Venus"], "C": [], "D": []}},
|
|
},
|
|
"ruling_planets": {
|
|
"day_lord": "Monday",
|
|
"moon_star_lord": "Saturn",
|
|
"ascendant_star_lord": "Sun",
|
|
},
|
|
},
|
|
},
|
|
"strengths_and_scores": {
|
|
"functional_benefic_malefic": {
|
|
"status": "used",
|
|
"ascendant": "Aries",
|
|
"functional_benefics": ["Jupiter", "Sun", "Mars"],
|
|
"functional_malefics": ["Mercury", "Saturn"],
|
|
"yogakarakas": ["Mars"],
|
|
},
|
|
"ashtakavarga": {"method": "local", "version": "test"},
|
|
},
|
|
}))
|
|
assert "### Upagraha / Sub-Planets" in markdown
|
|
assert "### KP Lord / Sub 原始表" in markdown
|
|
assert "#### KP Ruling Planets" in markdown or "#### 行星 KP Lord / Sub" in markdown
|
|
assert "#### KP 显著星 ABCD" in markdown
|
|
assert "#### KP 事业宫位核对" in markdown
|
|
assert "#### 功能性吉凶与 Yogakaraka" in markdown
|
|
assert "Mars" in markdown or "火星" in markdown
|
|
|
|
|
|
def test_cmd_kp_restores_report_ayanamsa() -> None:
|
|
apply_ayanamsa("raman")
|
|
args = SimpleNamespace(**BEIJING)
|
|
result = cmd_kp(args)
|
|
assert isinstance(result, dict)
|
|
assert isinstance(result.get("planets"), dict)
|
|
assert isinstance(result.get("ruling_planets"), dict)
|
|
assert ACTIVE_AYANAMSA_NAME == "raman"
|
|
|
|
|
|
def test_natal_foundation_returns_upagrahas_and_functional_layer() -> None:
|
|
apply_ayanamsa("raman")
|
|
args = SimpleNamespace(**BEIJING)
|
|
chart, _asc_idx, jd, _ayanamsa = _compute_chart_from_args(args)
|
|
planets = chart["planets"]
|
|
planet_lons = {
|
|
name: data.get("degree_raw", data["degree"])
|
|
for name, data in planets.items()
|
|
if isinstance(data, dict) and "degree" in data
|
|
}
|
|
asc_lon = chart["ascendant"].get("lon", chart["ascendant"].get("degree"))
|
|
foundation = _build_natal_foundation_modules(args, planet_lons, asc_lon=asc_lon, jd=jd)
|
|
assert foundation["upagrahas"].get("raw")
|
|
assert foundation["functional_benefic_malefic"].get("functional_benefics") or foundation["functional_benefic_malefic"].get("status")
|
|
assert foundation["functional_benefic_malefic"].get("yogakarakas") is not None
|
|
assert ACTIVE_AYANAMSA_NAME == "raman"
|