fix(tajika): year lord blocked instead of Muntha-lord stand-in (BUG-1223)

Panchadhikari blocked -> status blocked + reason, year_lord None, sentence
「年主暂无法确定(原因)」 on card and report; Tri-Pataka blocked with it;
Mudda unchanged. Aligned with upstream 03bea6ed. Nine public charts: both
producers byte-identical before/after.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N4f2nya58RoRu4yEmJgRGE
This commit is contained in:
Jesse_Chen
2026-10-04 07:25:48 +08:00
co-authored by Claude Opus 5.5
parent 77645ce25f
commit 7dda1b45c3
8 changed files with 249 additions and 37 deletions
+149
View File
@@ -0,0 +1,149 @@
"""Year lord: no stand-in when Panchadhikari is blocked (BUG-1223).
Product owner 2026-10-04 (batch 5 T5), aligned with upstream yinduzhanxing
03bea6ed (`docs/research/tajika_shared_entrypoint_repair_2026_10_03.md`
item 3): when the year lord cannot be chosen (annual chart incomplete) the
producers return `status: "blocked"` with the reason and no planet; the
Muntha sign lord is no longer put in its place. The card and the report show
"年主暂无法确定(原因)". Mudda is independent of the year lord (BUG-1216).
Fictional chart.
"""
from __future__ import annotations
import copy
import re
import types
from datetime import datetime
import pytest
import tajika_year_lord as year_lord_module # the name the producers import
from scripts.pl9_reader_english_terms import PHRASES_EN
BLOCKED = {"status": "blocked", "reason": "sun_or_moon_missing_for_panchadhikari"}
FICTIONAL = {"birth": {"date": "1990-01-01", "time": "12:00:00", "latitude": 39.9, "longitude": 116.4, "utc_offset": "+08:00"},
"settings": {"ayanamsa": "raman"}}
@pytest.fixture
def blocked_selector(monkeypatch):
monkeypatch.delenv("JYOTISH_YEAR_LORD_PYJHORA_REFERENCE", raising=False)
monkeypatch.setattr(year_lord_module, "calc_panchadhikari_year_lord", lambda *a, **k: dict(BLOCKED))
def test_selector_returns_blocked_without_a_stand_in_planet(blocked_selector) -> None:
sr = {"asc_sign_idx": 0, "planet_lons": {}, "solar_return": {}, "annual_location": {}}
data = year_lord_module.select_annual_year_lord(
sr, natal_asc_sign_idx=4, completed_years=30, ayanamsa_name="raman",
birth_lat=39.9, birth_lon=116.4, birth_tz=8.0)
assert data["status"] == "blocked"
assert data["reason"] == "sun_or_moon_missing_for_panchadhikari"
assert data["year_lord"] is None
assert "year_lord_sign" not in data and "fallback_reason" not in data
assert data["selection_basis"] == year_lord_module.BLOCKED_BASIS
assert data["selection_basis_text"] == "年主暂无法确定(年盘缺太阳或月亮位置)"
assert data["selection_basis_text_en"].startswith("Year lord cannot be determined yet (")
assert "Muntha" not in data["selection_basis_text"]
def test_solar_return_producer_blocks_the_year_lord_and_tri_pataka_keeps_mudda(blocked_selector) -> None:
from scripts.solar_return import solar_return_full_report
report = solar_return_full_report(1990, 1, 1, 12, 0, 39.9, 116.4, 8.0, 2026, ayanamsa_name="raman")
assert report["year_lord"]["status"] == "blocked"
assert report["year_lord"]["year_lord"] is None
assert report["tri_pataka"] == {"status": "blocked", "reason": "year_lord_blocked"}
assert report["mudda_dasha"]["status"] == "partial_verified"
assert report["mudda_dasha"]["independent_of_year_lord"] is True
def test_cmd_tajika_without_an_annual_chart_is_blocked_not_muntha_lord(monkeypatch) -> None:
import solar_return as solar_return_module
from scripts.jyotish_engine import cmd_tajika
monkeypatch.setattr(solar_return_module, "calc_solar_return_chart", lambda *a, **k: {"error": "fictional failure"})
args = types.SimpleNamespace(year=1990, month=1, day=1, hour=12, minute=0, lat=39.9, lon=116.4, tz=8.0,
age=36, mode="all", ayanamsa="raman")
result = cmd_tajika(args)
result = result.get("data", result) if isinstance(result.get("data"), dict) else result
year_lord = result["year_lord"]
assert year_lord["status"] == "blocked"
assert year_lord["reason"] == "solar_return_chart_unavailable"
assert year_lord["year_lord"] is None
assert year_lord["selection_basis_text"] == "年主暂无法确定(年盘无法计算)"
assert result["tri_pataka"] == {"status": "blocked", "reason": "year_lord_blocked"}
def test_pack_and_card_show_the_blocked_sentence(blocked_selector) -> None:
from scripts.annual_tajika_pack import build_annual_tajika_pack
from scripts.consultation_native_layers import build_annual_tajika
from scripts.jyotish_engine import compute_chart_data
pack = build_annual_tajika_pack({**FICTIONAL, "target_year": 2026})
field = pack["year_lord"]
assert field["status"] == "blocked"
assert field["reason"] == "sun_or_moon_missing_for_panchadhikari"
assert field["blocked_detail"]["year_lord"] is None
assert pack["mudda_dasha"]["status"] == "partial_verified"
payload = {"year": 1990, "month": 1, "day": 1, "hour": 12, "minute": 0, "second": 0,
"lat": 39.9, "lon": 116.4, "tz": 8.0, "ayanamsa": "raman", "node_mode": "mean"}
chart, *_ = compute_chart_data(1990, 1, 1, 12, 0, 39.9, 116.4, 8.0, ayanamsa_name="raman")
annual = build_annual_tajika(payload, chart, reference=datetime(2026, 10, 3))
assert annual["status"] == "parameter_sensitive" # the card is not blocked by the year lord alone
assert annual["varshesha"] is None
assert annual["varshesha_basis"] == "年主暂无法确定(年盘缺太阳或月亮位置)"
assert annual["mudda_dasha"]
def test_report_shows_the_blocked_sentence_in_both_editions(blocked_selector) -> None:
from scripts.annual_tajika_pack import build_annual_tajika_pack
from scripts.pl9_reader_export import _pl9_export_markdown_for_edition
from tests.test_report_english_edition import CASES, _packet
blocked_field = build_annual_tajika_pack({**FICTIONAL, "target_year": 2026})["year_lord"]
packet = _packet(CASES["day"])
replaced = 0
def walk(node):
nonlocal replaced
if isinstance(node, dict):
for key, value in list(node.items()):
if key == "year_lord" and isinstance(value, dict) and "producers_checked" in value:
node[key] = copy.deepcopy(blocked_field)
replaced += 1
else:
walk(value)
elif isinstance(node, list):
for value in node:
walk(value)
walk(packet)
assert replaced
zh = _pl9_export_markdown_for_edition(copy.deepcopy(packet), "reader_main")
english = copy.deepcopy(packet)
english["report_language"] = "en"
en = _pl9_export_markdown_for_edition(english, "reader_main")
zh_rows = [line for line in zh.splitlines() if line.startswith("| Selection basis |")]
en_rows = [line for line in en.splitlines() if line.startswith("| Selection basis |")]
assert zh_rows and all("年主暂无法确定(年盘缺太阳或月亮位置)" in row for row in zh_rows)
assert en_rows and all("Year lord cannot be determined yet" in row for row in en_rows)
assert not re.search(r"[一-鿿]", "\n".join(en_rows))
assert all("| Varshesha | - |" in zh for _ in zh_rows)
def test_every_blocked_sentence_has_its_english_twin() -> None:
phrases = dict(PHRASES_EN)
for reason in [*year_lord_module.BLOCKED_REASON_TEXT, "unknown_reason"]:
zh, en = year_lord_module.blocked_text(reason)
assert phrases.get(zh) == en
def test_no_producer_falls_back_to_the_muntha_lord() -> None:
from pathlib import Path
root = Path(__file__).resolve().parents[1]
for name in ("tajika_year_lord.py", "solar_return.py", "jyotish_engine.py"):
text = (root / "scripts" / name).read_text(encoding="utf-8")
assert "fallback_muntha_sign_lord_after_panchadhikari_block" not in text