merge: upstream sync 10 into the data audit branch

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DbBXoyeZrYn6ryNcSe5Y2Y

# Conflicts:
#	docs/BUG_HISTORY.md
This commit is contained in:
2026-11-04 12:41:54 +08:00
28 changed files with 601 additions and 76 deletions
+2 -2
View File
@@ -13,8 +13,8 @@ def cmd_narayana_dasha(args, chart_data):
narayana-dasha 子命令:计算并报告 Narayana Dasha(Rishi Dasha)。
用法:
python jyotish_engine.py narayana-dasha --year REDACTED_YEAR --month 4 --day 17 \\
--hour 14 --minute 45 --lat 36.47 --lon 114.2 --tz 8 --age 33
python jyotish_engine.py narayana-dasha --year 1935 --month 6 --day 21 \\
--hour 11 --minute 0 --lat 44.486 --lon 1.842 --tz 1 --age 33
"""
if chart_data is None:
return "\n".join([
+2 -2
View File
@@ -5,8 +5,8 @@ cmd_solar_return: Solar Return (Varshaphala) CLI 子命令
Jyotish Vedic Astrology Skill — v6.0.18
用法:
python3 jyotish_engine.py solar-return --year REDACTED_YEAR --month 4 --day 17 \
--hour 14 --minute 45 --lat 36.4667 --lon 114.2 --tz 8 --target-year 2026
python3 jyotish_engine.py solar-return --year 1935 --month 6 --day 21 \
--hour 11 --minute 0 --lat 44.486 --lon 1.842 --tz 1 --target-year 2026
"""
import sys
+39 -13
View File
@@ -587,7 +587,7 @@ def calc_dispositor_chain(planet_name, planets_data, max_depth=12):
return chain
def calc_inter_chart_linkage(planet_name, d1_data, d9_data, d10_data, d12_data=None, d1_asc_idx=None, d9_asc_idx=None, d10_asc_idx=None):
def calc_inter_chart_linkage(planet_name, d1_data, d9_data, d10_data, d12_data=None, d1_asc_idx=None, d9_asc_idx=None, d10_asc_idx=None, d12_asc_idx=None):
"""
计算行星在 D1/D9/D10 分盘之间的飞星落宫。
返回 {chart_type: {sign, house_in_chart, lord_of_sign_in_chart}}
@@ -599,7 +599,8 @@ def calc_inter_chart_linkage(planet_name, d1_data, d9_data, d10_data, d12_data=N
('D10', d10_data, d10_asc_idx),
]
if d12_data:
charts.append(('D12', d12_data, None))
# D12 houses count from the D12 ascendant (upstream cf6c316d / f60568a0).
charts.append(('D12', d12_data, d12_asc_idx))
for chart_name, chart_data, asc_idx in charts:
if not isinstance(chart_data, dict):
@@ -608,9 +609,14 @@ def calc_inter_chart_linkage(planet_name, d1_data, d9_data, d10_data, d12_data=N
if not isinstance(pdata, dict) or 'sign' not in pdata:
continue
sign = pdata['sign']
sign_idx = SIGNS.index(sign) if sign in SIGNS else 0
if sign not in SIGNS:
# An unknown sign used to become Aries silently.
raise ValueError(f'Unknown sign in {chart_name}: {sign!r}')
sign_idx = SIGNS.index(sign)
house = None
if asc_idx is not None:
if isinstance(asc_idx, bool) or not isinstance(asc_idx, int) or not 0 <= asc_idx < 12:
raise ValueError(f'Invalid ascendant sign index in {chart_name}: {asc_idx!r}')
house = ((sign_idx - asc_idx) % 12) + 1
lord = SIGN_LORDS.get(sign, '')
result[chart_name] = {
@@ -655,7 +661,7 @@ def calc_all_dispositor_chains(planets_data):
return result
def calc_all_inter_chart_linkages(d1_data, d9_data, d10_data, d12_data=None, d1_asc_idx=None, d9_asc_idx=None, d10_asc_idx=None):
def calc_all_inter_chart_linkages(d1_data, d9_data, d10_data, d12_data=None, d1_asc_idx=None, d9_asc_idx=None, d10_asc_idx=None, d12_asc_idx=None):
"""
为所有行星计算 D1/D9/D10 分盘间飞星。
需要各分盘的 asc_idx 来正确计算落宫。
@@ -664,7 +670,7 @@ def calc_all_inter_chart_linkages(d1_data, d9_data, d10_data, d12_data=None, d1_
for pname in ['Sun', 'Moon', 'Mars', 'Mercury', 'Jupiter', 'Venus', 'Saturn', 'Rahu', 'Ketu']:
result[pname] = calc_inter_chart_linkage(
pname, d1_data, d9_data, d10_data, d12_data,
d1_asc_idx, d9_asc_idx, d10_asc_idx
d1_asc_idx, d9_asc_idx, d10_asc_idx, d12_asc_idx
)
return result
@@ -13765,6 +13771,12 @@ def cmd_kp(args):
_apply_ayanamsa(requested_report_ayanamsa)
return {"error": "swisseph未安装"}
# The chart's julian_day is rounded for display. KP cusp and sub-lord
# boundaries use the civil instant itself, seconds included (upstream
# 26d5a52d, BUG-1543). Same formula as compute_chart_data.
jd = swe.julday(args.year, args.month, args.day,
_birth_hour_decimal(args.hour, args.minute, _arg_second(args)) - args.tz)
asc_sign = chart.get("ascendant", {}).get("sign", "Aries")
planets = chart.get("planets", {})
planet_lons = {
@@ -13778,7 +13790,8 @@ def cmd_kp(args):
from bhava_chalit import BhavaChalitCalculator
kp_house_calculator = BhavaChalitCalculator()
asc_lon = chart.get('ascendant', {}).get('degree_raw', chart.get('ascendant', {}).get('degree', 0.0))
boundaries = kp_house_calculator.calculate_bhava_boundaries(
# Unrounded cusps; calculate_bhava_boundaries rounds them to 4 decimals.
raw_cusps = kp_house_calculator.calculate_cusps(
float(asc_lon), 0.0, 'placidus', jd, args.lat, args.lon,
)
bhava_chart = kp_house_calculator.get_bhava_chalit_chart(
@@ -13792,16 +13805,19 @@ def cmd_kp(args):
for pname, pdata in planets.items():
if isinstance(pdata, dict) and 'sign' in pdata:
bhava_row = bhava_planets.get(pname) if isinstance(bhava_planets.get(pname), dict) else {}
raw_lon = planet_lons.get(pname)
planet_positions[pname] = {
'sign': pdata['sign'],
'degree': pdata.get('degree_in_sign', pdata.get('degree', 0) % 30),
# degree_in_sign is rounded to 4 decimals for display.
'degree': (float(raw_lon) % 30 if raw_lon is not None
else pdata.get('degree_in_sign', pdata.get('degree', 0) % 30)),
'house': bhava_row.get('bhava_house', pdata.get('house', 1)),
}
result = calc_kp_analysis(
planet_positions,
asc_sign,
house_cusps=boundaries.get('cusps'),
house_cusps=raw_cusps,
house_occupancy_basis='cusp_to_cusp',
)
moon_lon = planets.get('Moon', {}).get('degree_raw', planets.get('Moon', {}).get('degree', 0.0)) if isinstance(planets.get('Moon'), dict) else 0.0
@@ -15856,7 +15872,7 @@ def cmd_full_reading(args):
ic_result = calc_all_inter_chart_linkages(
d1_for_dc, d9_for_ic, d10_for_ic, d12_for_ic,
asc_idx, d9_asc_idx, d10_asc_idx
asc_idx, d9_asc_idx, d10_asc_idx, d12_asc_idx
)
report['modules']['inter_chart_linkage'] = ic_result
@@ -16124,7 +16140,10 @@ def cmd_full_reading(args):
from narayana_dasha import attach_narayana_method_comparison, narayana_dasha_full_report
# 计算当前年龄(从出生到 today)
birth_dt = _birth_datetime_from_args(args)
today_dt = datetime.strptime(args.today, '%Y-%m-%d') if hasattr(args, 'today') and args.today else datetime.now()
# One observation date for the whole report (upstream 933f254e, BUG-1542).
today_dt = datetime.strptime(
getattr(args, 'transit_date', None) or getattr(args, 'today', None)
or datetime.now().strftime('%Y-%m-%d'), '%Y-%m-%d')
current_age = (today_dt - birth_dt).days / 365.25
narayana_result = narayana_dasha_full_report(
lagna_sign_idx=asc_idx,
@@ -16451,7 +16470,9 @@ def cmd_full_reading(args):
if nak_age is None:
try:
birth_date = datetime(args.year, args.month, args.day)
nak_age = (datetime.now() - birth_date).days / 365.25
nak_reference = getattr(args, 'transit_date', None) or getattr(args, 'today', None)
observation = datetime.strptime(nak_reference, '%Y-%m-%d') if nak_reference else datetime.now()
nak_age = (observation - birth_date).days / 365.25
except:
nak_age = None
@@ -16490,8 +16511,13 @@ def cmd_full_reading(args):
if age is None:
# 自动计算年龄
try:
birth_date = datetime(args.year, args.month, args.day)
age = (datetime.now() - birth_date).days // 365
# Calendar-birthday age on the report's observation date; days // 365
# drifted about one day early every four years (upstream 933f254e).
ref_today = datetime.strptime(
getattr(args, 'transit_date', None) or getattr(args, 'today', None)
or datetime.now().strftime('%Y-%m-%d'), '%Y-%m-%d')
age = ref_today.year - int(args.year) - (
(ref_today.month, ref_today.day) < (int(args.month), int(args.day)))
report['warnings'].append(f"未提供年龄,自动计算为 {age} 岁")
except:
age = None
+1
View File
@@ -131,6 +131,7 @@ CORE_PYTEST_TARGETS = [
# Summary lines by default; failure keeps the last 200 lines and a full log (BUG-1230).
"tests/test_run_quality_gate_output.py",
"tests/test_upstream_git_import_b9a0ef8f.py",
"tests/test_upstream_git_import_5fb8379c.py",
"tests/test_interpretation_template_registry.py",
"tests/test_vedastro_external_technique_evidence.py",
# Self-hosted heading font slices: 6500-character coverage, no overlap, 120 KB cap.