Files
Jyotisha/docs/tasks/TASK-qizheng-native-chart-20260915.md
T
Jesse_ChenandClaude Opus 5 6f74aa6704 docs(tasks): brief the read-only chart and ephemeris pages
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
2026-09-15 07:09:23 +00:00

22 KiB
Raw Blame History

TASK-qizheng-native-chart-20260915 · 接入原生七政四余排盘,并开出三个只读计算端点

2026-09-15 修订(立单当天):本单扩了范围。原版只做 /api/qizheng;核对后发现 P0 星盘页与 P1 星历页还各缺一个后端端点,而三者都要改 scripts/jyotish_api_server.py。为了让后续两份前端单能真并行,把三个端点的注册全部收进本单,由本单独占该文件的写权。两份前端单一行后端代码都不碰。

基线

  • 代码基线:origin/staging = 2d7698eafix(chat): keep stored rectification titles on open)。
  • 上游来源:/workspace/yinduzhanxing,分支 codex/add-birth-time-rectification-skillcommit a911c890feat: add native qizheng chart adapter2026-09-15 14:14 +0800)。
    • 注意:上游 main 仍是 a6f47abd2026-09-03),即我们 09-03 那轮已同步的 commit。本单要的东西只在这条分支上,不在 main。
    • 同分支另有 6c27aab6 fix: harden report export and validation gates8bba9cb5 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.0Apache-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)。

事故实证

以下三条全部是实跑复现的,不是猜测。复现命令见每条末尾(CLIvendor/stem-branch/dist/cli.cjs)。

