Files
Jyotisha/tests/test_report_reader_main.py
T
Jesse_ChenandClaude Opus 5.5 0afcc2d9a3 feat(tajika): year lord by the Panchadhikari rule, ported from upstream c7117f92 (BUG-1214)
Round-4 astrologer ruling Q3. tajika.calc_panchadhikari_year_lord and its
missing pieces (Tri-Rasi tables, planet index maps, ayanamsa-aware PyJHora
lookups) plus references/tajika_panchavargiya_rules.json (blob e8c15ac3) are
ported verbatim from yinduzhanxing c7117f92, with one keyword added
(muntha_sign_idx): upstream builds the Muntha-lord candidate from the annual
Lagna + years; ours stays natal Lagna + completed years (BUG-1212).

Both annual producers (solar_return_full_report, cmd_tajika) call the new
tajika_year_lord.select_annual_year_lord, so the pack conflict gate compares
the same rule. Output carries candidates / candidate_list with Panchavargiya
score and engine, aspect flags, selection_basis and a one-sentence basis
(zh/en). The product path always uses the native proxy (PyJHora is AGPL, absent
from the API image and CI); JYOTISH_YEAR_LORD_PYJHORA_REFERENCE=1 restores the
upstream PyJHora lookup for research. Card varshesha_basis and the report
'Selection basis' rows show the sentence; the BUG-1212 stand-in label is gone.
Mudda Dasha and Tri-Pataka follow the selected year lord.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N4f2nya58RoRu4yEmJgRGE
2026-10-03 16:30:28 +08:00

