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
+15 -1
View File
@@ -277,11 +277,13 @@ def _field_value_is_blocked(value: Any) -> bool:
def _field_with_conflict_gate(key: str, solar_report: dict[str, Any], tajika_report: dict[str, Any]) -> dict[str, Any]:
values = []
blocked = []
for producer, report in (("solar_return", solar_report), ("tajika", tajika_report)):
if not isinstance(report, dict) or report.get(key) is None or report.get("error"):
continue
value = report[key]
if _field_value_is_blocked(value):
blocked.append({"producer": producer, "value": value})
continue
values.append({"producer": producer, "value": value})
comparable_values = [_annual_comparable_value(key, item["value"]) for item in values]
@@ -299,12 +301,20 @@ def _field_with_conflict_gate(key: str, solar_report: dict[str, Any], tajika_rep
"data": values[0]["value"],
"producers_checked": values,
}
return {
field = {
"status": "blocked",
"field": key,
"reason": f"{key}_missing",
"producers_checked": values,
}
if blocked:
# BUG-1223: keep the producer's own blocked record (reason and the
# "年主暂无法确定(原因)" sentence) instead of a bare "<key>_missing".
first = blocked[0]["value"]
field["reason"] = first.get("reason") or field["reason"]
field["blocked_detail"] = first
field["producers_blocked"] = blocked
return field
def _dasha_field(label: str, solar_report: dict[str, Any], tajika_report: dict[str, Any], key: str) -> dict[str, Any]:
@@ -643,6 +653,10 @@ def _field_brief(label: str, field: dict[str, Any] | None) -> str:
return "Tajika Yogas: annual candidate structures visible"
if label == "Sahams":
return "Sahams: annual sensitive points visible"
if status == "blocked" and label == "Year Lord":
detail = field.get("blocked_detail") if isinstance(field.get("blocked_detail"), dict) else {}
if detail.get("selection_basis_text"): # BUG-1223: 「年主暂无法确定(原因)」
return f"{label}: {detail['selection_basis_text']}"
return f"{label}: {status}"
+7 -2
View File
@@ -924,7 +924,12 @@ def build_annual_tajika(birth_payload: dict, chart: dict, *, reference: datetime
producers = {key: field.get("status") for key, field in fields.items()}
periods = fields["mudda_dasha"].get("periods")
required = ("solar_return", "annual_chart", "muntha", "year_lord", "mudda_dasha")
unreliable = [key for key in required if producers.get(key) != "partial_verified"]
# BUG-1223: a year lord the producers blocked (annual chart incomplete,
# no stand-in planet) does not block the card; it shows
# "年主暂无法确定(原因)". Mudda does not depend on the year lord.
year_lord_blocked = _record(fields["year_lord"].get("blocked_detail")) if producers.get("year_lord") == "blocked" else {}
unreliable = [key for key in required
if producers.get(key) != "partial_verified" and not (key == "year_lord" and year_lord_blocked)]
if start is None or end is None or end <= start:
unreliable.append("solar_return_dates")
if not isinstance(periods, list) or not periods:
@@ -939,7 +944,7 @@ def build_annual_tajika(birth_payload: dict, chart: dict, *, reference: datetime
}
annual_chart = _record(fields["annual_chart"].get("data"))
muntha = _record(fields["muntha"].get("data"))
year_lord_data = _record(fields["year_lord"].get("data"))
year_lord_data = year_lord_blocked or _record(fields["year_lord"].get("data"))
year_lord = year_lord_data.get("year_lord")
annual_lagna_idx = _sign_index(annual_chart.get("asc_sign"))
focus = {"Sun", "Moon", year_lord}
+10 -10
View File
@@ -14627,7 +14627,7 @@ def _tajika_annual_core(args, planet_lons, asc_si, age, mode='all'):
"""
try:
sys.path.insert(0, SCRIPT_DIR)
from tajika import calc_muntha, calc_year_lord, calc_tri_pataka
from tajika import calc_muntha, calc_tri_pataka
from tajika_mudda import annual_mudda_dasha
except ImportError as e:
return {"error": f"tajika模块导入失败: {e}"}
@@ -14652,15 +14652,13 @@ def _tajika_annual_core(args, planet_lons, asc_si, age, mode='all'):
def _year_lord() -> Dict:
# Panchadhikari on the solar-return chart, same helper as
# solar_return_full_report (BUG-1214); Muntha lord only when the
# annual chart cannot be computed (recorded as fallback, not hidden).
from tajika_year_lord import select_annual_year_lord
# solar_return_full_report (BUG-1214). No annual chart -> blocked, no
# stand-in Muntha lord (BUG-1223, aligned with upstream 03bea6ed).
from tajika_year_lord import blocked_year_lord, select_annual_year_lord
sr = _solar_return()
if not _sr_usable(sr):
fallback = calc_year_lord(asc_si, age)
fallback['selection_basis'] = 'fallback_muntha_sign_lord_after_panchadhikari_block'
fallback['fallback_reason'] = (sr or {}).get('error', 'solar_return_chart_unavailable') if isinstance(sr, dict) else 'solar_return_chart_unavailable'
return fallback
detail = sr.get('error') if isinstance(sr, dict) else None
return blocked_year_lord('solar_return_chart_unavailable', detail)
return select_annual_year_lord(
sr, natal_asc_sign_idx=sr['birth_asc_sign_idx'], completed_years=int(age),
ayanamsa_name=sr_cache['ayanamsa'], birth_lat=float(args.lat), birth_lon=float(args.lon), birth_tz=float(args.tz),
@@ -14685,9 +14683,11 @@ def _tajika_annual_core(args, planet_lons, asc_si, age, mode='all'):
if mode in ('all', 'mudda'):
result['mudda_dasha'] = _mudda()
if mode in ('all', 'tripataka'):
varsha_lord = year_lord.get('year_lord', 'Jupiter')
varsha_lord = year_lord.get('year_lord')
muntha_si = (asc_si + age) % 12
result['tri_pataka'] = calc_tri_pataka(planet_lons, varsha_lord, muntha_si)
# BUG-1223: no year lord -> no Tri-Pataka (no stand-in planet).
result['tri_pataka'] = (calc_tri_pataka(planet_lons, varsha_lord, muntha_si) if varsha_lord
else {'status': 'blocked', 'reason': 'year_lord_blocked'})
return result
+7
View File
@@ -177,6 +177,13 @@ _PHRASES: dict[str, str] = {
",12 等分": ", twelve equal parts",
# BUG-1214: tajika_year_lord.selection_basis_text (zh) → its en twin.
"年盘不全,暂用 Muntha 主星": "Annual chart incomplete; Muntha sign lord used for now",
# BUG-1223: tajika_year_lord.blocked_text (no stand-in year lord).
"年主暂无法确定(年盘缺太阳或月亮位置)": "Year lord cannot be determined yet (the annual chart lacks the Sun or Moon position)",
"年主暂无法确定(年盘缺行星位置,五分力量无法计算)": "Year lord cannot be determined yet (planet positions are missing from the annual chart, so the five-fold strength cannot be computed)",
"年主暂无法确定(五类候选星都不在年盘上)": "Year lord cannot be determined yet (none of the five candidates is in the annual chart)",
"年主暂无法确定(年盘无法计算)": "Year lord cannot be determined yet (the annual chart could not be computed)",
"年主暂无法确定(缺少 Muntha 星座)": "Year lord cannot be determined yet (the Muntha sign is missing)",
"年主暂无法确定(年盘数据不全)": "Year lord cannot be determined yet (the annual chart data are incomplete)",
"(Panchavargiya 为本地近似计算)": " (Panchavargiya is a local approximation)",
# BUG-1217: tajika_year_lord.ENGINE_NOTE["tajika_native_five_fold"]
"(按 Tajika 五分力量)": " (by Tajika five-fold strength)",
+4
View File
@@ -577,6 +577,10 @@ def render_pl9_parity_markdown(packet: dict) -> str:
def _annual_selection_basis(year_lord, muntha) -> str:
year_lord = _dict(year_lord)
blocked_detail = _dict(year_lord.get('blocked_detail'))
if year_lord.get('status') == 'blocked' and blocked_detail.get('selection_basis_text'):
# BUG-1223: no stand-in year lord; 「年主暂无法确定(原因)」.
return _cell(blocked_detail['selection_basis_text'])
if year_lord.get('status') == 'conflict':
return '两种计算口径给出的年主不同,暂不合并'
data = _dict(year_lord.get('data') or year_lord)
+12 -11
View File
@@ -559,18 +559,15 @@ def solar_return_full_report(
result['muntha'] = {'error': str(e)}
# 3. Year Lord:Panchadhikari 选法(占星师第四轮问 3,BUG-1214)。
# 与 cmd_tajika 共用 tajika_year_lord.select_annual_year_lord;年盘不全
# (降级模式或 Muntha 缺失)时才用 Muntha 主星,并写明 fallback。
# 与 cmd_tajika 共用 tajika_year_lord.select_annual_year_lord;年盘不全时
# 返回 blocked 与原因,不用 Muntha 主星顶替(BUG-1223)。
try:
muntha_data = result.get('muntha', {})
muntha_sign_val = muntha_data.get('muntha_sign_idx') if isinstance(muntha_data, dict) else None
from tajika_year_lord import blocked_year_lord, select_annual_year_lord
if not isinstance(muntha_sign_val, int) or isinstance(muntha_sign_val, bool):
result['year_lord'] = {
'status': 'blocked',
'reason': 'muntha_sign_required_for_year_lord',
}
result['year_lord'] = blocked_year_lord('muntha_sign_required_for_year_lord')
else:
from tajika_year_lord import select_annual_year_lord
result['year_lord'] = select_annual_year_lord(
sr,
natal_asc_sign_idx=sr['birth_asc_sign_idx'],
@@ -624,11 +621,13 @@ def solar_return_full_report(
try:
from tajika import calc_tri_pataka
yl_data = result.get('year_lord', {})
yl = yl_data.get('year_lord', '')
yl = yl_data.get('year_lord') if isinstance(yl_data, dict) else None
muntha_data = result.get('muntha', {})
ms = muntha_data.get('muntha_sign_idx', 0)
tri = calc_tri_pataka(planet_lons, yl, ms)
result['tri_pataka'] = tri
if yl:
result['tri_pataka'] = calc_tri_pataka(planet_lons, yl, ms)
else: # BUG-1223: no year lord -> no Tri-Pataka (no stand-in planet).
result['tri_pataka'] = {'status': 'blocked', 'reason': 'year_lord_blocked'}
except Exception as e:
result['tri_pataka'] = {'error': str(e)}
@@ -708,8 +707,10 @@ def _make_sr_summary(result: Dict) -> str:
lines.append(f" → {muntha.get('interpretation','')[:60]}")
yl = result.get('year_lord', {})
if 'year_lord' in yl:
if yl.get('year_lord'):
lines.append(f" Year Lord:{yl['year_lord']}")
elif yl.get('selection_basis_text'):
lines.append(f" Year Lord:{yl['selection_basis_text']}")
tp = result.get('tri_pataka', {})
if 'verdict' in tp:
+45 -13
View File
@@ -26,6 +26,13 @@ Drekkana, Navamsa; raw out of 80, score = raw / 4 out of 20;
`engine: tajika_native_five_fold`); the dignity proxy no longer selects a year
lord. Set JYOTISH_YEAR_LORD_PYJHORA_REFERENCE=1 to pass the annual JD and let
the upstream PyJHora lookup run (research comparison only).
No stand-in year lord (BUG-1223, product owner 2026-10-04, aligned with
upstream 03bea6ed `docs/research/tajika_shared_entrypoint_repair_2026_10_03.md`
item 3): when Panchadhikari is blocked (annual chart incomplete) the result is
`status: "blocked"` with the reason, `year_lord: None`, and the sentence
"年主暂无法确定(原因)"; the Muntha sign lord is no longer put in its place.
Mudda is independent of the year lord (BUG-1216) and is unaffected.
"""
from __future__ import annotations
@@ -36,14 +43,23 @@ import os
from typing import Any, Dict, List, Optional, Tuple
try: # scripts/ on sys.path
from tajika import SIGNS, SIGN_LORDS, calc_panchadhikari_year_lord
from tajika import calc_panchadhikari_year_lord
except ImportError: # pragma: no cover - package import
from scripts.tajika import SIGNS, SIGN_LORDS, calc_panchadhikari_year_lord
from scripts.tajika import calc_panchadhikari_year_lord
PYJHORA_REFERENCE_ENV = "JYOTISH_YEAR_LORD_PYJHORA_REFERENCE"
SOURCE = "tajika.calc_panchadhikari_year_lord (upstream yinduzhanxing c7117f92)"
MUNTHA_BASIS = "natal_ascendant_plus_completed_years"
FALLBACK_BASIS = "fallback_muntha_sign_lord_after_panchadhikari_block"
BLOCKED_BASIS = "panchadhikari_blocked_no_year_lord"
# Reason token -> (zh, en) for "年主暂无法确定(原因)".
BLOCKED_REASON_TEXT = {
"sun_or_moon_missing_for_panchadhikari": ("年盘缺太阳或月亮位置", "the annual chart lacks the Sun or Moon position"),
"tajika_five_fold_panchavargiya_unavailable": ("年盘缺行星位置,五分力量无法计算", "planet positions are missing from the annual chart, so the five-fold strength cannot be computed"),
"no_panchadhikari_candidates_available": ("五类候选星都不在年盘上", "none of the five candidates is in the annual chart"),
"solar_return_chart_unavailable": ("年盘无法计算", "the annual chart could not be computed"),
"muntha_sign_required_for_year_lord": ("缺少 Muntha 星座", "the Muntha sign is missing"),
}
BLOCKED_REASON_DEFAULT = ("年盘数据不全", "the annual chart data are incomplete")
ENGINE_NOTE = {
"tajika_native_five_fold": ("按 Tajika 五分力量", "by Tajika five-fold strength"),
@@ -61,8 +77,8 @@ def selection_basis_text(data: Dict[str, Any]) -> Tuple[str, str]:
"""One sentence (zh, en) saying why this planet is the year lord."""
basis = data.get("selection_basis")
lord = data.get("year_lord")
if basis == FALLBACK_BASIS:
return ("年盘不全,暂用 Muntha 主星", "Annual chart incomplete; Muntha sign lord used for now")
if basis == BLOCKED_BASIS or data.get("status") == "blocked":
return blocked_text(data.get("reason"))
candidates = [row for row in data.get("candidates") or [] if isinstance(row, dict)]
benefic = [row for row in candidates if row.get("benefic_aspects_asc")]
malefic = [row for row in candidates if row.get("malefic_aspects_asc")]
@@ -91,6 +107,29 @@ def selection_basis_text(data: Dict[str, Any]) -> Tuple[str, str]:
return zh, en
def blocked_text(reason: Any) -> Tuple[str, str]:
"""「年主暂无法确定(原因)」 and its English twin."""
zh, en = BLOCKED_REASON_TEXT.get(str(reason), BLOCKED_REASON_DEFAULT)
return f"年主暂无法确定({zh})", f"Year lord cannot be determined yet ({en})"
def blocked_year_lord(reason: Any, detail: Any = None) -> Dict[str, Any]:
"""Blocked year lord: no planet, reason and sentence only (BUG-1223)."""
data: Dict[str, Any] = {
"status": "blocked",
"reason": str(reason or "panchadhikari_blocked"),
"year_lord": None,
"selection_basis": BLOCKED_BASIS,
"muntha_basis": MUNTHA_BASIS,
"candidate_list": [],
"source": SOURCE,
}
if detail is not None:
data["detail"] = detail
data["selection_basis_text"], data["selection_basis_text_en"] = blocked_text(data["reason"])
return data
def _candidate_list(candidates: List[Dict[str, Any]], winner: Optional[str]) -> List[Dict[str, Any]]:
return [{
"source": row.get("source"),
@@ -142,14 +181,7 @@ def select_annual_year_lord(
data = _select(sr, natal_asc_sign_idx, completed_years, muntha_sign_idx,
ayanamsa_name, birth_lat, birth_lon, birth_tz)
if data.get("status") != "partial_verified":
lord = SIGN_LORDS[SIGNS[muntha_sign_idx]]
data = {
"year_lord": lord,
"year_lord_sign_idx": muntha_sign_idx,
"year_lord_sign": SIGNS[muntha_sign_idx],
"selection_basis": FALLBACK_BASIS,
"fallback_reason": data.get("reason", "panchadhikari_blocked"),
}
return blocked_year_lord(data.get("reason", "panchadhikari_blocked"), data.get("detail"))
data["muntha_basis"] = MUNTHA_BASIS
data["candidate_list"] = _candidate_list(data.get("candidates") or [], data.get("year_lord"))
data["source"] = SOURCE