diff --git a/scripts/jyotish_api_server.py b/scripts/jyotish_api_server.py index 1e0af92e..a4bfd9c6 100644 --- a/scripts/jyotish_api_server.py +++ b/scripts/jyotish_api_server.py @@ -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() diff --git a/scripts/jyotish_engine.py b/scripts/jyotish_engine.py index 4d273a24..7c812428 100644 --- a/scripts/jyotish_engine.py +++ b/scripts/jyotish_engine.py @@ -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 {} diff --git a/scripts/pl9_reader_export.py b/scripts/pl9_reader_export.py index e737c4da..3b673e79 100644 --- a/scripts/pl9_reader_export.py +++ b/scripts/pl9_reader_export.py @@ -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([ diff --git a/scripts/shadbala_display.py b/scripts/shadbala_display.py new file mode 100644 index 00000000..e9b13469 --- /dev/null +++ b/scripts/shadbala_display.py @@ -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 diff --git a/tests/test_shadbala_minimum_first.py b/tests/test_shadbala_minimum_first.py new file mode 100644 index 00000000..7c2ef1d6 --- /dev/null +++ b/tests/test_shadbala_minimum_first.py @@ -0,0 +1,53 @@ +"""Shadbala: BPHS minimum first, the band labelled as the site's (BUG-1208). + +Astrologer rulings 2026-10-03 甲13 (TASK-astrologer-rulings-batch1-20261003 T3). +`shadbala.py` is unchanged (frozen rectification identity); display fields come +from `shadbala_display`. Fictional chart for the report render. +""" + +from __future__ import annotations + +import copy + +from scripts.pl9_reader_export import _pl9_export_markdown_for_edition +from scripts.shadbala import MIN_REQUIRED +from scripts.shadbala_display import band_text, minimum_fields, row_fields +from tests.test_report_english_edition import CASES, _packet + + +def test_minimum_fields() -> None: + assert minimum_fields(6.0, 6.5, "略弱") == { + "min_required": 6.5, "meets_minimum": False, "minimum_check": "未达到 BPHS 最低要求", + "level_source": "site_band", "level": "略弱(网站分档)"} + assert minimum_fields(6.5, 6.5, "充足")["meets_minimum"] is True + assert minimum_fields(None, 6.5, None) == {"min_required": 6.5, "meets_minimum": None, "minimum_check": None, "level_source": "site_band"} + assert row_fields({"total_rupas": 5.1, "min_required": MIN_REQUIRED["Mars"], "strength_level": "充足"})["meets_minimum"] is True + assert band_text("充足", "en") == "adequate (site band)" + + +def test_report_table_shows_minimum_check_then_site_band() -> None: + packet = _packet(CASES["day"]) + for language, check, band in (("zh", "BPHS 最低要求", "(网站分档)"), ("en", "BPHS minimum", "(site band)")): + edition = copy.deepcopy(packet) + edition["report_language"] = language + markdown = _pl9_export_markdown_for_edition(edition, "reader_main") + header = next(line for line in markdown.splitlines() if line.startswith("| Planet | Sthana |")) + assert header.rstrip(" |").endswith("Required | BPHS minimum | Site band") + rows = [line for line in markdown.splitlines()[markdown.splitlines().index(header) + 2:][:7]] + assert len(rows) == 7 and all(check in row and band in row for row in rows), language + + +def test_consultation_chart_summary_carries_the_fields() -> None: + """`chart.shadbala` (copied onto the consultation card) from a live chart.""" + from scripts.capture_report_blocked_repairs_golden import _handler + + case = CASES["day"] + chart = _handler()._compute_chart({**{key: case[key] for key in ("year", "month", "day", "hour", "minute", "second", "lat", "lon", "tz", "ayanamsa", "node_mode")}, + "skip_vedastro_main_entry_overview": True}) + assert set(chart["shadbala"]) == {"Sun", "Moon", "Mars", "Mercury", "Jupiter", "Venus", "Saturn"} + for planet, row in chart["shadbala"].items(): + assert set(row) == {"rupas", "min_required", "meets_minimum", "minimum_check", "level_source", "level"}, planet + assert row["min_required"] == MIN_REQUIRED[planet] + assert row["meets_minimum"] == (row["rupas"] >= MIN_REQUIRED[planet]) or abs(row["rupas"] - MIN_REQUIRED[planet]) < 0.01 + assert row["level_source"] == "site_band" and row["level"].endswith("(网站分档)") + assert row["minimum_check"] in ("达到 BPHS 最低要求", "未达到 BPHS 最低要求")