331 lines
14 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Reader-main export and the KP / Muntha defects behind BUG-1026/1027/1028."""
from __future__ import annotations
import copy
import re
import pytest
from scripts.annual_tajika_pack import _field_brief, _field_with_conflict_gate
from scripts.jyotish_engine import (
_copy_call_namespace,
_professional_report_reference_boundary_notice,
cmd_full_reading,
render_pl9_markdown,
)
from scripts.pl9_reader_export import _pl9_export_markdown_for_edition
from scripts.professional_report_reference import (
REPORT_VERSION_READER,
build_professional_report_reference,
)
from scripts.shared_full_report_authority import DEFAULT_REPORT_VERSION
from scripts.solar_return import calc_solar_return_chart, solar_return_full_report
# Clearly fictional. Not a user, not a published celebrity case.
FICTIONAL = {
"name": "虚构甲",
"year": 1991,
"month": 4,
"day": 7,
"hour": 9,
"minute": 15,
"second": 0,
"lat": 30.5728,
"lon": 104.0668,
"tz": 8.0,
"node_mode": "mean",
"ayanamsa": "lahiri",
"today": "2026-04-07",
"transit_date": "2026-04-07",
"target_year": 2026,
"age": 35,
"birth_time_accuracy": "confirmed",
}
LEAK_PATTERNS = (
re.compile(r"\bblocked\b", re.I),
re.compile(r"\bexecuted\b", re.I),
re.compile(r"\bpartial_verified\b", re.I),
re.compile(r"\bmissing_in_local\b", re.I),
re.compile(r"\bproducer\b", re.I),
re.compile(r"\bparity\b", re.I),
re.compile(r"PL9\s*第\s*\d+\s*页"),
re.compile(r"full-report://"),
re.compile(r"\bpl9_[A-Za-z0-9_]+\b", re.I),
re.compile(r"\b[A-Za-z][A-Za-z0-9]*_unavailable\b"),
re.compile(r"\b[A-Za-z][A-Za-z0-9]*_(?:pack|packet)\b", re.I),
re.compile(r"object has no attribute"),
re.compile(r"\bTraceback\b"),
re.compile(r"结论等级规则"),
re.compile(r"This reference export"),
re.compile(r"producer failed"),
re.compile(r"PyJHora"),
re.compile(r"\bparameter_sensitive\b"),
re.compile(r"\bschema\b", re.I),
re.compile(r"\bsource_path\b"),
re.compile(r"\{planets\}"),
)
CORE_CHAPTERS = (
"出生资料",
"Ashtakavarga",
"标准大运",
"KP",
"Year Lord",
"Tajika",
"Muntha",
)
def _class_args(**overrides):
fields = {**FICTIONAL, **overrides}
return type("Args", (), fields)()
def _leak_hits(markdown: str) -> list[str]:
return [pattern.pattern for pattern in LEAK_PATTERNS if pattern.search(markdown)]
@pytest.fixture(scope="module")
def full_reading():
return cmd_full_reading(_class_args())
def test_default_edition_stays_reference_and_reader_requests_v3() -> None:
assert DEFAULT_REPORT_VERSION == "pl9_personal_long_report.v3"
assert REPORT_VERSION_READER == "pl9_personal_long_report.v3"
class Handler:
def _high_rigor_birth_payload(self, body):
return dict(FICTIONAL)
def _compute_full_reading_for_thematic(self, body):
return {"modules": {"chart": {}}}
class Engine:
def build_professional_report_reference_packet(self, full_reading, args, packs):
return {"schema": "pl9_style_professional_export_v1", "selected_report_pack_ids": packs}
def render_pl9_markdown(self, packet):
return "reference-body"
handler = Handler()
engine = Engine()
default = build_professional_report_reference(handler, {"format": "markdown"}, engine=engine)
assert default["edition"] == "reference"
assert default["markdown"] == "reference-body"
reader = build_professional_report_reference(
handler,
{"format": "markdown", "edition": "reader_main"},
engine=engine,
)
assert reader["edition"] == "reader_main"
assert reader["report_version"] == "pl9_personal_long_report.v3"
def test_reference_body_drops_local_leak_phrases() -> None:
packet = {
"schema": "pl9_style_professional_export_v1",
"birth_info": {"name": "虚构甲"},
"reader_engine_boundary_notice": _professional_report_reference_boundary_notice(),
"worksheets": {},
}
markdown = render_pl9_markdown(packet)
assert "结论等级规则" not in markdown
assert "This reference export" not in markdown
assert "producer failed" not in markdown
assert "多引擎口径说明" in markdown
def test_class_args_copy_keeps_year_for_kp_and_transit(full_reading) -> None:
args = _class_args()
copied = _copy_call_namespace(args)
assert copied.year == 1991
assert vars(args) == {}
modules = full_reading["modules"]
for name in ("kp", "double_transit_pac", "transit_ll7l", "planetary_congregation"):
module = modules.get(name)
assert isinstance(module, dict), name
blob = repr(module)
assert "has no attribute" not in blob
assert module.get("status") != "blocked", (name, module.get("status"), module.get("reason"))
assert "error" not in module or not str(module.get("error", "")).startswith("KP producer failed")
def test_reader_main_hides_audit_vocabulary(full_reading) -> None:
from scripts.jyotish_engine import build_professional_report_reference_packet
packet = build_professional_report_reference_packet(full_reading, _class_args(), ["full"])
reference = _pl9_export_markdown_for_edition(packet, "reference")
reader = _pl9_export_markdown_for_edition(packet, "reader_main")
assert "结论等级规则" not in reference
assert "This reference export" not in reference
hits = _leak_hits(reader)
assert hits == [], hits
for chapter in CORE_CHAPTERS:
assert chapter in reader, chapter
assert reader.strip().startswith("#")
def test_solar_return_restores_birth_asc_and_year_lord_moves() -> None:
lords = set()
signs = set()
for year in (2024, 2026, 2028):
chart = calc_solar_return_chart(
FICTIONAL["year"], FICTIONAL["month"], FICTIONAL["day"],
FICTIONAL["hour"], FICTIONAL["minute"],
FICTIONAL["lat"], FICTIONAL["lon"], FICTIONAL["tz"],
year,
ayanamsa_name="lahiri",
)
assert "error" not in chart, chart.get("error")
assert isinstance(chart.get("birth_asc_sign_idx"), int)
report = solar_return_full_report(
FICTIONAL["year"], FICTIONAL["month"], FICTIONAL["day"],
FICTIONAL["hour"], FICTIONAL["minute"],
FICTIONAL["lat"], FICTIONAL["lon"], FICTIONAL["tz"],
year,
ayanamsa_name="lahiri",
)
muntha = report["muntha"]
assert muntha.get("status") != "blocked", muntha
assert isinstance(muntha.get("muntha_sign_idx"), int)
year_lord = report["year_lord"]
assert year_lord.get("status") != "blocked", year_lord
lords.add(year_lord["year_lord"])
signs.add(muntha["muntha_sign"])
assert len(signs) >= 2
assert len(lords) >= 2
def test_blocked_side_does_not_invent_a_conflict() -> None:
gated = _field_with_conflict_gate(
"muntha",
{"muntha": {"status": "blocked", "reason": "natal_ascendant_required_for_muntha"}},
{"muntha": {"muntha_sign": "Capricorn", "muntha_lord": "Saturn", "note": "extra"}},
)
assert gated["status"] == "partial_verified"
assert "conflict" not in _field_brief("Muntha", gated)
same_sign = _field_with_conflict_gate(
"year_lord",
{"year_lord": {"year_lord": "Mars", "year_lord_sign": "Aries", "trace": "left"}},
{"year_lord": {"year_lord": "Mars", "year_lord_sign": "Aries", "trace": "right"}},
)
assert same_sign["status"] == "partial_verified"
disagree = _field_with_conflict_gate(
"year_lord",
{"year_lord": {"year_lord": "Mars", "year_lord_sign": "Aries"}},
{"year_lord": {"year_lord": "Venus", "year_lord_sign": "Taurus"}},
)
assert disagree["status"] == "conflict"
@pytest.fixture(scope="module")
def reader_packet(full_reading):
from scripts.jyotish_engine import build_professional_report_reference_packet
return build_professional_report_reference_packet(full_reading, _class_args(), ["full"])
def test_real_annual_producers_agree_without_identical_metadata(reader_packet) -> None:
years = reader_packet["worksheets"]["timing_and_predictive_systems"]["annual_tajika_series"]["years"]
for annual in years.values():
field = annual["year_lord"]
assert field["status"] == "partial_verified"
left, right = [item["value"] for item in field["producers_checked"]]
# 原值:左(太阳返照)有 year_lord_sign、右(Tajika)没有——元数据不同仍判一致
# 新值:两个生产者共用 tajika_year_lord,键相同;断言选出的年主、候选与依据一致
# 原因:BUG-1214 两个生产者改为同一 Panchadhikari 选法(任务书 T2)
assert left["year_lord"] == right["year_lord"]
assert left["candidates"] == right["candidates"]
assert left["selection_basis"] == right["selection_basis"]
assert field["data"] == left
sections = annual["report_sections"]
assert "冲突" not in repr(sections["executive_summary"]["summary_lines"])
assert "分歧" not in repr(sections["thematic_narrative"]["highlights"])
assert "外部验证已经完成" in repr(sections["thematic_narrative"]["highlights"])
def test_reader_annual_cells_use_real_distinct_years_and_keep_return_dates(reader_packet) -> None:
reader = _pl9_export_markdown_for_edition(reader_packet, "reader_main")
lords = re.findall(r"^\| Varshesha \| ([^|]+) \|$", reader, re.M)
signs = re.findall(r"^\| Muntha sign \| ([^|]+) \|$", reader, re.M)
assert len(lords) == len(signs) == 3
assert "-" not in lords + signs
assert len(set(lords)) >= 2
assert len(set(signs)) >= 2
# 原值:「| Selection basis | Muntha 所在星座的守护星(当前本地口径) |」(开工基线即已失败:
# BUG-1212 把该文案改成了「暂用 Muntha 主星…」,本条未跟改)
# 新值:每个年度都有 Panchadhikari 选法依据一句话
# 原因:BUG-1214(任务书 T2)
bases = re.findall(r"^\| Selection basis \| ([^|]+) \|$", reader, re.M)
assert len(bases) == len(lords) and all("年盘上升" in basis for basis in bases)
assert "| Annual ascendant sign | - |" not in reader
timing = reader_packet["worksheets"]["timing_and_predictive_systems"]
for year in ("2027", "2028"):
annual = timing["annual_tajika_series"]["years"][year]
return_time = annual["solar_return"]["data"]["dt_local"]
section = reader.split(f"### {year} 年度 Varshaphala / Tajika 细项", 1)[1].split("### ", 1)[0]
assert f"| Varshapravesha | {return_time} |" in section
assert _leak_hits(reader) == []
def test_reader_annual_conflict_keeps_both_real_producer_values(reader_packet) -> None:
packet = copy.deepcopy(reader_packet)
timing = packet["worksheets"]["timing_and_predictive_systems"]
annual = timing["annual_tajika_pack"]
current = annual["year_lord"]["data"]
later = timing["annual_tajika_series"]["years"]["2027"]["year_lord"]["data"]
annual["year_lord"] = _field_with_conflict_gate(
"year_lord", {"year_lord": current}, {"year_lord": later},
)
assert annual["year_lord"]["status"] == "conflict"
reader = _pl9_export_markdown_for_edition(packet, "reader_main")
# 原值:「太阳返照:火星;Tajika:金星(两者不同)」(当年 / 次年的 Muntha 主星)
# 新值:「太阳返照:火星;Tajika:月亮(两者不同)」(当年 / 次年按 Panchadhikari 选出的年主)
# 原因:BUG-1214 年主改为 Panchadhikari 选法(任务书 T2);本用例只测冲突两值都保留
assert (current["year_lord"], later["year_lord"]) == ("Mars", "Moon")
assert "| Varshesha | 太阳返照:火星;Tajika:月亮(两者不同) |" in reader
assert "两种计算口径给出的年主不同,暂不合并" in reader
assert "| Muntha sign | 白羊座 |" in reader
assert _leak_hits(reader) == []
def test_reader_omits_missing_name_and_panchanga_machine_keys(reader_packet) -> None:
packet = copy.deepcopy(reader_packet)
packet["birth_info"].pop("name", None)
packet["birth_info"].pop("user_name", None)
reader = _pl9_export_markdown_for_edition(packet, "reader_main")
assert "- 姓名:" not in reader
assert "overall_score" not in reader
assert "吉性_count" not in reader
assert "total_elements" not in reader
assert "| 综合性质 |" in reader
assert "| 吉性要素数量 |" in reader
assert "| 要素总数 |" in reader
def test_reader_headings_drop_field_jargon_and_dignity_is_one_name(reader_packet) -> None:
"""BUG-1092 / BUG-1093: no 「字段」 in what a reader sees, readable planet-lord
headers, and the bilingual engine dignity label printed once, not 「入旺(入旺)」."""
from scripts.pl9_reader_export import _dignity_label
reader = _pl9_export_markdown_for_edition(reader_packet, "reader_main")
assert "字段" not in reader
assert "#### 行星主星与状态" in reader
assert "| 行星 | 星座主 | 星宿主 | 分主 | 分分主 | 尊贵状态 | 力量比 |" in reader
assert "| Planet | RL | NL | SL | SS |" not in reader
assert "力量比:六维力量与所需力量之比" in reader
table = reader.split("#### 行星主星与状态", 1)[1].split("\n\n", 2)[1]
for row in table.splitlines()[2:]:
dignity = row.split("|")[6].strip()
assert "(" not in dignity and "(" not in dignity, row
assert not re.search(r"([一-鿿]+)\(\1\)", reader)
assert _dignity_label("入旺(Exalted)") == "入旺"
assert _dignity_label("落陷取消(Neecha Bhanga)") == "落陷取消"
assert _dignity_label("极敌(Great Enemy)") == "极敌"
assert _dignity_label("中性") == "中性"
assert _dignity_label(None) == ""
assert _leak_hits(reader) == []