feat(annual): annual data package v1 (#152 ruling, phase 1 of #153)

POST /api/annual with mode=package (or /api/annual/package) returns one
versioned package built from the existing producers: year charts with
ascendant/lord, Muntha, Panchadhikari year lord, annual strength, Sahams,
Tajika yogas, Tripataki, Mudda, native Patyayini, Tajika monthly charts;
natal Vimshottari MD/AD/PD and Narayana (legacy + Rath); slow-planet
ingresses, stations, retrograde loops and natal-point crossings; Tithi
Pravesha as an independent view. Report range, year-chart validity and each
period system's own year are separate, every boundary is an exact instant
with timezone, and every block carries computed/blocked/not_applicable with
a reason. The default /api/annual response is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DbBXoyeZrYn6ryNcSe5Y2Y
This commit is contained in:
2026-11-01 17:07:54 +08:00
co-authored by Claude Opus 5.5
parent 52f85a793f
commit 340f0f2b85
8 changed files with 6319 additions and 0 deletions
+98
View File
@@ -0,0 +1,98 @@
# 年度数据包 v1(annual_package_version = 1)
依据:Gitea #152 裁定(jesse 2026-11-01 确认)「落地前要先做的四件事」。这是 #153(年运报告)和 #154(入口遮罩页)的数据接口。只做计算和数据搬运,不含解读文字,不建候选窗口。
## 入口
- `POST /api/annual`,请求体加 `"mode": "package"`;或 `POST /api/annual/package`(同一个处理函数)。
- 不带 `mode` 的 `/api/annual` 仍返回原来的基础太阳返照报告(`report`),形状不变;只多传了 `node_mode`(默认 mean,结果不变)。
- 代码:`scripts/annual_package.py`(`normalize_request`、`build_annual_package`),API 处理在 `scripts/jyotish_api_server.py::_compute_annual_package`。前端合同:`frontend/src/lib/annual-package-contract.ts`(zod + 映射,无界面)。
### 请求字段
| 字段 | 说明 |
| --- | --- |
| `year` `month` `day` `hour` `minute` `second` | 出生时间,秒会传到所有生产器 |
| `lat` `lon` `tz` | 出生地;`tz` 缺省时按经纬度推算 |
| `timezone_id` / `timezone` | 可选 IANA 时区;有则返照本地时间、报告范围按该时区(含夏令时) |
| `ayanamsa` | 岁差,默认同其他接口 |
| `node_mode` | `mean` / `true` |
| `report_range` | `{kind, year?, start_date?}`,`kind` 为 `birthday_year`(生日年度)/ `calendar_year`(公历年)/ `next_12_months`(未来十二个月) |
| `observation_date` | 报告观测日期,默认今天;`next_12_months` 不给 `start_date` 时从这天起 |
| `return_location` | `{mode: "birth_place"}`。v1 只开放出生地,其它值返回 400;字段保留,以后加所在地对照不改形状 |
## 响应结构
`{success, endpoint: "annual", mode: "package", annual_package_version: 1, package}`,`package` 的键序:
1. `schema` = `jyotish.annual_package.v1`、`annual_package_version`
2. `parameters`:出生时刻(UTC、出生固定偏移下的本地时间)、岁差、交点、宫制、回归地点、观测日期、本命盘经度(`natal_chart`)
3. `report_range`:报告范围
4. `year_charts[]`:覆盖报告范围的每张年盘
5. `natal_periods`:`vimshottari`、`narayana`(`legacy` + `rath`)
6. `slow_transits`:木星、土星、罗睺、计都
7. `independent_annual_views.tithi_pravesha`:独立年度视角
8. `computation_boundaries`:时间语义与计算边界的说明
9. `not_included`:没放进来的项和原因
10. `block_index`:所有块的路径、状态、生产器、原因;`blocked_count`
### 块状态
每个块都有 `status`:`computed` / `blocked` / `not_applicable`,以及 `producer`;非 `computed` 一定有 `reason`。生产器原有的状态(如 `partial_verified`、`parameter_sensitive`)放在 `producer_status`,不提升。生产器报错、返回 blocked、两个年运生产器结果冲突(`producer_conflict:...`,两个值都在 `conflict_values`)都写成 blocked。某个块 blocked 不影响整个响应。
## 时间语义(四种时间分开表示)
所有边界都是一个“时刻”:`{utc, local, utc_offset, timezone_id, jd_ut}`。`local` 用回归地点的时区(v1 即出生地;给了 IANA 就按当时的规则取偏移,否则用出生固定偏移)。出生时刻本身用出生固定偏移显示,因为排盘按这个偏移读出生时钟(例:上海 1986–1991 年有夏令时)。
1. **报告范围** `report_range`:`start` 含、`end` 不含。
- 生日年度:本次太阳返照到下次太阳返照;
- 公历年:当地 1 月 1 日 0 点到次年 1 月 1 日 0 点;
- 未来十二个月:`start_date` 当地 0 点到十二个日历月后(月底按月末截)。
- `year_chart_years` 列出与范围重叠的年盘,跨生日时是相邻两张;`crosses_birthday`、`birthday_boundaries` 给出换年盘的时刻。
- `observation`:观测日期、在范围内 / 前 / 后,回看起点(默认三个月)。
2. **年盘有效期** `year_charts[].validity`:本次返照(含)到下次返照(不含),`end_source = next_solar_return`;`in_report_range` 是与报告范围的交集。
3. **各运限自己的年制**:Mudda 365.25 天、Patyayini 固定 365.25 天,都从返照时刻起;Vimshottari 365.25 天,从出生时刻起;Narayana 在年龄轴上,日期 = 出生时刻 + 年龄 × 365.25 天(与引擎算当前年龄的口径一致)。各行保留原始起止,`in_report_range` 是裁到报告范围的部分,不拉伸到返照年(返照年约 365.256 天,Mudda / Patyayini 的年末比下次返照早几分钟)。
4. **计算边界**:生产器在本地时钟上加天数的(Mudda、Patyayini、Vimshottari),按“从锚点时刻起经过的时间”换成 UTC,跨夏令时也对。月盘第 12 张止于下次返照。
候选窗口不在本期,由 #153 用上面这些时刻去建。
## 各块由谁计算
| 块 | 生产器 |
| --- | --- |
| 年盘、上升及上升主、宫位 | `cmd_solar_return` → `solar_return.solar_return_full_report`(结果交给 `annual_tajika_pack.build_annual_tajika_pack`) |
| Muntha、Muntha 主落宫 | `solar_return._muntha_from_natal_ascendant`(经年运包);落宫按年盘整宫数 |
| 年主 | `tajika_year_lord.select_annual_year_lord`(Panchadhikari,经年运包冲突门) |
| 年度力量 | `tajika.calc_tajika_strength_layers`:Panchavargiya、Harsha Bala;不是本命 Shadbala |
| Sahams | `tajika.calc_all_sahams`(原样保留公式来源与昼夜依据) |
| Tajika Yoga | `tajika_named_yoga_authority`(只有结构,不翻译成事件) |
| Tripataki | `annual_native.tripataki_chakra` |
| Mudda | `tajika_mudda.annual_mudda_dasha`(出生月宿起运,与年主无关) |
| Patyayini | `annual_native.patyayini.build_native_patyayini`(`tajika-annual` skill 指定的年度原生入口;不用 `extended_dashas.py` 的等长旧函数),输入八点的经度一并给出 |
| Tajika 月盘 | `annual_native.monthly_charts.build_monthly_charts`:太阳每前进 30° 一张,不是公历月,也不是月亮回归盘 |
| Tithi Pravesha | `annual_native.tithi_pravesh.build_tithi_pravesh`,独立单列:起点规则不同于太阳回归,地点用出生地和出生固定偏移(生产器自己的规则);找到几个候选就列几个,不替用户选 |
| Vimshottari MD/AD/PD | `jyotish_engine.cmd_dasha`(出生时刻起运) |
| Narayana | `narayana_dasha.narayana_dasha_full_report`(产品现用的旧算法,`used_by_product_timeline: true`)和 `narayana_rath`(Sanjay Rath 版,占星师口径,并列展示,`false`);只列与范围重叠的 MD / AD |
| 慢星行运 | 位置取 `jyotish_engine._calc_sidereal_planets_for_jd`(与本命盘同一参考系,跟随交点和岁差);每天 12:00 UT 取样,再二分到 1 秒以内。给出:范围起点的位置(从上升和月亮各算宫)、换宫、停滞、与本命十点(上升 + 九曜)的精确合相经过及次数、木星 / 土星的逆行回路(停滞时刻与经度、回扫弧段、前后影区进出时刻、落在弧段内的本命点及每次经过) |
## 本期修的参数缺口(BUG-1501)
`solar_return.calc_solar_return_chart` 排出生盘和年盘时交点口径写死为 `mean`,`node_mode=true` 传不到年盘,年盘罗睺 / 计都和 Tripataki 总是平均交点。现在 `calc_solar_return_chart` / `solar_return_full_report` 接受 `node_mode`(默认 mean),`cmd_solar_return` 和 `cmd_tajika` 都会传。
## 没放进来的(`not_included`)
- Varsha Narayana:本仓没有原生生产器(上游 yinduzhanxing 有,未同步)。
- Yoga / Nakshatra Pravesha:上游只有研究版,裁定不进报告。
- 外部引擎回放(PyJHora):只在完整数据版的子进程里跑,这里不执行。
- 所在地返照:v1 只开放出生地。
- 行运的 BAV / SAV、Vedha、Sade Sati 阶段:报告阶段做;BAV / SAV 来自 `/api/ashtakavarga`。
- 候选窗口:#153。
- Real Case Calibration:按裁定仍是 blocked,不作准确率声明。
## 已知限制
- Narayana 旧算法的 AD 年龄由生产器保留 4 位小数,换成日期约有 53 分钟误差;MD 年龄是整年相加,没有这个问题。旧算法只排一轮,范围超出这一轮时写 blocked。
- 慢星事件按天取样,一天内相继发生的两件事可能合并(慢星实际不会出现)。真交点每天取样。
- 回路的影区边界落在扫描窗口(范围前后各 220 天)之外时写 `null`,并在 `shadow_note` 里说明。
- 返照、Mudda、Patyayini 的时刻都按返照求解器的精确时刻(JD)对齐;生产器内部用的是截到秒的时间,差不到 1 秒。
- 一个公历年的完整包大约 350 KB(大头是 Mudda / Patyayini 的子运行,每行有三个时刻)。