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
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
bc03bba26b
commit
0afcc2d9a3
@@ -234,9 +234,12 @@ def test_real_annual_producers_agree_without_identical_metadata(reader_packet) -
|
||||
field = annual["year_lord"]
|
||||
assert field["status"] == "partial_verified"
|
||||
left, right = [item["value"] for item in field["producers_checked"]]
|
||||
assert "year_lord_sign" in left
|
||||
assert "year_lord_sign" not in right
|
||||
# 原值:左(太阳返照)有 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"])
|
||||
@@ -252,7 +255,12 @@ def test_reader_annual_cells_use_real_distinct_years_and_keep_return_dates(reade
|
||||
assert "-" not in lords + signs
|
||||
assert len(set(lords)) >= 2
|
||||
assert len(set(signs)) >= 2
|
||||
assert "| Selection basis | Muntha 所在星座的守护星(当前本地口径) |" in reader
|
||||
# 原值:「| 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"):
|
||||
@@ -274,7 +282,11 @@ def test_reader_annual_conflict_keeps_both_real_producer_values(reader_packet) -
|
||||
)
|
||||
assert annual["year_lord"]["status"] == "conflict"
|
||||
reader = _pl9_export_markdown_for_edition(packet, "reader_main")
|
||||
assert "| Varshesha | 太阳返照:火星;Tajika:金星(两者不同) |" in reader
|
||||
# 原值:「太阳返照:火星;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) == []
|
||||
|
||||
@@ -0,0 +1,163 @@
|
||||
"""Tajika year lord by the Panchadhikari rule (BUG-1214, round-4 ruling Q3).
|
||||
|
||||
`tajika.calc_panchadhikari_year_lord` is ported from upstream yinduzhanxing
|
||||
c7117f92 together with `references/tajika_panchavargiya_rules.json`. Upstream's
|
||||
`tests/test_tajika.py` has no Panchadhikari case and its CLI smoke cases need a
|
||||
private local subject chart, so the rule is pinned here with synthetic
|
||||
longitudes (selection branches) and a fictional chart (both producers agree).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
import scripts.tajika as tajika
|
||||
from scripts.annual_tajika_pack import build_annual_tajika_pack
|
||||
from scripts.tajika_year_lord import selection_basis_text
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
UPSTREAM_RULES_BLOB = "e8c15ac31246a1b923f2d108729c319f8731d179" # c7117f92:references/tajika_panchavargiya_rules.json
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _native_panchavargiya(monkeypatch):
|
||||
# Product path: native-proxy Panchavargiya unless the research opt-in is set.
|
||||
monkeypatch.delenv("JYOTISH_YEAR_LORD_PYJHORA_REFERENCE", raising=False)
|
||||
|
||||
|
||||
def _scores(monkeypatch, scores: dict) -> None:
|
||||
def fake(planet, lon_value, **_kwargs):
|
||||
return {"score": scores.get(planet, 0.0), "engine": "native_proxy", "components": {}}
|
||||
monkeypatch.setattr(tajika, "_resolve_year_lord_panchavargiya_score", fake)
|
||||
|
||||
|
||||
def _lon(sign_idx: int, deg: float = 10.0) -> float:
|
||||
return sign_idx * 30.0 + deg
|
||||
|
||||
|
||||
def test_rules_json_is_the_upstream_blob() -> None:
|
||||
data = (ROOT / "references/tajika_panchavargiya_rules.json").read_bytes()
|
||||
assert hashlib.sha1(b"blob %d\0" % len(data) + data).hexdigest() == UPSTREAM_RULES_BLOB
|
||||
assert set(tajika._PANCHAVARGIYA_RULES) == {"source", "tajika_relation", "hudda", "drekkana_lords"}
|
||||
|
||||
|
||||
# Annual Lagna Aries (0). Benefic Tajika aspects reach the Lagna from signs
|
||||
# 3/5/9/11 away (5th/9th/3rd/11th), malefic from 1/4/7/10.
|
||||
BASE = {"Sun": _lon(1), "Moon": _lon(1), "Mars": _lon(1), "Mercury": _lon(1),
|
||||
"Jupiter": _lon(1), "Venus": _lon(1), "Saturn": _lon(1)}
|
||||
|
||||
|
||||
def _select(planet_lons, **kwargs):
|
||||
defaults = dict(annual_asc_sign_idx=0, natal_lagna_sign_idx=3, years_from_dob=0, muntha_sign_idx=6)
|
||||
defaults.update(kwargs)
|
||||
return tajika.calc_panchadhikari_year_lord(planet_lons, **defaults)
|
||||
|
||||
|
||||
def test_benefic_shortlist_takes_the_strongest(monkeypatch) -> None:
|
||||
# Candidates: natal Lagna lord Moon (Cancer), Muntha lord Venus (Libra),
|
||||
# annual Lagna lord Mars, new-year lord and Tri-Rasi lord (day/night).
|
||||
lons = {**BASE, "Moon": _lon(4), "Venus": _lon(8), "Mars": _lon(1)} # Moon in Leo (5th), Venus in Sag (9th)
|
||||
_scores(monkeypatch, {"Moon": 12.0, "Venus": 15.0, "Mars": 20.0})
|
||||
result = _select(lons)
|
||||
assert result["selection_basis"] == "panchadhikari_benefic_shortlist_pvb"
|
||||
assert result["year_lord"] == "Venus" # Mars is stronger but casts no benefic aspect
|
||||
rows = {row["planet"]: row for row in result["candidates"]}
|
||||
assert rows["Moon"]["benefic_aspects_asc"] and rows["Venus"]["benefic_aspects_asc"]
|
||||
assert not rows["Mars"]["benefic_aspects_asc"]
|
||||
|
||||
|
||||
def test_single_benefic_wins_even_when_weak(monkeypatch) -> None:
|
||||
lons = {**BASE, "Moon": _lon(2)} # Moon in Gemini: 3rd from Aries
|
||||
_scores(monkeypatch, {"Moon": 1.0, "Mars": 20.0, "Venus": 20.0})
|
||||
result = _select(lons)
|
||||
assert (result["year_lord"], result["selection_basis"]) == ("Moon", "panchadhikari_single_benefic")
|
||||
|
||||
|
||||
def test_malefic_shortlist_when_no_benefic(monkeypatch) -> None:
|
||||
lons = {**BASE, "Moon": _lon(3), "Venus": _lon(6), "Mars": _lon(1)} # Moon 4th, Venus 7th, Mars 2nd
|
||||
_scores(monkeypatch, {"Moon": 9.0, "Venus": 11.0, "Mars": 30.0})
|
||||
result = _select(lons)
|
||||
assert (result["year_lord"], result["selection_basis"]) == ("Venus", "panchadhikari_malefic_shortlist_pvb")
|
||||
|
||||
|
||||
def test_strongest_of_all_when_no_candidate_aspects(monkeypatch) -> None:
|
||||
lons = {**BASE, "Moon": _lon(1), "Venus": _lon(5), "Mars": _lon(7)} # 2nd / 6th / 8th: no Tajika aspect
|
||||
lons = {planet: (lon if planet in ("Moon", "Venus", "Mars") else _lon(11)) for planet, lon in lons.items()} # others 12th
|
||||
_scores(monkeypatch, {"Moon": 9.0, "Venus": 11.0, "Mars": 14.0, "Sun": 3.0, "Jupiter": 2.0, "Saturn": 1.0, "Mercury": 1.0})
|
||||
result = _select(lons)
|
||||
assert result["selection_basis"] == "panchadhikari_pvb_fallback_all_candidates"
|
||||
assert result["year_lord"] == max(result["candidates"], key=lambda row: row["panchavargiya_score"])["planet"]
|
||||
|
||||
|
||||
def test_muntha_candidate_uses_the_supplied_natal_muntha(monkeypatch) -> None:
|
||||
_scores(monkeypatch, {})
|
||||
lons = dict(BASE)
|
||||
ours = _select(lons, years_from_dob=5, muntha_sign_idx=9) # natal Muntha Capricorn
|
||||
upstream = _select(lons, years_from_dob=5, muntha_sign_idx=None) # upstream: annual Lagna + years = Virgo
|
||||
assert next(row for row in ours["candidates"] if row["source"] == "muntha_lord")["planet"] == "Saturn"
|
||||
assert next(row for row in upstream["candidates"] if row["source"] == "muntha_lord")["planet"] == "Mercury"
|
||||
assert (ours["muntha_sign"], upstream["muntha_sign"]) == ("Capricorn", "Virgo")
|
||||
|
||||
|
||||
@pytest.mark.parametrize("basis", [
|
||||
"panchadhikari_single_benefic", "panchadhikari_benefic_shortlist_pvb", "panchadhikari_single_malefic",
|
||||
"panchadhikari_malefic_shortlist_pvb", "panchadhikari_pvb_fallback_all_candidates",
|
||||
])
|
||||
def test_every_basis_has_a_sentence(basis: str) -> None:
|
||||
zh, en = selection_basis_text({"selection_basis": basis, "year_lord": "Mars", "panchavargiya_engine": "native_proxy",
|
||||
"candidates": [{"planet": "Mars", "benefic_aspects_asc": True, "malefic_aspects_asc": True}]})
|
||||
assert "Mars" in zh and zh.endswith("(Panchavargiya 为本地近似计算)")
|
||||
assert "Mars" in en and en.endswith("(Panchavargiya is a local approximation)")
|
||||
|
||||
|
||||
FICTIONAL = {"birth": {"date": "1990-01-01", "time": "12:00:00", "latitude": 39.9, "longitude": 116.4, "utc_offset": "+08:00"},
|
||||
"settings": {"ayanamsa": "raman"}}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("target_year", [2024, 2025, 2026])
|
||||
def test_both_annual_producers_select_the_same_year_lord(target_year: int) -> None:
|
||||
pack = build_annual_tajika_pack({**FICTIONAL, "target_year": target_year})
|
||||
field = pack["year_lord"]
|
||||
assert field["status"] == "partial_verified", field
|
||||
values = [row["value"] for row in field["producers_checked"]]
|
||||
assert {row["producer"] for row in field["producers_checked"]} == {"solar_return", "tajika"}
|
||||
for value in values:
|
||||
assert value["selection_basis"].startswith("panchadhikari_")
|
||||
assert value["muntha_basis"] == "natal_ascendant_plus_completed_years"
|
||||
assert value["selection_basis_text"] and value["selection_basis_text_en"]
|
||||
assert {row["panchavargiya_engine"] for row in value["candidate_list"]} == {"native_proxy"}
|
||||
assert sum(row["selected"] for row in value["candidate_list"]) == 1
|
||||
assert values[0]["candidates"] == values[1]["candidates"]
|
||||
assert values[0]["year_lord"] == values[1]["year_lord"]
|
||||
# Mudda Dasha starts from the selected year lord on both lanes.
|
||||
assert pack["mudda_dasha"]["data"]["varsha_lord"] == values[0]["year_lord"]
|
||||
|
||||
|
||||
def test_pyjhora_reference_runs_only_on_the_research_opt_in(monkeypatch) -> None:
|
||||
"""The product path never hands PyJHora (AGPL, absent from the API image)
|
||||
the annual JD; the research opt-in restores the upstream lookup."""
|
||||
from scripts.solar_return import calc_solar_return_chart
|
||||
from scripts.tajika_year_lord import PYJHORA_REFERENCE_ENV, select_annual_year_lord
|
||||
|
||||
seen = []
|
||||
|
||||
def fake_external(planet, *, annual_jd, **_kwargs):
|
||||
seen.append(annual_jd)
|
||||
return None
|
||||
|
||||
import sys
|
||||
from scripts import tajika_year_lord
|
||||
selector_module = sys.modules[tajika_year_lord.calc_panchadhikari_year_lord.__module__]
|
||||
monkeypatch.setattr(selector_module, "_external_pyjhora_panchavargiya_score", fake_external)
|
||||
sr = calc_solar_return_chart(1990, 1, 1, 12, 0, 39.9, 116.4, 8.0, 2026, ayanamsa_name="raman")
|
||||
kwargs = dict(natal_asc_sign_idx=sr["birth_asc_sign_idx"], completed_years=36, ayanamsa_name="raman",
|
||||
birth_lat=39.9, birth_lon=116.4, birth_tz=8.0)
|
||||
select_annual_year_lord(sr, **kwargs)
|
||||
assert seen and set(seen) == {None}
|
||||
seen.clear()
|
||||
monkeypatch.setenv(PYJHORA_REFERENCE_ENV, "1")
|
||||
select_annual_year_lord(sr, **kwargs)
|
||||
assert seen and set(seen) == {sr["solar_return"]["jd_ut"]}
|
||||
@@ -1,8 +1,10 @@
|
||||
"""Year lord is labelled as a Muntha-lord stand-in for Panchavargiya (BUG-1212).
|
||||
"""Year lord shows its selection basis on every surface (BUG-1212 → BUG-1214).
|
||||
|
||||
Astrologer rulings 2026-10-03 甲11: Muntha = natal Lagna + completed years
|
||||
(kept); year lord by Panchavargiya is not implemented (native producer blocked),
|
||||
so every surface that shows the year lord says so. Fictional chart.
|
||||
Astrologer rulings 2026-10-03 甲11 and round-4 Q3: Muntha = natal Lagna +
|
||||
completed years (kept); the year lord is chosen by the Panchadhikari rule
|
||||
(`tajika_year_lord`), and the card and both report editions carry its
|
||||
one-sentence basis instead of the BUG-1212 "Muntha lord for now" label.
|
||||
Fictional chart.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -12,7 +14,9 @@ import re
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
from scripts.consultation_native_layers import YEAR_LORD_BASIS, build_annual_tajika
|
||||
import pytest
|
||||
|
||||
from scripts.consultation_native_layers import build_annual_tajika
|
||||
from scripts.pl9_reader_export import _pl9_export_markdown_for_edition
|
||||
from scripts.solar_return import SIGNS, solar_return_full_report
|
||||
from tests.test_report_english_edition import CASES, _packet
|
||||
@@ -20,14 +24,28 @@ from tests.test_report_english_edition import CASES, _packet
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _native_panchavargiya(monkeypatch):
|
||||
# Product path: native-proxy Panchavargiya unless the research opt-in is set.
|
||||
monkeypatch.delenv("JYOTISH_YEAR_LORD_PYJHORA_REFERENCE", raising=False)
|
||||
|
||||
|
||||
def test_report_shows_the_basis_in_both_editions() -> None:
|
||||
# 原值:两版都断言「暂用 Muntha 主星;占星师口径为 Panchavargiya,待接入」
|
||||
# 新值:断言 Panchadhikari 选法依据一句话(中文版含「年盘上升」与「本地近似计算」,
|
||||
# 英文版是同一句的英文、不含中文)
|
||||
# 原因:BUG-1214 接入 Panchadhikari 年主选法,去掉 BUG-1212 的暂用标注(任务书 T2)
|
||||
packet = _packet(CASES["day"])
|
||||
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")
|
||||
assert zh.count("| Selection basis | 暂用 Muntha 主星;占星师口径为 Panchavargiya,待接入 |") >= 1
|
||||
assert "| Selection basis | Muntha sign lord for now; the astrologer's rule is Panchavargiya, not yet wired |" in en
|
||||
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 and "Panchavargiya 为本地近似计算" in row for row in zh_rows)
|
||||
assert en_rows and all("annual Lagna" in row and "local approximation" in row for row in en_rows)
|
||||
assert not re.search(r"[\u4e00-\u9fff]", "\n".join(en_rows))
|
||||
assert "暂用 Muntha 主星" not in zh
|
||||
assert zh.count("| Varshesha |") == zh.count("| Selection basis |")
|
||||
|
||||
|
||||
@@ -39,7 +57,11 @@ def test_consultation_annual_card_carries_the_basis() -> None:
|
||||
case["lat"], case["lon"], case["tz"], ayanamsa_name=case["ayanamsa"])
|
||||
annual = build_annual_tajika(payload, chart, reference=datetime(2026, 10, 3))
|
||||
assert annual["status"] == "parameter_sensitive"
|
||||
assert annual["varshesha_basis"] == YEAR_LORD_BASIS
|
||||
# 原值:varshesha_basis == YEAR_LORD_BASIS(「暂用 Muntha 主星…待接入」)
|
||||
# 新值:varshesha_basis 是年主选法依据一句话(tajika_year_lord.selection_basis_text)
|
||||
# 原因:BUG-1214(任务书 T2)
|
||||
assert "年盘上升" in annual["varshesha_basis"] and "暂用" not in annual["varshesha_basis"]
|
||||
assert annual["varshesha_basis"].endswith("(Panchavargiya 为本地近似计算)")
|
||||
card = (ROOT / "frontend/src/lib/consultation-evidence-card.ts").read_text(encoding="utf-8")
|
||||
assert '"varshesha", "varshesha_basis"' in card
|
||||
workflow = (ROOT / "frontend/src/mastra/consultation-workflow.ts").read_text(encoding="utf-8")
|
||||
@@ -59,7 +81,15 @@ def test_main_path_muntha_is_natal_lagna_plus_completed_years() -> None:
|
||||
assert (muntha["natal_asc_sign_idx"], muntha["age"]) == (natal_asc, completed)
|
||||
assert muntha["muntha_sign_idx"] == (natal_asc + completed) % 12
|
||||
assert muntha["muntha_sign"] == SIGNS[muntha["muntha_sign_idx"]]
|
||||
assert report["year_lord"]["year_lord"] == muntha["muntha_lord"]
|
||||
# 原值:年主 == Muntha 主星
|
||||
# 新值:年主由 Panchadhikari 选出;Muntha 主星一定在候选里(来源 muntha_lord),
|
||||
# 候选 Muntha 用的是「本命上升 + 已满年数」
|
||||
# 原因:BUG-1214(第四轮问 3)
|
||||
year_lord = report["year_lord"]
|
||||
assert year_lord["muntha_basis"] == "natal_ascendant_plus_completed_years"
|
||||
assert year_lord["muntha_sign_idx"] == muntha["muntha_sign_idx"]
|
||||
planets = {row["planet"] for row in year_lord["candidates"]}
|
||||
assert muntha["muntha_lord"] in planets and year_lord["year_lord"] in planets
|
||||
|
||||
|
||||
def test_sun_sign_muntha_variant_is_off_the_main_path() -> None:
|
||||
|
||||
Reference in New Issue
Block a user