Splits the remaining work into three briefs that own disjoint files so agents can run them in parallel. The qizheng brief is widened to own scripts/jyotish_api_server.py outright and register all three read-only endpoints (/api/qizheng, /api/western, /api/ephemeris_events), because the chart page needs a tropical natal route that no path exposes today and the ephemeris page needs ingress/station scanning that /api/transit does not provide. The chart-page brief owns app-sidebar.tsx and adds both nav entries; the ephemeris-page brief owns only its own route. Checking the tree changed one plan: vedic-chart-svg.tsx is also imported by rectification-board, so the component stays where it is instead of moving to a shared layer. BUG ranges are pre-allocated per brief to keep parallel work from colliding, and the panchanga Lahiri/Raman split found while prototyping is filed as investigating rather than fixed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JUei7K13cYxLHE3Axe4A45
22 KiB
TASK-qizheng-native-chart-20260915 · 接入原生七政四余排盘,并开出三个只读计算端点
2026-09-15 修订(立单当天):本单扩了范围。原版只做
/api/qizheng;核对后发现 P0 星盘页与 P1 星历页还各缺一个后端端点,而三者都要改scripts/jyotish_api_server.py。为了让后续两份前端单能真并行,把三个端点的注册全部收进本单,由本单独占该文件的写权。两份前端单一行后端代码都不碰。
基线
- 代码基线:
origin/staging=2d7698ea(fix(chat): keep stored rectification titles on open)。 - 上游来源:
/workspace/yinduzhanxing,分支codex/add-birth-time-rectification-skill,commita911c890(feat: add native qizheng chart adapter,2026-09-15 14:14 +0800)。- 注意:上游
main仍是a6f47abd(2026-09-03),即我们 09-03 那轮已同步的 commit。本单要的东西只在这条分支上,不在 main。 - 同分支另有
6c27aab6 fix: harden report export and validation gates与8bba9cb5 docs: record 2026-09-15 governance audit,不在本单范围,不要顺手带进来。
- 注意:上游
- Skill 版本:
6.9.16,本单不 bump(不新增解读口径,只新增计算与只读接口)。 - 开工前置检查已在立单时跑过:
scripts/pre_work_check.py --remote-timeout 8 --command-timeout 45全绿(python_runtime / fragment_scan / external_engine_adapters / remote_visibility / focused_tests 均 ok)。执行方开工时必须重跑。
上游带来了什么
a911c890 的实质不是继续做考据,而是vendored 了一个开源引擎:
| 文件 | 说明 |
|---|---|
scripts/qizheng_chart_engine.py |
168 行纯适配器,subprocess 调 vendored CLI |
vendor/stem-branch/dist/cli.cjs |
45,963 行 / 2.68 MB 单文件 bundle |
vendor/stem-branch/{LICENSE,README.md,package.json} |
@4n6h4x0r/stem-branch 0.8.0,Apache-2.0 |
scripts/jyotish_api_server.py |
+10 行,挂 /api/qizheng |
许可证是本单能立的前提:Apache-2.0 是 permissive,与本仓 MIT 兼容(对照:PyJHora 是 AGPL,只能黑盒对数,见 docs/research/open_source_integration_priority_table_2026_07_03.md)。Apache-2.0 §4 要求保留 LICENSE 与 attribution,任务 1 里必须落实。
立单前的实测(在本机跑通,不是读代码推断)
用示例出生资料 1990-04-09T13:24:00+08:00 / 31.19N 121.44E:
bodies: 七政(日月水金火木土)+ 四余(罗睺 计都 月孛 紫炁),共 11 曜
太阳 宿度 177.94 奎 7.44° 亥宮 陷
罗睺 宿度 112.09 女 5.09° 丑宮 平
紫炁 宿度 33.34 房 0.34° 卯宮 平
palaces: 12(地支宮 + 人事宮 + 所辖宿 + 占星)
ascendant: 星宿 · 午宮 aspects: 27 dignities: 11 曜全有
宿度自角宿初度起算,内部自洽(太阳 177.94 = 奎宿累计 + 7.44)。
事故实证
以下三条全部是实跑复现的,不是猜测。复现命令见每条末尾(CLI 指 vendor/stem-branch/dist/cli.cjs)。
实证 1 · 四柱时柱按 UTC 墙钟计算(BUG-700)
vendored CLI 的 --pillars 读的是 UTC 墙钟,不是出生地本地时间:
| 传入 | 引擎回的时柱 | 应为 |
|---|---|---|
1990-04-09T13:24:00+08:00 |
丁卯(卯时 = 05–07 时) | 辛未 |
1990-04-09T13:24:00(裸) |
辛未 | 辛未 ✓ |
1990-04-09T13:24:00+00:00 |
辛未 | — |
而上游适配器 scripts/qizheng_chart_engine.py 的 _iso_local() 带偏移量输出(f"...{sign}{offset_hours:02d}:{offset_minutes:02d}")。也就是说:一旦把 --pillars 接到同一个适配器上,每个中国用户的时柱都会错 8 小时,午夜前后日柱也会跟着错。
--seven-governors 不受影响:天文量要的正是正确的 UTC 时刻,带偏移量是对的。
node CLI --date "1990-04-09T13:24:00+08:00" --lat 31.19 --lng 121.44 --pillars --json
node CLI --date "1990-04-09T13:24:00" --lat 31.19 --lng 121.44 --pillars --json
这条与 docs/BUG_HISTORY.md 的 BUG-245 / BUG-905 是同一族防复发条款(「所有星盘计算入口必须统一补算历史 offset」),必须在新记录里关联。
实证 2 · 计都派别被写死,且未暴露(BUG-701)
引擎回 ketuMode: "apogee"。实测计都落在 亢 15.46°,月孛 亢 14.23°——两者只差 1.2°,不是罗睺的对点(罗睺 女 5.09°,对点应在 133° 附近)。
这是真实的派别分歧,换一派整张盘变。上游适配器的 boundary 文案提了紫气 / 庙旺 / 限法差异,唯独没提 ketu mode,请求侧也没有参数可以选。
实证 3 · boundary 文案与实际输出不符(BUG-702)
上游 _normalize() 的 boundary 写:
「已生成完整七政四余本命结构:七政、四余、二十八宿、十二宫、命宫、相位、神煞和当前庙旺字段。」
实测:
starSpirits: 0—— 神煞是空的。dignities11 曜里,除太阳记「陷」外全部返回「平」。
第二条对得上上游自己的台账:references/oracle/qizheng_runtime_truth_closure_status_2026_08_29.json 的 promotion_decision.may_update_runtime_dignity_table = false,runtime_promotable_count = 0;qizheng_positive_layer_runtime_gate_2026_08_30.json 63 格全部 blocked;十一体 × 十二宫共 132 格的庙旺表只拿到 9 格直接证据。
庙旺这一列现在没有可用的判定,只能当占位。
根因
- vendored 引擎把「天文时刻」和「历法干支」用了同一个时间入口,前者要 UTC、后者要本地民用时,混用导致时柱错位。
- 适配器把派别选择(
ketuMode、siderealMode)当成引擎内部细节透传,没有提升为产品参数。 - boundary 文案是按引擎能力清单写的,不是按本次实际输出写的,于是把空字段说成已生成。
决策记录
产品负责人在 2026-09-15 的对话中授权:
- 接入七政四余原生排盘,走 vendored
stem-branch(Apache-2.0)路线,不再自己做 132 格考据。 - 本单只做引擎与只读接口,前端另出一单(原因见「范围边界」)。
- 不做八字 / 四柱。
--pillars存在实证 1 的缺陷,本轮不修、不调用;只把它记成红线与 BUG,避免后来者踩。 - 计都派别与宿度起算口径必须让用户看得见,按岁差(
ayanamsa)既有的做法处理成显式参数,不接受引擎默认静默生效。 - 庙旺不得进入任何结论。它可以出现在响应里,但必须带
unclosed标记;解读层、报告层、咨询层一律不得引用。
本单不推翻既有红线。特别地,AGENTS.md §6「scripts/jyotish_api_server.py must not grow」继续有效,见任务 3 的硬上限。
硬红线
--pillars/--luck/--polaris/--qimen/--liuren等其余子命令,本单一律不得调用。 vendored CLI 里有它们,但只有--seven-governors在本单验收范围内。- 绝不把带时区偏移的 ISO 字符串喂给需要本地民用时的子命令。 若将来要做四柱,必须先解决实证 1,并在
docs/BUG_HISTORY.md关联 BUG-700 / BUG-245 / BUG-905。 - 宿度不是恒星黄经。 引擎的
siderealLon自角宿初度起算,与本仓 Raman / Lahiri 恒星黄经、与西洋回归黄道都不是同一套。响应里必须显式标注坐标系,任何界面都不得做三套之间的度数换算,也不得把两套结果叠加成「双重印证」。 - 庙旺(dignities)不得进入结论。 见决策记录 5。
scripts/jyotish_api_server.py不得新增 handler 主体,只允许薄注册,见任务 3。frontend/src/app/page.tsx在origin/staging上是 1951 行,不得再增长;本单不碰前端。- 不得顺手升级依赖、不得顺手修不在本单里的 warning;发现了写进
BLOCKED.md或进度记录。 - 不得把
/workspace/yinduzhanxing当作运行主仓;从它那里只取a911c890的文件内容,不引用它的路径。
范围边界
本单做:vendored 归档、镜像 Node runtime、适配器、只读接口、测试、Bug 记录。
本单不做:任何前端文件。
并行与文件归属。同期还有两份前端单,三份单之间没有任何共享代码文件,可以并行开工:
| 文件 / 目录 | 归属 |
|---|---|
vendor/**、NOTICE、deploy/** |
本单 |
scripts/qizheng_chart_engine.py、scripts/ephemeris_events.py |
本单 |
scripts/jyotish_api_server.py |
本单独占。两份前端单一行都不许改 |
tests/**(本单新增的三个文件) |
本单 |
frontend/src/app/chart/**、frontend/src/app/api/chart-view/**、frontend/src/components/app-sidebar.tsx |
TASK-chart-page-20260915 |
frontend/src/app/ephemeris/**、frontend/src/app/api/ephemeris/**、frontend/src/lib/ephemeris-*.ts |
TASK-ephemeris-page-20260915 |
docs/BUG_HISTORY.md、CHANGELOG.md、frontend/DESIGN.md |
三方都写,只许追加各自小节,合入顺序 本单 → chart-page → ephemeris-page |
前端对本单的依赖是运行时依赖,不是代码依赖:端点没上线时,前端对应区块渲染静态说明,端点合入后自然点亮。
原型(五个 Tab 的完整形态、中宫排盘参数卡、三套坐标系的边界文案)已经画好,两份前端单都会引用它。
任务分解
任务 1 · vendored 归档与 API 镜像 Node runtime
这是后续全部任务的阻塞项,必须先完成。
- 从上游
a911c890取入以下四个文件,路径保持一致:vendor/stem-branch/LICENSE、vendor/stem-branch/README.md、vendor/stem-branch/package.json、vendor/stem-branch/dist/cli.cjs。 - 新增仓库根
NOTICE(或在既有归属文件中追加):注明@4n6h4x0r/stem-branch0.8.0、Apache-2.0、上游地址与取入 commit。Apache-2.0 §4 要求保留 LICENSE 与 attribution,这一条是合规必需,不是可选。 deploy/railway-api.Dockerfile:装 Node.js 运行时(当前基底是python:3.12-slim,全文无 node / npm)。只要运行时,不要 npm、不要构建工具链;装完清理 apt 列表,与既有build-essential的 purge 写法保持一致风格。- 同文件新增
COPY vendor ./vendor(现有 COPY 列表在第 17–22 行区间:SKILL.md mcp_server.py/assets/jyotish_vedic/references/scripts/skills)。 deploy/gated-paths.txt:新增vendor/**。当前清单里没有它,不加的话 vendored 引擎的任何改动都不会触发门禁、不会重建镜像。
验收标准
- 本地构建 API 镜像成功;容器内
node --version有输出。 - 容器内
python -c "from pathlib import Path; assert Path('/app/vendor/stem-branch/dist/cli.cjs').exists()"通过。 /api/health仍为200且swisseph_available = true。- 镜像体积变化(MB)写进
docs/tasks/PROGRESS-qizheng-native-chart-20260915.md;超过 +150 MB 要说明原因。 - 无 Docker 时:把构建与容器内验证写进
BLOCKED.md,并给出宿主机上node --version与 CLI 实跑的替代证据,不得写成「通过」。
任务 2 · 适配器 scripts/qizheng_chart_engine.py
以上游 168 行版本为起点,但必须做以下修改:
- 派别参数化:
ketu_mode、sidereal_mode从请求体读取,缺省值在模块常量里写死并回写进响应的calculation段。响应必须能让调用方看出这次用的是哪一派。 - boundary 文案按实际输出重写:不得出现「神煞」(实测为空);庙旺必须标注为未闭合,附上
references/oracle/qizheng_runtime_truth_closure_status_2026_08_29.json的口径(132 格只闭合 9 格、runtime_promotable_count = 0)。 - 坐标系标注:响应顶层显式带
coordinate_system: "qizheng_mansion_degrees_from_jiao"(命名可调,但必须一眼看出不是恒星黄经、不是回归黄道)。 - 错误路径:
node不存在、CLI 文件缺失、超时、stdout 非 JSON、CLI 非零退出——五种都要落到结构化错误,不得 500,不得把 stderr 原文整段外泄。 - 保留
raw_engine_output供审计,但确认其中不含任何本机路径或环境信息。
验收标准
- 新增
tests/test_qizheng_chart_engine.py:- fixture 来自真实引擎响应(golden),不得手造形状(AGENTS.md §7.4)。
- 覆盖:11 曜齐全、12 宫齐全、命宫存在、
coordinate_system存在、ketu_mode可被请求覆盖且回写、庙旺带未闭合标记、boundary 不含「神煞」。 - 覆盖五种错误路径各一条。
.venv/bin/python -m pytest tests/test_qizheng_chart_engine.pyfail=0。.venv/bin/python scripts/run_quality_gate.py --profile quick通过。
任务 3 · 三个只读端点薄注册(本单独占 scripts/jyotish_api_server.py 写权)
- 在
scripts/jyotish_api_server.py的路由分支处新增一条elif path == '/api/qizheng':。参照点:origin/staging上elif path == '/api/chart':在 3497 行。 - handler 用既有的
_load_local_module('qizheng_chart_engine')模式(该函数已存在,见 137 / 413 / 1009 行的用法),函数体 ≤ 8 行,只做加载、调用、把领域异常翻成BadRequest。 - 行数硬上限:
origin/staging上该文件是 11313 行,契约上限是 11363(tests/test_api_server_growth_contract.py,baseline 11063 + 300 余量),总余额只有 50 行。三个端点加起来,本轮对该文件的净增 不得超过 32 行。超了就把 handler 再压薄,不要动契约。 - 端点是只读计算,按既有重计算端点的口径纳入
JYOTISH_HEAVY_COMPUTE_CONCURRENCY限流(默认 2,饱和 429)。CLI 子进程 20 秒超时要与限流口径对得上。
3b · /api/western(回归黄道本命盘)
前端「西洋盘」Tab 需要,但当前没有任何路由暴露它:scripts/western_chart_engine.py 的 build_tropical_natal_chart 只在 /api/consult 内部被 _western_evidence_packet_from_body 间接调用(见 jyotish_api_server.py:2205)。
- 新增
elif path == '/api/western':,薄注册直接调build_tropical_natal_chart(不要走build_tropical_western_evidence_packet,那条带route_packet是咨询链概念)。 house_system从请求读,默认P(Placidus),回写进响应。- 响应必须显式带坐标系标注(
zodiac: "tropical"引擎已有,再加一条人类可读的边界句),并写明与恒星黄道的岁差差值。 - handler 函数体 ≤ 8 行。
3c · /api/ephemeris_events(区间换座与停滞)
前端「未来九十天」需要。/api/transit 返回的是对本命的触发点(transit_trigger.search_all_transit_triggers,见 jyotish_api_server.py:9947 一带),不是换座与顺逆停滞,不能复用。
- 新增模块
scripts/ephemeris_events.py(计算主体放这里,不进 api_server):给定起止日期、岁差、交点模式,逐日扫描并返回ingress(进入星座)与station(停滞转顺 / 转逆)两类事件。 - 区间上限与既有过境端点对齐(
transit search range must be <= 730 days,见jyotish_api_server.py:9929),超限返回 400。 - 岁差必须按请求参数走,不得写死(附录里那条 panchanga 写死 Lahiri 的教训就在这)。
- 新增
elif path == '/api/ephemeris_events':,handler 函数体 ≤ 8 行。 - 参考实现口径(立单时用 pyswisseph 实跑过,
1990那组资料 2026-09-15 起 90 天、Raman、六星体得到 14 条事件):逐日取黄经与速度,星座号变化记 ingress,速度符号翻转记 station。
验收标准
.venv/bin/python -m pytest tests/test_api_server_growth_contract.py通过。- 新增
tests/test_qizheng_api_productization.py(上游同名文件可参考,但断言按本仓合同重写):正常请求返回 11 曜 12 宫;缺lat/lon返回 400 而非 500;node 不可用时返回结构化错误。 - 新增
tests/test_readonly_chart_endpoints.py:/api/western返回十一项行星 + 十二宫 cusp + 相位 + 元素模式分布,house_system可覆盖并回写;/api/ephemeris_events在 90 天窗口返回按日期升序的事件列表,两类事件都有,超过 730 天返回 400,岁差参数生效(同一区间 Raman 与 Lahiri 的结果不相同)。 - 新增
tests/test_ephemeris_events.py:模块级单测,golden 来自真实引擎输出。 curl -s -X POST http://127.0.0.1:5200/api/qizheng -d '{"year":1990,"month":4,"day":9,"hour":13,"minute":24,"lat":31.19,"lon":121.44,"tz":8}'的输出贴进进度记录(脱敏不需要,示例资料是虚构的)。
任务 4 · Bug 记录
在同一变更内更新 docs/BUG_HISTORY.md,新增连续编号(起点见下):
-
BUG-700|vendored 引擎四柱时柱按 UTC 墙钟计算。状态
blocked(本轮不修,不调用--pillars)。必须写明三组实测对照、关联 BUG-245 / BUG-905 的「所有星盘计算入口必须统一补算历史 offset」防复发条款,并把红线 2 写进防复发栏。 -
BUG-701|计都派别(
ketuMode: apogee)未暴露为参数。状态resolved(任务 2 参数化后)。 -
BUG-702|适配器 boundary 文案把空的神煞与未闭合的庙旺说成已生成。状态
resolved(任务 2 改写后)。 -
BUG-703|
/api/panchanga_range岁差写死 Lahiri,与账户默认 Raman 不一致。状态investigating(本轮只记录,不修,见附录)。
不得把姓名、出生资料、邮箱、用户/案例 ID、Cookie、JWT、密钥、完整请求体或模型原文写进去(AGENTS.md §5.6)。
让步顺序
资源不够时按这个顺序砍,不得自行调整:
- 先保证 任务 1 + 任务 2 + 任务 3a + 任务 4。没有镜像 Node 与适配器,这一单等于没做。
- 任务 3 的限流接入可以延后,但路由与行数上限不能延后。
- 3b 与 3c 若来不及,可以拆到本单的第二次推送,但必须仍由本单交付——不得让前端单去改
scripts/jyotish_api_server.py。延后时要在进度记录里写明,并通知两份前端单:对应区块先渲染「该数据源尚未上线」。 raw_engine_output的审计字段可以先不落地,改为进度记录里说明。- 不可让步:许可证归档(Apache-2.0 §4)、
vendor/**进 gated-paths、庙旺不得进结论、--pillars不得调用、api_server 行数上限。
开工前置命令
cd /workspace/Jyotisha
git status -sb # 确认分支,主检出常被别的会话切走
git fetch origin --prune
git worktree add -b codex/qizheng-native-chart-20260915 \
.worktrees/qizheng-native-chart-20260915 origin/staging
cd .worktrees/qizheng-native-chart-20260915
python3 scripts/pre_work_check.py --remote-timeout 8 --command-timeout 45
grep -nE "BUG-[0-9]{3}" docs/BUG_HISTORY.md | tail -5 # 核对当前最大号
wc -l scripts/jyotish_api_server.py # 应为 11313,上限 11363
取上游文件(只取内容,不引用上游路径作为运行时依赖):
cd /workspace/yinduzhanxing
git fetch origin codex/add-birth-time-rectification-skill
for f in vendor/stem-branch/LICENSE vendor/stem-branch/README.md \
vendor/stem-branch/package.json vendor/stem-branch/dist/cli.cjs; do
git show a911c890:"$f" # 写入目标 worktree 的同名路径
done
交付:git push origin HEAD:staging(会触发 backend-quality-gate,因为 scripts/**、deploy/**、vendor/** 都在门禁路径内),推送后必须核对远端 SHA,并确认 /api/health 的 deployment.gitCommit 等于本次提交。
BUG 编号起点
origin/staging 上 docs/BUG_HISTORY.md 的当前最大号是 BUG-699。
三份并行单预分配编号段,各自不得越界;开工时仍要重新核对当前最大号(可能已被别的会话占用,占用了就整体后移并通知另外两单):
| 单 | 编号段 |
|---|---|
| 本单(七政 + 只读端点) | BUG-700 ~ 703 |
TASK-chart-page-20260915 |
BUG-704 ~ 706 |
TASK-ephemeris-page-20260915 |
BUG-707 ~ 709 |
附:本轮发现但不在本单范围
立单期间实跑我们自己的引擎时发现一条与七政无关的口径不一致,不要在本单修,但请在本单的 Bug 记录里记成 BUG-703 investigating,避免丢失:
- panchanga 岁差与账户设置不一致。
/api/panchanga_range的响应里calculation_policy.panchanga写着"SwissEph Lahiri at sunrise-relative reference time",而账户默认岁差自80102459(2026-08-20)起已是 Raman,且/api/chart确实按 Raman 算。同一个用户在星盘页看到的是 Raman 盘,在星历/今日路径看到的五要素却是 Lahiri 口径。 - 复现:
POST /api/panchanga_range {"start_date":"2026-09-14","end_date":"2026-09-16","lat":31.19,"lon":121.44,"tz":8,"ayanamsa":"raman"},读report.calculation_policy.panchanga。 - 处置:记
investigating,写明现象与复现,不要猜根因、不要顺手改默认值。是统一到 Raman 还是明确声明 panchanga 永远用 Lahiri,是产品决策,另出单。
进度与记录
- 进度记录:
docs/tasks/PROGRESS-qizheng-native-chart-20260915.md,本单状态板一行。 - 被环境挡住的写
BLOCKED.md(预期缺口:无 Docker → 镜像构建与容器内node --version无法验证)。 - 用户可感知的行为变化写
CHANGELOG.md;本单只新增只读接口,Skill 版本不 bump,在 CHANGELOG 里写明「未 bump」。 - 索引:
docs/tasks/README.md追加本单。