fix(solar-return): return instant in the solver's frame, annual chart keeps seconds (BUG-1220)
Independent Staging Quality Gate / validate (push) Successful in 20m3s
Independent Staging Quality Gate / publish (push) Successful in 4m2s

The natal Sun target came from compute_chart_data while the solver iterates
on _get_sun_lon_jd (swe sidereal flags); the frames differ by up to ~17",
moving the return by up to ~7 minutes. The annual chart was also cast at the
truncated minute. Ports upstream yinduzhanxing 7cc6425d / 46ed9399 (these two
hunks only). PyJHora reference: 3 cases within 0.03 min. 243 public-figure
annual charts: annual Lagna changes 13, year lord 11, Mudda first lord 0.
v5 77 cases identical to 3c27c38e; consult goldens re-captured byte-identical.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017eEAG8HD3mm8gsKXgk8uU8
This commit is contained in:
Jesse_Chen
2026-10-03 22:10:20 +08:00
co-authored by Claude Opus 5.5
parent d0b783bd05
commit 01929baea6
7 changed files with 200 additions and 9 deletions
+6
View File
@@ -1,5 +1,11 @@
# 印度占星 Skill 更新日志
## 2026-10-03 — 太阳返照时刻改准:约 5% 的年份年盘上升和年主会变
- 年运(Varshaphala)的太阳返照时刻以前会偏 3–7 分钟:出生太阳和求返照用的不是同一套参考系,年盘还按整分钟起。现在两者对齐并保留秒,与外部参照引擎 PyJHora 抽查 3 例相差不到 0.03 分钟(BUG-1220)。
- 影响:9 位公开名人 2000–2026 年共 243 张年盘里,年盘上升星座变 13 张、年主变 11 张;昼夜判定与 Mudda 起运星不变。数据卡、参考版报告的年运段随之改变;旧报告内容不变。
- 生时校正不用年盘,分数与算法版本不变。Skill 版本不变;不改数据库结构。
## 2026-10-03 — 占星师口径第三批:Mudda 改从出生月宿起排、年主按 Tajika 五分力量比、Rath 版 Narayana 起运宫重做(未上线)
- 年内 Mudda Dasha 不再从年主起排,改按占星师定的公式:起运星 =(已满年数 + 出生月宿序号 − 2)除以 9 的余数(1 日、2 月、3 火、4 罗、5 木、6 土、7 水、8 计、0 金),余额按出生时月亮在月宿里走过的比例,年长 365.25 天。年主怎么变,月份分段都不变。数据卡和报告的年内分段都来自这个新算法;9 位公开名人里 8 位的年内第一段与原来不同(BUG-1216)。
+16
View File
@@ -16451,3 +16451,19 @@
- 相关记录:BUG-1214、BUG-1217。
- 复发自:无
- 修复版本:—(上游)
## BUG-1220 | 太阳返照时刻偏 3–7 分钟:出生太阳与求解器不在同一参考系,年盘还截到整分钟
- 状态:resolved(分支 `codex/annual-return-frame-20261003`;待推送部署)
- 首次发现 / 最近更新:2026-10-03 / 2026-10-03
- 来源:共享引擎复用核对 `docs/research/shared_engine_reuse_audit_2026_10_03.md` §S1;任务书 `TASK-annual-return-frame-20261003`。移植来源:上游 `yinduzhanxing` `7cc6425d`(出生太阳改在求解器参考系重算)、`46ed9399`(年盘保留秒),只取这两处。
- 影响面:`solar_return.calc_solar_return_chart` → `solar_return_full_report`、`jyotish_engine.cmd_tajika`、全量解读 Step 9、`annual_tajika_pack`、咨询年运层(数据卡)、参考版报告年运段:年盘上升、年主、Sahams、Mudda 起点时刻。生时校正不用年盘,不受影响。
- 现象:同一张盘同一岁差,本站返照时刻与上游、PyJHora 差几分钟(Rath 标准盘 2025 年差 6 分 16 秒)。9 位公开名人 × 2000–2026 年共 243 张年盘里,年盘上升星座错 13 张、年主错 11 张,昼夜判定 0 张受影响。
- 触发条件:任何盘。偏差大小取决于出生时刻的章动等参考系差(约 ±17″ 以内,折合返照时刻约 ±7 分钟);年盘上升恰在星座边界附近的年份会换星座。
- 根因:① 目标经度取 `compute_chart_data` 的出生太阳 `degree_raw`,求解器 `_find_solar_return_swe` 用 `_get_sun_lon_jd`(`swe.calc_ut` + `sidereal_flags`)逐步逼近,两套参考系对出生太阳差 15.1″(标准盘)。② 起年盘时用整点分钟调用 `compute_chart_data`,丢了秒。上游 9 月 7–8 日已修,本站引擎同步停在 `f2241463`(9 月 3 日),之后 `solar_return.py` 只按 BUG-1095 补过本地化,漏了这两处(ERR-109 同类)。
- 修复:`calc_solar_return_chart` 中目标经度改为 `_get_sun_lon_jd(birth_jd_ut)`,拿不到才退回出生盘值;返回 `solar_return.target_sun_frame`(`swe_sidereal_flags` / `birth_chart_degree_raw`)。起年盘传 `second=sr_dt_ut.second`。上游同期加的 `position_mode` 等研究回放参数未搬;本地化保留本站实现。
- 验证:`tests/test_solar_return_frame.py`:三例返照 JD 与 PyJHora `drik.next_solar_date` 参照值(只存数字)差 ≤ 0.5 分钟,实测 0.0 / 0.021 / 0.006 分钟;目标经度等于求解器参考系出生太阳(< 1″);年盘时刻与返照差 < 1 秒。换回旧代码这 5 条全部失败。243 张年盘前后对比、v5(三段与基线逐项相同)、快速门、前端失败名单比对见进度记录。咨询两份 golden 用真实引擎重采后逐字节不变(三张盘 2026 年的年盘上升与年主恰好不变,日期只存到天)。
- 防复发:上述测试;同步上游太阳返照时,核对目标经度与求解器同一参考系(已补进 ERR-109)。
- 相关记录:BUG-1095、BUG-1026~1028、ERR-109。
- 复发自:无(ERR-109 同类漏同步)
- 修复版本:待发布
+2
View File
@@ -443,6 +443,8 @@ Prevention: 前台同步路径不得进入无上限排队,超预算即 fail-fa
Prevention: 引擎/上游同步类任务必须 fetch 后核对 `origin/main`,以实际 SHA 为准,并在进度和本台账写明与任务书 tip 的差异。读者版整段引入要带来源提交。不要把 `.workbuddy` 当主仓。同步太阳返照时核对返回键,而不只核对函数还在。关联 BUG-1026、BUG-1027、BUG-1028。
2026-10-03 复发(BUG-1220):同一次同步漏了上游 `7cc6425d`(出生太阳在求解器参考系重算)与 `46ed9399`(年盘保留秒),返照时刻偏 3–7 分钟、约 5% 年份年盘上升换星座。补充:同步太阳返照时还要核对目标经度与求解器是同一参考系,并用外部引擎抽几例返照时刻对照。
## ERR-110 | 输出逐字节不变的引擎性能改动也会打红校正验证完整性门禁 | observed 2026-09-26
`scripts/rectification/event_probes.py`、`refinement_packet.py` 等 10 个文件在 `scripts/research/sealed_holdout_rerun.py` `PRODUCTION_FILES` 里,另有 dataset `frozen_scoring.files` 12 个遗留文件;快速门的 `tests/test_rectification_validation_integrity_gate.py` 按文件字节哈希核对冻结记录。BUG-1047 的探针大运表复用(同机 A/B 7 个场景逐字节一致,refresh 61 候选 2.33 → 1.84 s)只改了 `event_probes.py`,就让该门禁 4 条失败。
@@ -0,0 +1,78 @@
# PROGRESS:太阳返照时刻对齐(出生太阳同参考系 + 保留秒)— 2026-10-03
- 任务书:`docs/tasks/TASK-annual-return-frame-20261003.md`。执行方:Claude 直接执行(产品负责人 2026-10-03 晚确认)。
- 分支 `codex/annual-return-frame-20261003`,工作树 `.worktrees/annual-return-frame-20261003`,基线 `origin/staging` = `3c27c38e`(第三批已合入,含 BUG-1216~1219)。
- 上游参照:`yinduzhanxing` `7cc6425d`、`46ed9399`(只读)。
- BUG 编号:开工时最大号 BUG-1219;本单 BUG-1220。
- 预检:`scripts/pre_work_check.py` → `status: pass`。磁盘余 44G。
## 结论
| 任务 | 结果 |
| --- | --- |
| T1 出生太阳与求解器同参考系 | 完成。PyJHora 对照 3 例:0.000 / 0.021 / 0.006 分钟(门槛 0.5) |
| T2 年盘保留秒 | 完成。年盘时刻与返照差 < 1 秒 |
| T3 下游与 golden | 咨询两份 golden 用真实引擎重采,逐字节不变;243 张年盘对比见下 |
| T4 记录 | BUG-1220、ERR-109 复发说明、CHANGELOG、本文件、README |
## 改动
- `scripts/solar_return.py:calc_solar_return_chart`:
- 目标经度 = `_get_sun_lon_jd(birth_jd_ut)`,拿不到才退回出生盘 `degree_raw`;
- `solar_return.target_sun_frame` 标明用了哪一种;
- 起年盘传 `second=sr_dt_ut.second`。
- 新测试 `tests/test_solar_return_frame.py`(5 条)。换回旧代码时 5 条全部失败。
- 未搬:上游 `position_mode`、`solar_return_time_offset_seconds`、`node_longitude_offset_arcseconds`、`annual_house_system_for_sahams`(研究回放参数)。
## PyJHora 对照(只存数字,Lahiri)
| 例 | PyJHora 返照 JD(UT) | 本站修后 | 差(分钟) | 修前差(分钟) |
| --- | --- | --- | --- | --- |
| Rath 标准盘 2025 | 2460895.0424092 | 2460895.0424094 | 0.000 | −6.3 |
| 乔布斯 2012 | 2455983.2569387 | 2455983.2569242 | −0.021 | +7.2 |
| 齐达内 2002 | 2452449.2759534 | 2452449.2759490 | −0.006 | +6.3 |
## 243 张年盘前后对比
9 位公开名人(`tests/test_pancha_mahapurusha_main_detector.py:FIGURES`)× 2000–2026 年,Lahiri,年主与 Mudda 用第三批的产品函数。
| 指标 | 变化 |
| --- | --- |
| 返照时刻最大偏移 | 7.2 分钟 |
| 年盘上升星座 | 13/243 |
| 昼夜判定 | 0/243 |
| 年主 | 11/243 |
| Mudda 起运星 | 0/243(第三批后与年盘无关,作为回归证据) |
| 名人 | 年 | 时刻偏移(分钟) | 年盘上升 | 年主 |
| --- | --- | --- | --- | --- |
| 乔布斯 | 2012 | −7.2 | 金牛 → 白羊 | 金星 → 太阳 |
| 乔布斯 | 2015 | −7.2 | 摩羯 → 射手 | 金星 → 太阳 |
| 乔布斯 | 2024 | −7.2 | 双子 → 金牛 | 土星 → 金星 |
| 奥巴马 | 2009 | +3.4 | 金牛 → 双子 | 月亮 → 水星 |
| 奥巴马 | 2024 | +3.4 | 双鱼 → 白羊 | 月亮 → 土星 |
| 梦露 | 2007 | +7.1 | 白羊 → 金牛 | 月亮 → 火星 |
| 琵雅芙 | 2009 | −4.9 | 射手 → 天蝎 | 土星(不变) |
| 琵雅芙 | 2021 | −4.9 | 摩羯 → 射手 | 火星 → 木星 |
| 卡罗 | 2001 | +6.5 | 狮子 → 处女 | 水星 → 月亮 |
| 卡罗 | 2012 | +6.5 | 双子 → 巨蟹 | 土星 → 金星 |
| 小布什 | 2007 | +6.8 | 摩羯 → 水瓶 | 月亮 → 太阳 |
| 小布什 | 2023 | +6.8 | 双鱼 → 白羊 | 土星(不变) |
| 齐达内 | 2002 | −6.3 | 射手 → 天蝎 | 木星 → 金星 |
## 验收
| 项 | 结果 |
| --- | --- |
| 新测试 + 年运相关定向(`test_solar_return_frame`、`test_solar_return_timezone`、`test_annual_tajika_pack`) | 26 passed |
| `test_report_english_dictionary` + Tajika / Mudda / 年主四个文件 | 208 passed |
| 快速门 Python 段 | 1047 passed、1 skipped、0 failed |
| 前端 `npm test`(Node 22.14) | 4931 个,fail 24、cancelled 0;失败名单与基线 `3c27c38e` 逐条相同 |
| v5 77 例 | 三段结果(分钟级打分、候选范围宽度、采集无效停止回放)与基线 `3c27c38e` 逐项相同,只有耗时字段不同 |
| `tsc` / lint / build | 未跑:本单没有前端源码改动,只有前端 golden 重采且逐字节不变 |
| 历史报告可打开 | 不涉及:旧报告存的是生成时结果,不重算;未改迁移与 Skill 版本 |
## 环境缺口
- 名人生平回测:本机无模型凭据,未跑(本单不改提示词)。
- 真机:部署后在 staging 打开一张年运数据卡或参考版报告,看年运段能出;数值变化只影响约 5% 的年份,不另立真机清单。
+1 -1
View File
@@ -416,4 +416,4 @@
| `TASK-astrologer-rulings-batch1-20261003.md` | `PROGRESS-astrologer-rulings-batch1-20261003.md` | 占星师定稿 2026-10-03 第一批(低风险批):五大人格、Kemadruma 各只留一个主检测器;Shadbala 达标在前、分档标「网站分档」;Bhava Bala 改正式三分量;罗计不设本宫、宫主链固定传统主星;8 星制 Karaka 排位按 BPHS 修正;八分法净化值进报告/卡片(过运不动);落陷取消改条件清单;Tajika 年主如实标注。乙3 Narayana、甲1、甲8/9 不在本单(BUG 从 1204 起) | 已验收,已部署 staging `e7eeb404`(Claude 补修确认门测试 `14d5c761`) | 分支 `codex/astrologer-rulings-batch1-20261003`(BUG-1204~1212;校正分数不变、未升版本) |
| `TASK-astrologer-rulings-batch2-20261003.md` | `PROGRESS-astrologer-rulings-batch2-20261003.md` | 占星师第四轮:五大人格 D9 落陷只算减弱;Tajika 年主按 Panchadhikari 选法(移植上游 c7117f92);Narayana 实现 Sanjay Rath 版并用原书三张样表验收,不接打分;旧 Narayana 如实标注;Rath 版替换校正 Narayana 的 v5 试算(只研究)。产品选方案 A(BUG 从 1213 起) | 已验收,已部署 staging `503c1190`(T3 起运宫未过原书样表,按红线未接、T5 未做;Claude 补修英文对照 `dda966e6`) | 分支 `codex/astrologer-rulings-batch2-20261003`(BUG-1213~1215;校正分数不变、未升版本) |
| `TASK-astrologer-rulings-batch3-20261003.md` | `PROGRESS-astrologer-rulings-batch3-20261003.md` | 占星师第五轮:Mudda 改出生月宿起排(与年主脱钩);年主改 Tajika 正式五分力量;Rath 版 Narayana 按原书 7 级强弱、计都反转、子运起点重做并以三张样表验收,过了再并列展示与 v5 试算(研究分支);土计同宫计都优先(产品选 A);上游两个 bug 记录(BUG 从 1216 起) | 已实现待验收(T1–T4 完成;T3 按修订后红线 3 通过;T5 v5 试算在研究分支,未合入) | 分支 `codex/astrologer-rulings-batch3-20261003`(未推送;BUG-1216~1219;校正分数不变、未升版本);研究分支 `codex/narayana-rath-rectification-trial-20261003`(未推送、不合入) |
| `TASK-annual-return-frame-20261003.md` | `PROGRESS-annual-return-frame-20261003.md` | 太阳返照时刻错位:出生太阳与求解器不同参考系(差 15″≈6 分钟)+ 年盘丢秒;243 张年盘里年盘上升变 13、年主变 11;搬上游 7cc6425d/46ed9399 两处,PyJHora 对照 ≤0.5 分钟;排在第三批之后(依据 `docs/research/shared_engine_reuse_audit_2026_10_03.md`) | 已确认,第三批合入后开工 | 分支 `codex/annual-return-frame-20261003` |
| `TASK-annual-return-frame-20261003.md` | `PROGRESS-annual-return-frame-20261003.md` | 太阳返照时刻错位:出生太阳与求解器不同参考系(差 15″≈6 分钟)+ 年盘丢秒;243 张年盘里年盘上升变 13、年主变 11;搬上游 7cc6425d/46ed9399 两处,PyJHora 对照 ≤0.5 分钟;排在第三批之后(依据 `docs/research/shared_engine_reuse_audit_2026_10_03.md`) | 已实现(BUG-1220),待部署核对 | 分支 `codex/annual-return-frame-20261003` |
+16 -8
View File
@@ -384,6 +384,16 @@ def calc_solar_return_chart(
moon_data = birth_chart.get('planets', {}).get('Moon', {})
birth_moon_lon = moon_data.get('degree_raw', moon_data.get('degree')) if isinstance(moon_data, dict) else None
birth_jd_ut = birth_jd
# BUG-1220: the target longitude must come from the same frame the
# solver iterates in (`_get_sun_lon_jd`, swe sidereal flags). The chart's
# Sun differs by up to ~17" (about 7 minutes of return time). Upstream
# yinduzhanxing 7cc6425d.
solver_frame_sun = _get_sun_lon_jd(birth_jd_ut, ayanamsa_name=ayanamsa_name)
target_sun_frame = 'swe_sidereal_flags'
if solver_frame_sun is None:
target_sun_frame = 'birth_chart_degree_raw'
else:
birth_sun_lon = solver_frame_sun
# Step 2: 计算太阳返照精确时刻
sr_result = find_solar_return_ut(
@@ -393,6 +403,7 @@ def calc_solar_return_chart(
)
if 'error' in sr_result:
return {'error': sr_result['error'], 'solar_return': sr_result}
sr_result['target_sun_frame'] = target_sun_frame
sr_jd_ut = sr_result['jd_ut']
sr_dt_ut = sr_result['dt_ut']
@@ -406,17 +417,14 @@ def calc_solar_return_chart(
'utc_offset': annual_tz, 'timezone_id': local_timezone_id}
# Step 3: 以返照时刻计算星盘
# compute_chart_data 接受 year/month/day/hour/minute
sr_year, sr_month, sr_day = sr_dt_ut.year, sr_dt_ut.month, sr_dt_ut.day
sr_hour = sr_dt_ut.hour + sr_dt_ut.minute / 60.0 + sr_dt_ut.second / 3600.0
sr_hour_int = int(sr_hour)
sr_minute_int = int((sr_hour - sr_hour_int) * 60)
# Keep the solver's seconds (BUG-1220, upstream 46ed9399): truncating
# to the minute moves the annual Ascendant by up to ~0.25 deg.
sr_chart, sr_asc_idx, sr_jd, sr_ayanamsa = compute_chart_data(
sr_year, sr_month, sr_day,
sr_hour_int, sr_minute_int,
sr_dt_ut.year, sr_dt_ut.month, sr_dt_ut.day,
sr_dt_ut.hour, sr_dt_ut.minute,
annual_lat, annual_lon, 0, # UT 时间,时区=0
'mean',
second=sr_dt_ut.second,
ayanamsa_name=ayanamsa_name,
)
if sr_chart is None:
+81
View File
@@ -0,0 +1,81 @@
"""Solar-return instant in the solver's frame, annual chart with seconds (BUG-1220).
The return is the instant the Sun regains its natal sidereal longitude. The
website took the natal Sun from ``compute_chart_data`` while
``_find_solar_return_swe`` iterates on ``_get_sun_lon_jd`` (swe sidereal
flags); the two differ by up to ~17", which moved the return by up to ~7
minutes and changed the annual Ascendant sign in 13 of 243 public-figure
years. The annual chart was also cast at the truncated minute. Upstream
yinduzhanxing 7cc6425d / 46ed9399.
Reference instants were computed once with PyJHora ``drik.next_solar_date``
(Lahiri) as an external reference engine; only the resulting numbers are kept
here, PyJHora is not imported. Charts: Sanjay Rath's "standard nativity" and
two public figures from references/real_case_calibration.
"""
from __future__ import annotations
import contextlib
import io
import sys
from pathlib import Path
import pytest
ROOT = Path(__file__).resolve().parents[1]
SCRIPTS = ROOT / "scripts"
if str(SCRIPTS) not in sys.path:
sys.path.insert(0, str(SCRIPTS))
import solar_return # noqa: E402
try: # pragma: no cover - environment probe
import swisseph # noqa: F401
HAS_SWE = True
except Exception: # pragma: no cover
HAS_SWE = False
pytestmark = pytest.mark.skipif(not HAS_SWE, reason="swisseph required")
# (birth y, m, d, h, min, lat, lon, tz), target year, PyJHora return JD (UT)
PYJHORA_RETURNS = {
"rath_standard_nativity_2025": ((1963, 8, 7, 21, 15, 21.4667, 84.0167, 5.5), 2025, 2460895.0424092),
"steve_jobs_2012": ((1955, 2, 24, 19, 15, 37.7749, -122.4194, -8.0), 2012, 2455983.2569387),
"zinedine_zidane_2002": ((1972, 6, 23, 3, 0, 43.3, 5.4, 1.0), 2002, 2452449.2759534),
}
def _chart(birth, target_year):
with contextlib.redirect_stdout(io.StringIO()):
return solar_return.calc_solar_return_chart(*birth, target_year, "lahiri")
@pytest.mark.parametrize("case", sorted(PYJHORA_RETURNS))
def test_return_instant_matches_the_external_reference(case: str) -> None:
birth, target_year, reference_jd = PYJHORA_RETURNS[case]
result = _chart(birth, target_year)
assert result["solar_return"]["target_sun_frame"] == "swe_sidereal_flags"
# Before BUG-1220 these were 6-7 minutes apart.
assert abs(result["solar_return"]["jd_ut"] - reference_jd) * 1440 <= 0.5
def test_target_longitude_is_the_natal_sun_in_the_solver_frame() -> None:
birth, target_year, _ = PYJHORA_RETURNS["rath_standard_nativity_2025"]
with contextlib.redirect_stdout(io.StringIO()):
from jyotish_engine import compute_chart_data
natal, _asc, natal_jd, _ay = compute_chart_data(*birth, "mean", ayanamsa_name="lahiri")
solver_sun = solar_return._get_sun_lon_jd(natal_jd, ayanamsa_name="lahiri")
chart_sun = natal["planets"]["Sun"]["degree_raw"]
assert abs(chart_sun - solver_sun) * 3600 > 10 # the two frames really differ here
result = _chart(birth, target_year)
assert abs(result["solar_return"]["sun_lon"] - solver_sun) * 3600 < 1
def test_annual_chart_keeps_the_seconds_of_the_return() -> None:
birth, target_year, _ = PYJHORA_RETURNS["rath_standard_nativity_2025"]
result = _chart(birth, target_year)
info = result["chart"]["birth_info"]
return_dt = result["solar_return"]["dt_ut"]
assert (info["hour"], info["minute"], info["second"]) == (return_dt.hour, return_dt.minute, return_dt.second)
assert abs(info["julian_day"] - result["solar_return"]["jd_ut"]) * 86400 < 1