实证 1 · 四柱时柱按 UTC 墙钟计算(BUG-700

vendored CLI 的 --pillars 读的是 UTC 墙钟,不是出生地本地时间:

传入 引擎回的时柱 应为
1990-04-09T13:24:00+08:00 (卯时 = 0507 时)
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 —— 神煞是空的
  • dignities 11 曜里,除太阳记「陷」外全部返回「平」

第二条对得上上游自己的台账:references/oracle/qizheng_runtime_truth_closure_status_2026_08_29.jsonpromotion_decision.may_update_runtime_dignity_table = falseruntime_promotable_count = 0qizheng_positive_layer_runtime_gate_2026_08_30.json 63 格全部 blocked;十一体 × 十二宫共 132 格的庙旺表只拿到 9 格直接证据。

庙旺这一列现在没有可用的判定,只能当占位。

根因

  1. vendored 引擎把「天文时刻」和「历法干支」用了同一个时间入口,前者要 UTC、后者要本地民用时,混用导致时柱错位。
  2. 适配器把派别选择(ketuModesiderealMode)当成引擎内部细节透传,没有提升为产品参数。
  3. boundary 文案是按引擎能力清单写的,不是按本次实际输出写的,于是把空字段说成已生成。

决策记录

产品负责人在 2026-09-15 的对话中授权:

  1. 接入七政四余原生排盘,走 vendored stem-branch(Apache-2.0)路线,不再自己做 132 格考据。
  2. 本单只做引擎与只读接口,前端另出一单(原因见「范围边界」)。
  3. 不做八字 / 四柱--pillars 存在实证 1 的缺陷,本轮不修、不调用;只把它记成红线与 BUG,避免后来者踩。
  4. 计都派别与宿度起算口径必须让用户看得见,按岁差(ayanamsa)既有的做法处理成显式参数,不接受引擎默认静默生效。
  5. 庙旺不得进入任何结论。它可以出现在响应里,但必须带 unclosed 标记;解读层、报告层、咨询层一律不得引用。

本单不推翻既有红线。特别地,AGENTS.md §6「scripts/jyotish_api_server.py must not grow」继续有效,见任务 3 的硬上限。

硬红线

  1. --pillars / --luck / --polaris / --qimen / --liuren 等其余子命令,本单一律不得调用。 vendored CLI 里有它们,但只有 --seven-governors 在本单验收范围内。
  2. 绝不把带时区偏移的 ISO 字符串喂给需要本地民用时的子命令。 若将来要做四柱,必须先解决实证 1,并在 docs/BUG_HISTORY.md 关联 BUG-700 / BUG-245 / BUG-905。
  3. 宿度不是恒星黄经。 引擎的 siderealLon 自角宿初度起算,与本仓 Raman / Lahiri 恒星黄经、与西洋回归黄道都不是同一套。响应里必须显式标注坐标系,任何界面都不得做三套之间的度数换算,也不得把两套结果叠加成「双重印证」。
  4. 庙旺(dignities)不得进入结论。 见决策记录 5。
  5. scripts/jyotish_api_server.py 不得新增 handler 主体,只允许薄注册,见任务 3。
  6. frontend/src/app/page.tsxorigin/staging 上是 1951 行,不得再增长;本单不碰前端。
  7. 不得顺手升级依赖、不得顺手修不在本单里的 warning;发现了写进 BLOCKED.md 或进度记录。
  8. 不得把 /workspace/yinduzhanxing 当作运行主仓;从它那里只取 a911c890 的文件内容,不引用它的路径。

范围边界

本单做vendored 归档、镜像 Node runtime、适配器、只读接口、测试、Bug 记录。

本单不做:任何前端文件。

并行与文件归属。同期还有两份前端单,三份单之间没有任何共享代码文件,可以并行开工:

文件 / 目录 归属
vendor/**NOTICEdeploy/** 本单
scripts/qizheng_chart_engine.pyscripts/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.mdCHANGELOG.mdfrontend/DESIGN.md 三方都写,只许追加各自小节,合入顺序 本单 → chart-page → ephemeris-page

前端对本单的依赖是运行时依赖,不是代码依赖:端点没上线时,前端对应区块渲染静态说明,端点合入后自然点亮。

原型(五个 Tab 的完整形态、中宫排盘参数卡、三套坐标系的边界文案)已经画好,两份前端单都会引用它。

任务分解

任务 1 · vendored 归档与 API 镜像 Node runtime

这是后续全部任务的阻塞项,必须先完成。

  • 从上游 a911c890 取入以下四个文件,路径保持一致: vendor/stem-branch/LICENSEvendor/stem-branch/README.mdvendor/stem-branch/package.jsonvendor/stem-branch/dist/cli.cjs
  • 新增仓库根 NOTICE(或在既有归属文件中追加):注明 @4n6h4x0r/stem-branch 0.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 仍为 200swisseph_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_modesidereal_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.py fail=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/stagingelif path == '/api/chart':3497 行
  • handler 用既有的 _load_local_module('qizheng_chart_engine') 模式(该函数已存在,见 137 / 413 / 1009 行的用法),函数体 ≤ 8 行,只做加载、调用、把领域异常翻成 BadRequest
  • 行数硬上限origin/staging 上该文件是 11313 行,契约上限是 11363tests/test_api_server_growth_contract.pybaseline 11063 + 300 余量),总余额只有 50 行。三个端点加起来,本轮对该文件的净增 不得超过 32 行。超了就把 handler 再压薄,不要动契约。
  • 端点是只读计算,按既有重计算端点的口径纳入 JYOTISH_HEAVY_COMPUTE_CONCURRENCY 限流(默认 2,饱和 429)。CLI 子进程 20 秒超时要与限流口径对得上。

3b · /api/western(回归黄道本命盘)

前端「西洋盘」Tab 需要,但当前没有任何路由暴露它scripts/western_chart_engine.pybuild_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 从请求读,默认 PPlacidus),回写进响应。
  • 响应必须显式带坐标系标注(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-700vendored 引擎四柱时柱按 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. 先保证 任务 1 + 任务 2 + 任务 3a + 任务 4。没有镜像 Node 与适配器,这一单等于没做。
  2. 任务 3 的限流接入可以延后,但路由与行数上限不能延后。
  3. 3b 与 3c 若来不及,可以拆到本单的第二次推送,但必须仍由本单交付——不得让前端单去改 scripts/jyotish_api_server.py。延后时要在进度记录里写明,并通知两份前端单:对应区块先渲染「该数据源尚未上线」。
  4. raw_engine_output 的审计字段可以先不落地,改为进度记录里说明。
  5. 不可让步:许可证归档(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/healthdeployment.gitCommit 等于本次提交。

BUG 编号起点

origin/stagingdocs/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",而账户默认岁差自 801024592026-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 追加本单。