feat(shadbala): BPHS minimum check first, bands labelled as the site's (T3, BUG-1208)

Astrologer ruling 甲13. New scripts/shadbala_display.py (display only; shadbala.py is
frozen and unchanged): chart.shadbala now carries min_required, meets_minimum,
minimum_check (达到/未达到 BPHS 最低要求), level_source=site_band and level
'<band>(网站分档)'; report Shadbala table gains 'BPHS minimum' and 'Site band'
columns (zh/en); evidence snapshot ranking and health narrative say the minimum
check first and mark the band as the site's.

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-03 12:30:19 +08:00
co-authored by Claude Opus 5.5
parent de79390175
commit 5fdf2f7498
5 changed files with 137 additions and 5 deletions
+4 -1
View File
@@ -6554,7 +6554,9 @@ class JyotishAPIHandler(BaseHTTPRequestHandler, VedastroEvidenceMixin, SynastryM
planets_data.get('Sun',{}).get('lon',0),
moon_lon,
)
shadbala_summary = {p: {'rupas': round(d['total_rupas'],2), 'level': d['strength_level']}
# BUG-1208 (甲13): BPHS minimum first, then the site band, labelled.
from shadbala_display import row_fields
shadbala_summary = {p: {'rupas': round(d['total_rupas'],2), **row_fields(d)}
for p,d in sb.get('planets',{}).items()}
except Exception as e:
import logging
@@ -6833,6 +6835,7 @@ class JyotishAPIHandler(BaseHTTPRequestHandler, VedastroEvidenceMixin, SynastryM
{
'planet': planet,
'rupas': pdata.get('rupas'),
'minimum_check': pdata.get('minimum_check'),
'level': pdata.get('level'),
}
for planet, pdata in shadbala.items()
+7 -1
View File
@@ -153,6 +153,7 @@ from cmd_solar_return import cmd_solar_return # v6.0.18
from cmd_narayana_dasha import cmd_narayana_dasha as _cmd_narayana_dasha_impl # v6.0.20
from cmd_muhurta import cmd_muhurta # v6.0.21
from yoga_engine import detect_yogas # v6.0.26: data-driven Yoga engine
from shadbala_display import check_text as _shadbala_check_text, row_fields as _shadbala_row_fields # BUG-1208
from kp_system import calc_kp_analysis, get_kp_lords # v6.9.10: KP完整系统
from bhava_chalit import BhavaChalitCalculator, cmd_bhava_chalit # v6.9.13: Bhava Chalit 不等宫边界调整
from sudarshana_chakra import calc_sudarshana_chakra, generate_sudarshana_report # v6.9.14: Sudarshana Chakra 三参考点盘
@@ -11361,7 +11362,9 @@ def _build_health_narrative_payload(modules, chart=None):
total = row.get('total_rupas', row.get('total_rupa'))
minimum = row.get('min_required')
level = row.get('strength_level') or row.get('level')
strengths.append(f'{lord} Shadbala:total={total if total is not None else "-"},minimum={minimum if minimum is not None else "-"},level={level or "-"}。')
# BUG-1208 (甲13): the BPHS minimum check first, the band marked as the site's.
check = _shadbala_check_text(_shadbala_row_fields(row)['meets_minimum'])
strengths.append(f'{lord} Shadbala:total={total if total is not None else "-"},minimum={minimum if minimum is not None else "-"},{check},网站分档={level or "-"}。')
if isinstance(total, (int, float)) and isinstance(minimum, (int, float)) and total < minimum:
risks.append(f'{lord} 的 Shadbala 低于本地最低参考值,只能作为压力负荷提示。')
@@ -11857,7 +11860,10 @@ def _build_ai_prompt_pack(report):
'rank': pdata.get('rank'),
'total_rupas': pdata.get('total_rupas'),
'min_required': pdata.get('min_required'),
# BUG-1208 (甲13): BPHS minimum first; the band is the site's own.
'meets_minimum': _shadbala_row_fields(pdata)['meets_minimum'],
'strength_level': pdata.get('strength_level'),
'level_source': 'site_band',
})
current_ad = current_dasha.get('antardasha') if isinstance(current_dasha, dict) else {}
+8 -3
View File
@@ -1124,9 +1124,12 @@ def render_pl9_parity_markdown(packet: dict) -> str:
f"- Shadbala 方法:{_cell(shadbala.get('method'))}",
f"- 外部边界:{_cell(shadbala.get('external_parity_boundary'))}",
'',
'| Planet | Sthana | Dig | Kala | Chesta | Naisargika | Drik | Total | Required |',
'|--------|--------|-----|------|--------|------------|------|-------|----------|',
'| Planet | Sthana | Dig | Kala | Chesta | Naisargika | Drik | Total | Required | BPHS minimum | Site band |',
'|--------|--------|-----|------|--------|------------|------|-------|----------|--------------|-----------|',
])
# BUG-1208 (甲13): whether the BPHS minimum is met comes first; the
# 150/125/100/75/50 band is the site's own and is labelled so.
from shadbala_display import band_text as _band_text, check_text as _check_text, row_fields as _row_fields
for planet in ('Sun', 'Moon', 'Mars', 'Mercury', 'Jupiter', 'Venus', 'Saturn'):
row = _dict(shadbala_planets.get(planet))
if not row:
@@ -1137,7 +1140,9 @@ def render_pl9_parity_markdown(packet: dict) -> str:
f"| {_cell(_token(planet))} | {_num(_dict(row.get('sthana_bala')).get('total') or row.get('sthana_bala'))} | "
f"{_num(_dict(row.get('dig_bala')).get('total') or row.get('dig_bala'))} | {_num(_dict(row.get('kala_bala')).get('total') or row.get('kala_bala'))} | "
f"{_num(_dict(row.get('chesta_bala')).get('total') or row.get('chesta_bala'))} | {_num(row.get('naisargika_bala'))} | "
f"{_num(_dict(row.get('drik_bala')).get('total') or row.get('drik_bala'))} | {_num(total)} | {_num(required)} |"
f"{_num(_dict(row.get('drik_bala')).get('total') or row.get('drik_bala'))} | {_num(total)} | {_num(required)} | "
f"{_cell(_check_text(_row_fields(row)['meets_minimum'], _pl9_report_language(packet)))} | "
f"{_cell(_band_text(row.get('strength_level'), _pl9_report_language(packet)))} |"
)
lines.append('')
lines.extend([
+65
View File
@@ -0,0 +1,65 @@
#!/usr/bin/env python3
"""How a Shadbala strength is shown (astrologer rulings 2026-10-03 甲13, BUG-1208).
First whether the planet meets the BPHS minimum (BPHS ch. 27 vv. 32-33, the
`shadbala.MIN_REQUIRED` table), then the site's own band (150/125/100/75/50 % of
that minimum -> 极强…极弱), labelled as the site's band. `shadbala.py` is in the
frozen rectification identity, so the fields are added here, at display time,
from values the engine already returns; nothing is recomputed.
"""
from __future__ import annotations
from typing import Any, Dict, Optional
LEVEL_SOURCE = 'site_band'
SITE_BAND_SUFFIX = '(网站分档)'
SITE_BAND_SUFFIX_EN = ' (site band)'
MEETS_ZH = '达到 BPHS 最低要求'
MISSES_ZH = '未达到 BPHS 最低要求'
MEETS_EN = 'meets the BPHS minimum'
MISSES_EN = 'below the BPHS minimum'
def _number(value: Any) -> Optional[float]:
return float(value) if isinstance(value, (int, float)) and not isinstance(value, bool) else None
def minimum_fields(rupas: Any, min_required: Any, band: Any) -> Dict[str, Any]:
"""`min_required`, `meets_minimum`, `minimum_check`, band label and source."""
total = _number(rupas)
minimum = _number(min_required)
meets = None if total is None or minimum is None else total >= minimum
fields: Dict[str, Any] = {
'min_required': minimum,
'meets_minimum': meets,
'minimum_check': None if meets is None else (MEETS_ZH if meets else MISSES_ZH),
'level_source': LEVEL_SOURCE,
}
if band not in (None, ''):
fields['level'] = f'{band}{SITE_BAND_SUFFIX}'
return fields
def row_fields(row: Dict[str, Any]) -> Dict[str, Any]:
"""Display fields for one `shadbala.calc_shadbala` planet row."""
return minimum_fields(row.get('total_rupas', row.get('rupas')), row.get('min_required'), row.get('strength_level'))
BAND_EN = {'极强': 'very strong', '强': 'strong', '充足': 'adequate', '略弱': 'slightly weak', '弱': 'weak', '极弱': 'very weak'}
def band_text(band: Any, language: str = 'zh') -> Optional[str]:
if band in (None, ''):
return None
if language == 'en':
return f"{BAND_EN.get(str(band), str(band))}{SITE_BAND_SUFFIX_EN}"
return f'{band}{SITE_BAND_SUFFIX}'
def check_text(meets: Optional[bool], language: str = 'zh') -> str:
if meets is None:
return '-'
if language == 'en':
return MEETS_EN if meets else MISSES_EN
return MEETS_ZH if meets else MISSES_ZH