fix(dignity): record Neecha Bhanga kendras and clip the first dasha from its full parent (BUG-1268, upstream 4a807e7d)

This commit is contained in:
jesse-ux
2026-10-07 17:40:58 +08:00
parent f02d27a73f
commit edeec41309
4 changed files with 202 additions and 36 deletions
+104 -28
View File
@@ -612,32 +612,83 @@ def _build_planetary_friendship_snapshot(planets_data):
),
}
def _check_neecha_bhanga(planet, sign, planets_data):
"""Check for Cancellation of Debilitation."""
def _neecha_bhanga_evidence(planet, sign, planets_data):
"""Record cancellation conditions. A hit is not a Raja Yoga."""
evidence = {
'planet': planet,
'debilitation_sign': sign,
'成立': False,
'reasons': [],
'conditions_checked': [
'dispositor_exalted',
'exaltation_lord_conjunct_debilitation_sign',
'dispositor_kendra_from_moon',
'dispositor_kendra_from_lagna',
],
'royal_yoga_label_allowed': False,
'claim_boundary': (
'Neecha Bhanga evidence records cancellation conditions only; it does not by itself prove a Raja Yoga, '
'complete reversal of weakness, or a guaranteed outcome.'
),
}
if not planets_data:
return False
evidence['status'] = 'blocked_missing_planets'
return evidence
sign_lord = SIGN_LORDS.get(sign)
# Condition 1: Dispositor is exalted.
lord_data = planets_data.get(sign_lord, {}) if sign_lord else {}
moon_sign = (planets_data.get('Moon') or {}).get('sign') if isinstance(planets_data.get('Moon'), dict) else None
lord_sign = lord_data.get('sign') if isinstance(lord_data, dict) else None
signs = list(SIGN_LORDS)
if lord_sign in signs and moon_sign in signs:
moon_house = (signs.index(lord_sign) - signs.index(moon_sign)) % 12 + 1
if moon_house in (1, 4, 7, 10):
evidence['成立'] = True
evidence['reasons'].append({
'condition': 'dispositor_kendra_from_moon',
'planet': sign_lord,
'house_from_moon': moon_house,
})
if isinstance(lord_data, dict) and lord_data.get('house') in (1, 4, 7, 10):
evidence['成立'] = True
evidence['reasons'].append({
'condition': 'dispositor_kendra_from_lagna',
'planet': sign_lord,
'house': lord_data['house'],
})
if sign_lord and sign_lord in planets_data:
lord_sign = planets_data[sign_lord].get('sign')
if lord_sign and EXALTATION.get(sign_lord) == lord_sign:
return True
# Condition 2: Planet exalted in this sign is conjunct.
evidence['成立'] = True
evidence['reasons'].append({
'condition': 'dispositor_exalted',
'planet': sign_lord,
'sign': lord_sign,
})
exalted_planet = None
for p, ex_sign in EXALTATION.items():
if ex_sign == sign:
exalted_planet = p
break
if exalted_planet and exalted_planet in planets_data:
ex_p_sign = planets_data[exalted_planet].get('sign')
if ex_p_sign == sign:
return True
return False
evidence['成立'] = True
evidence['reasons'].append({
'condition': 'exaltation_lord_conjunct_debilitation_sign',
'planet': exalted_planet,
'sign': sign,
})
evidence['status'] = 'executed'
return evidence
def _check_neecha_bhanga(planet, sign, planets_data):
"""Boolean compatibility wrapper. Details live on the evidence helper."""
return bool(_neecha_bhanga_evidence(planet, sign, planets_data).get('成立'))
def _build_dignity_context(chart_data):
@@ -652,6 +703,7 @@ def _build_dignity_context(chart_data):
context[pn] = {
'sign': pd.get('sign'),
'degree_in_sign': pd.get('degree_in_sign', pd.get('degree', 0) % 30),
'house': pd.get('house'),
}
return context
@@ -6808,7 +6860,8 @@ def render_pl9_markdown(packet: dict) -> str:
bhava_bala = strength_sheet.get('bhava_bala') if isinstance(strength_sheet.get('bhava_bala'), dict) else {}
strongest = _humanize_reader_token(shadbala.get('strongest')) if isinstance(shadbala, dict) else None
weakest = _humanize_reader_token(shadbala.get('weakest')) if isinstance(shadbala, dict) else None
houses = bhava_bala.get('houses') if isinstance(bhava_bala.get('houses'), list) else []
from special_lagnas import bhava_house_rows
houses = bhava_house_rows(bhava_bala)
top_house = None
top_score = None
weak_house = None
@@ -7653,7 +7706,8 @@ def render_pl9_markdown(packet: dict) -> str:
)
out.append('')
if bhava_bala:
rows = bhava_bala.get('houses') if isinstance(bhava_bala.get('houses'), list) else []
from special_lagnas import bhava_house_rows
rows = bhava_house_rows(bhava_bala)
if rows:
out.extend([
'#### Bhava Bala(三分量)',
@@ -12470,29 +12524,51 @@ def cmd_dasha(args):
today = datetime.strptime(args.today, "%Y-%m-%d") if args.today else datetime.now()
current = None
year_days = 365.25
for d in timeline:
ds = datetime.strptime(d["start"], "%Y-%m-%d"); de = datetime.strptime(d["end"], "%Y-%m-%d")
total_days = (de - ds).days; li = DASHA_ORDER.index(d["lord"])
# v3.7.2: 为所有 Mahadasha 计算 Antardasha(不仅当前大运)
sub = []; sdt = ds
# 出生只截断第一大运的可见段。小运和更小运从完整父段起点排,再裁到出生之后。
parent_start = datetime.fromisoformat(d["start_datetime"])
parent_end = datetime.fromisoformat(d["end_datetime"])
visible_origin = birth_dt if d.get("is_balance") else parent_start
total_duration = timedelta(days=float(d["full_years"]) * year_days)
li = DASHA_ORDER.index(d["lord"])
sub = []
sdt = parent_start
for j in range(9):
sl = DASHA_ORDER[(li + j) % 9]; sd = total_days * DASHA_YEARS[sl] / 120
se = sdt + timedelta(days=sd)
is_cur = sdt <= today < se
sl = DASHA_ORDER[(li + j) % 9]
se = parent_end if j == 8 else sdt + (total_duration * (DASHA_YEARS[sl] / 120.0))
if se <= visible_origin:
sdt = se
continue
visible_start = max(sdt, visible_origin)
children = _build_pratyantar_timeline(d["lord"], sl, sdt, se, reference_dt=today)
visible_children = []
for child in children:
child_start = datetime.fromisoformat(child["start_datetime"])
child_end = datetime.fromisoformat(child["end_datetime"])
if child_end <= visible_origin:
continue
if child_start < visible_origin:
child["unclipped_start_datetime"] = child_start.isoformat(timespec="seconds")
child["start"] = visible_origin.strftime("%Y-%m-%d")
child["start_datetime"] = visible_origin.isoformat(timespec="seconds")
child["is_current"] = bool(visible_origin <= today < child_end)
visible_children.append(child)
sub.append({
"lord": sl,
"lord_cn": PLANET_CN[sl],
"start": sdt.strftime("%Y-%m-%d"),
"start": visible_start.strftime("%Y-%m-%d"),
"end": se.strftime("%Y-%m-%d"),
"start_datetime": sdt.isoformat(timespec="seconds"),
"start_datetime": visible_start.isoformat(timespec="seconds"),
"unclipped_start_datetime": sdt.isoformat(timespec="seconds"),
"end_datetime": se.isoformat(timespec="seconds"),
"is_current": is_cur,
"pratyantar_dasha_timeline": _build_pratyantar_timeline(
d["lord"], sl, sdt, se, reference_dt=today,
),
"is_current": visible_start <= today < se,
"pratyantar_dasha_timeline": visible_children,
})
sdt = se
d["antardasha_timeline"] = sub
ds = datetime.strptime(d["start"], "%Y-%m-%d")
de = datetime.strptime(d["end"], "%Y-%m-%d")
if ds <= today < de:
d["is_current"] = True
# 从 antardasha 列表中提取当前正在运行的 antardasha
+9 -8
View File
@@ -1197,11 +1197,11 @@ def render_pl9_parity_markdown(packet: dict) -> str:
)
lines.append('')
bhava_bala = _dict(strengths.get('bhava_bala'))
bhava_rows = _list(bhava_bala.get('houses')) or [
{'house': key, **value}
for key, value in bhava_bala.items()
if str(key).isdigit() and isinstance(value, dict)
]
try:
from special_lagnas import bhava_house_rows
except ImportError:
from scripts.special_lagnas import bhava_house_rows
bhava_rows = bhava_house_rows(bhava_bala)
if bhava_rows:
# BUG-1210 (甲13): the formal three components (scripts/bhava_bala.py);
# the local +2 / -1.5 house score is never shown.
@@ -1211,7 +1211,7 @@ def render_pl9_parity_markdown(packet: dict) -> str:
'| House | Sign | Lord | Bhavadhipati | Dig | Drishti | Total (Virupa) | Total (Rupa) |',
'|-------|------|------|--------------|-----|---------|----------------|--------------|',
])
for row in bhava_rows[:12]:
for row in bhava_rows:
if not isinstance(row, dict) or row.get('total') is None:
continue
lines.append(
@@ -1292,8 +1292,8 @@ def render_pl9_parity_markdown(packet: dict) -> str:
lines.extend(['### Jaimini Karaka、Arudha Pada 与特殊 Lagna', ''])
if karaka_table:
lines.extend([
'| Karaka | Planet | Degree | Domain |',
'|--------|--------|--------|--------|',
'| Karaka | Planet | Degree | Ranking degree | Domain |',
'|--------|--------|--------|----------------|--------|',
])
for name, row in karaka_table.items():
if not isinstance(row, dict):
@@ -1301,6 +1301,7 @@ def render_pl9_parity_markdown(packet: dict) -> str:
lines.append(
f"| {_cell(row.get('cn_name') or name)} | {_cell(_token(row.get('planet')))} | "
f"{_cell(_degree(row.get('degree_in_sign') or row.get('sign_degree')))} | "
f"{_cell(_degree(row.get('effective_degree_in_sign', row.get('degree_in_sign'))))} | "
f"{_cell(row.get('domain'))} |"
)
lines.append('')