docs: task brief for #173 ondemand-transit-date (2026-11-09)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DbBXoyn7Hj3LCW3BUs5bZ8
This commit is contained in:
@@ -0,0 +1,182 @@
|
||||
# TASK:聊天按需计算(一)指定日期的行运,并建立六类按需工具的共用模块与合同(2026-11-09)
|
||||
|
||||
- 分支:`codex/ondemand-transit-date-20261109`(基于 staging `b453505d`)
|
||||
- Bug 编号:BUG-1816(开工时核对 `docs/BUG_HISTORY.md` 最大编号;1809–1830 已预留给本批任务)
|
||||
- 负责验收:Claude(jyotisha-2a)
|
||||
|
||||
## 背景
|
||||
|
||||
占星师 2026-11-07 Q9:聊天“至少应支持以下六类,不能把所有问题压成‘今天起 90 天’”,第一类是“指定日期行运 `transit --year --month --day --tz`”;并且“所有适用入口都应显式传递已确认的岁差、交点和位置口径,不能让 CLI 默认 Raman 替用户做选择”。
|
||||
|
||||
2026-11-08 第五轮第 9 条:“指定日期行运:支持日期、地点、时区、岁差、节点和位置模式”。
|
||||
|
||||
Q3:“每次回答必须核对实际计算结果的日期和参数,不能复用过期行运。”
|
||||
|
||||
验收清单 §5 要求每个问题沿“计算 → 数据卡 → 模型输入 → 回答 → 导出”五个阶段都能核对:计算阶段要保存“原始输入、计算版本、profile、原始结果”,并区分“缺输入与未实现”。
|
||||
|
||||
**本任务有两件事**:
|
||||
|
||||
1. 建立六类按需工具(#173–#178)共用的模块和输入输出合同;
|
||||
2. 在这个合同上实现第一个工具:指定日期的行运。
|
||||
|
||||
#174–#178 会在本任务验收合并后,各自往这个模块里加一个文件和一行注册。因此本任务的合同设计决定了后面五单,请写清楚。
|
||||
|
||||
## 现状(b453505d 实测,行号以符号为准)
|
||||
|
||||
- 聊天工具都在 `frontend/src/mastra/consultation-tools.ts`。`createConsultationTools()`(约 :1371)在约 :1702–1707 返回工具表:`run-jyotish-consultation`、`read-consultation-evidence`、`search_jyotish_books`,以及条件出现的校时工具。目前没有 `frontend/src/mastra/tools/` 这类目录。
|
||||
- 第 0 步只开放本命计算工具:`consultationNatalPrepareStep`(约 :260–283)。之后各步 `toolChoice: "auto"`,所有工具可用。新工具不必加进第 0 步。
|
||||
- 出生资料由服务器绑定,不经模型:`ConsultationAgentContext`(约 :599)里的 `serverChart.toolInput`(类型见 `frontend/src/lib/consultation-route-service.ts` 约 :47)有年月日时分、city、lat、lon、tz、ayanamsa,**没有 node_mode 和 position_mode**。
|
||||
- 后端 `scripts/jyotish_api_server.py` 是冻结文件:`tests/test_api_server_growth_contract.py` 要求类方法数不增长(上限 215)、`JyotishAPIHandler.__new__` 不增长、行数不超过粗护栏。新接口只能是 `do_POST` 里一条薄 `elif`,加载独立模块。范例是 `/api/book_citations`(约 :10401–10408)和 `/api/chart_points`(约 :10387)。`tests/test_chart_view_points_api.py` 的 `test_route_is_registered_thinly` 是对应的测试写法。
|
||||
- 现有 `transit` CLI(`scripts/jyotish_engine.py` 的 `cmd_transit`,约 :12992)**不能直接包装**:
|
||||
- 没有 `--ayanamsa`、`--lat`、`--lon` 参数,岁差总是进程默认的 raman;
|
||||
- 按当地正午算:`hd = 12.0 - (args.tz or 8)`,tz=0 会被当成 8;
|
||||
- `--retrograde-calendar`、`--find-conjunction` 依赖的 `scripts/transit_search.py` 在仓库里不存在。
|
||||
- 现有 `/api/transit`(`_compute_transit_triggers`)需要前端传本命行星,没有交点参数。
|
||||
- 默认值互相矛盾:引擎 `DEFAULT_AYANAMSA_NAME='raman'`(`scripts/ayanamsa_utils.py`),`scripts/calculation_profile_contract.py` 的 `DEFAULTS` 却写 lahiri / mean / apparent。这正是占星师说的“不能让默认值替用户做选择”。
|
||||
- 位置口径:引擎有 `POSITION_MODES=('legacy','mean','apparent')` 与 `_sidereal_calculation_profile(jd, mode)`(`jyotish_engine.py` 约 :1029–1046)。但 `_calc_sidereal_planets_for_jd` 把 `'legacy'`(即 apparent)写死了。
|
||||
- 已有可复用的输出约定:
|
||||
- `references/interpretation_status_contract.md` 定义 `calculation_status`(executed / partial / blocked / not_applicable);
|
||||
- `domain_calculation_service._contract` 输出 `{requested, effective}` 与 `result_hash`;
|
||||
- `scripts/rectification_input_contract.py` 的 `candidate_input_fingerprint` 是输入指纹的写法。
|
||||
|
||||
## 设计(本任务定下,#174–#178 照用)
|
||||
|
||||
### 冲突处理:共用模块 + 串行
|
||||
|
||||
六个工具共用一个后端包、一个前端目录、一条后端路由、一处工具表接入:
|
||||
|
||||
- 本任务建好以上四样,并实现第一个工具。
|
||||
- #174–#178 在本任务验收合并到 staging 之后才开工。每单只新增自己的文件,并在两个注册表里各加一行。
|
||||
- 本任务之外,任何一单都不再改 `consultation-tools.ts` 和 `jyotish_api_server.py`。
|
||||
|
||||
### 后端:新包 `scripts/ondemand_calc/`
|
||||
|
||||
- `__init__.py`
|
||||
- `contract.py`
|
||||
- **输入校验**:`profile` 必须**显式**带齐 `ayanamsa`、`node_mode`(mean / true)、`position_mode`(apparent / mean)和 `location`(`lat`、`lon`,以及 `tz` 小时数或 `timezone_id` 二者之一)。缺任何一项就抛 `OnDemandInputError`,不得补默认值。
|
||||
- **输出信封** `build_envelope(...)`,固定字段如下:
|
||||
- `schema`:`"jyotish.ondemand_calc.v1"`;
|
||||
- `tool`:工具名;
|
||||
- `calculation_status`:executed / partial / blocked;
|
||||
- `profile`:实际生效的 ayanamsa、node_mode、position_mode、lat、lon、tz、timezone_id,以及 `location_source`:`user_supplied`,或 `birth_place_fallback`(用户没给地点、由服务器回退到出生地);
|
||||
- `requested`:调用方原样传入的参数;
|
||||
- `input_fingerprint`:对“规范化后的 requested + profile + 引擎版本”做 sha256,键排序,浮点固定小数位;
|
||||
- `engine_version`;
|
||||
- `raw`:原始计算结果,不做解释性加工;
|
||||
- `gaps`:列表,每项为 `{field, reason}`;
|
||||
- `computed_at_utc`。
|
||||
- **失败也必须返回信封**:`calculation_status="blocked"`,`raw` 为空对象,`gaps` 写清原因。失败信封里**不得出现任何日期型推断结果**(只能回显 requested 里调用方给的日期)。
|
||||
- `registry.py`:`TOOLS: dict[str, Callable[[dict], dict]]`,以及入口 `ondemand_calc_api(body)`。入口按 `body["tool"]` 分派;未知工具抛 `OnDemandInputError`。后续各单只在 `TOOLS` 里加一行。
|
||||
- `transit_date.py`:本任务的工具,见下节。
|
||||
|
||||
**路由**:在 `jyotish_api_server.py` 的 `do_POST` 里,紧挨 `/api/book_citations` 分支之后,加一条:
|
||||
|
||||
```python
|
||||
elif path == '/api/ondemand_calc':
|
||||
ondemand = _load_local_module('ondemand_calc.registry')
|
||||
try:
|
||||
result = ondemand.ondemand_calc_api(body)
|
||||
except ondemand.OnDemandInputError as exc:
|
||||
raise BadRequest(str(exc)) from exc
|
||||
self._json(result)
|
||||
```
|
||||
|
||||
不得新增类方法。若要占用重计算槽位,可在 `scripts/api_heavy_compute_gate.py` 的 `HEAVY_COMPUTE_PATHS` 加这个路径。
|
||||
|
||||
### 指定日期行运 `transit_date.py`
|
||||
|
||||
- **输入**:
|
||||
- `date`(YYYY-MM-DD,必填);
|
||||
- `time`(HH:MM,可选;不给就用当地 12:00,并在 `profile` 里写 `time_source: "local_noon_default"`);
|
||||
- `profile`(同上);
|
||||
- `natal`(服务器绑定的出生数据,用于算落入本命第几宫)。
|
||||
- **计算**:直接调用引擎的恒星黄经计算,显式传入 ayanamsa、node_mode、position_mode。position_mode 若引擎做不到(例如被写死成 apparent),请求 mean 时返回 `blocked`,gap 写 `position_mode_unsupported`,不得悄悄按 apparent 算。不调用 `cmd_transit`,也不调用 `transit_search`。
|
||||
- **`raw`**:九曜(含罗睺、计都)的恒星黄经、星座、度数、星宿与 pada、是否逆行、日速度,以及相对本命上升与月亮的宫位。时刻同时给 UTC 与当地时间。
|
||||
|
||||
### 前端:新目录 `frontend/src/mastra/ondemand-tools/`
|
||||
|
||||
- `contract.ts`:
|
||||
- zod 的信封 schema(与 Python 字段一一对应);
|
||||
- 公共输入片段 `locationInputSchema`(可选的 `lat`、`lon`、`timezone_id` 或 `tz`,以及 `place_label`);
|
||||
- 工具描述里的固定句子:“计算失败时结果里只有缺口,不得自行推算或补写任何日期”。
|
||||
- `client.ts`:`postOnDemandCalc(tool, body, signal)`,调用 `${JYOTISH_API_BASE}/api/ondemand_calc`(apiBase 的取法照 `consultation-workflow.ts`)。网络错误或非 200 时,在前端生成同形状的 `blocked` 信封,不抛给模型。
|
||||
- `profile.ts`:从 `ctx.serverChart` 和本命计算实际生效的口径里,取出 ayanamsa、node_mode、position_mode。
|
||||
- 本命计算工具的结果里若有 `calculation_contract.effective` 或 `calculation_profile`,以它为准。
|
||||
- 若 node_mode 或 position_mode 在上下文里取不到,就按 staging 本命计算实际发给后端的值显式填入,并在 PROGRESS 里写明来源(文件:行)。
|
||||
- 若查明本命计算本身依赖后端默认值、并没有显式口径,**停下选“需拍板”**(见末节)。
|
||||
- `registry.ts`:
|
||||
- `createOnDemandTools(ctx)` 返回 `{ [id]: tool }`。本任务只放 `ONDEMAND_TRANSIT_DATE_TOOL_ID = "calc-transit-on-date"`。
|
||||
- 每个工具在 `ctx.state` 上计数,每轮最多 3 次,第 4 次返回 `refused`(照 `read-consultation-evidence` 约 :1620–1635 的写法)。
|
||||
- 每次调用用 `appendConsultationRuntimeStep` 记一笔:工具名、`input_fingerprint`、`calculation_status`。审计表据此由真实回执生成。
|
||||
- `transit-date.ts`:
|
||||
- 模型能填的只有 `date`、`time`、`location`。出生资料、ayanamsa、node_mode、position_mode 一律由服务器从 `profile.ts` 填入,模型不能改。
|
||||
- 用户没给地点时,用 `serverChart.toolInput` 的出生地,信封写 `location_source: "birth_place_fallback"`。
|
||||
|
||||
### 接入 `consultation-tools.ts`(本任务唯一允许的改动)
|
||||
|
||||
- 在 :1702–1707 的工具表里加 `...createOnDemandTools(ctx)`。
|
||||
- 若计数需要,在 `ConsultationRuntimeState`(约 :453/:474)加一个计数字段。
|
||||
- 检查 `frontend/src/lib/stream-agent-response.ts` 的 `isCalculationTool`(约 :518)和 `contractReady()`(约 :419–425)。新工具不得被算作本命计算工具,不得影响 `consultationToolSuccessCount === 1` 的判定。若必须改这两处才能工作,只做最小改动并在 PROGRESS 说明。
|
||||
|
||||
## 不要碰
|
||||
|
||||
- `references/**`(占星师方法文件),一字不改。
|
||||
- `frontend/src/lib/consult-preset-prompts.ts`、`references/product_runtime_contract.md`、`frontend/src/lib/consultation-methodology.ts`:不改答题规则和提示词正文。工具说明只写在工具自己的 `description` 里。
|
||||
- 冻结文件:
|
||||
- `scripts/jyotish_api_server.py` 只许加上面那条 `elif`;
|
||||
- `scripts/domain_calculation_service.py`、`scripts/dasha_analyzer.py`、`scripts/narayana_dasha.py`、`scripts/shadbala.py`、`scripts/ashtakavarga.py`、`scripts/jaimini.py`、`scripts/varga.py`、`scripts/functional_benefics.py`、`scripts/divisional_charts_extended.py`:只许 import 调用,不许改。
|
||||
- 其他单子的地盘:
|
||||
- `consultation-tools.ts` 的 :1025–1201 与 :1614–1662(查卡与序列化,#170/#171);
|
||||
- `run-jyotish-consultation` 的 execute 主体(#172);
|
||||
- `frontend/src/lib/consultation-evidence-card.ts`(#169/#170);
|
||||
- `frontend/src/app/api/consult/route.ts`(#172);
|
||||
- `scripts/muhurta.py`、`scripts/cmd_muhurta.py`(#179)。
|
||||
- 不改 `cmd_transit` 的行为,不补 `transit_search.py`(那是 #174 的事)。
|
||||
|
||||
## 验收标准
|
||||
|
||||
1. **后端合同测试** `tests/test_ondemand_calc_contract_20261109.py`:
|
||||
- 缺 ayanamsa、node_mode、position_mode、location 任一项都返回 400,错误信息点名缺的字段;
|
||||
- 同一输入两次调用的 `input_fingerprint` 相同,改任一参数后就不同;
|
||||
- 未知 `tool` 返回 400;
|
||||
- 路由是薄注册:照 `test_route_is_registered_thinly` 断言 `elif path == '/api/ondemand_calc'` 和 `_load_local_module('ondemand_calc.registry')`;
|
||||
- `tests/test_api_server_growth_contract.py` 仍通过。
|
||||
2. **行运正确性** `tests/test_ondemand_transit_date_20261109.py`:
|
||||
- 用公开日期(例如 2026-01-01 12:00 UTC,地点 0°/0°)分别以 lahiri 与 raman、mean 与 true 交点计算,断言结果确有差别,且 `profile` 如实回显;
|
||||
- 与引擎现有的同时刻恒星黄经计算比对,误差 ≤ 0.01°;
|
||||
- tz=0 不得被当成 8;
|
||||
- `position_mode` 请求做不到时返回 `blocked` 且 gap 点名。
|
||||
3. **失败不编日期**:
|
||||
- 后端让引擎抛错(monkeypatch),信封为 `blocked`,`raw == {}`,`gaps` 非空;
|
||||
- 前端 `client.ts` 在网络失败时返回同形状信封;
|
||||
- 两边都断言结果 JSON 里除 `requested.date` 外没有别的日期字符串。
|
||||
4. **前端测试** `frontend/tests/ondemand-transit-date-20261109.test.ts`:
|
||||
- 工具出现在 `createConsultationTools` 的工具表里;
|
||||
- 模型输入 schema 只有 `date`、`time`、`location`,传入 `ayanamsa` 或出生字段会被 `.strict()` 拒绝;
|
||||
- 发给后端的 body 里 `profile` 四项都显式存在;
|
||||
- 不给地点时 `location_source` 为 `birth_place_fallback`;
|
||||
- 第 4 次调用为 `refused`;
|
||||
- 每次调用都写了一条 runtime step。
|
||||
5. **合同说明**:在 `scripts/ondemand_calc/contract.py` 顶部 docstring 写清信封字段、错误规则和“后续工具如何注册”(加文件、`TOOLS` 一行、`registry.ts` 一行)。#174–#178 照它执行。
|
||||
6. **Bug 记录**:在 `docs/BUG_HISTORY.md` 末尾追加 BUG-1816(格式照最近的条目),关联占星师 Q9 与第五轮第 9 条。不写任何出生资料或姓名。
|
||||
7. **测试数据**:只用公开日期、合成出生数据(如 `tests/neutral_subject.py`),或公开名人盘。
|
||||
8. **改既有断言**:写“原值 / 新值 / 原因”三栏注释。原因写:`占星师 Q9 / 第五轮 9:按需计算显式传口径`。
|
||||
|
||||
## 自测命令(Windows / PowerShell)
|
||||
|
||||
- `frontend\node_modules\.bin\tsc --noEmit -p frontend`
|
||||
- 在 `frontend` 下运行 `npm run lint`
|
||||
- 在 `frontend` 下运行 `node node_modules/tsx/dist/cli.mjs --test tests/ondemand-transit-date-20261109.test.ts tests/consultation-agentic-runtime.test.ts tests/consult-evidence-lookup-20260927.test.ts`
|
||||
- `.venv\Scripts\python -m pytest tests/test_ondemand_calc_contract_20261109.py tests/test_ondemand_transit_date_20261109.py tests/test_api_server_growth_contract.py -q`
|
||||
|
||||
前端全量测试和 Python 快速门由 Claude 在验收时跑。
|
||||
|
||||
## 进度记录
|
||||
|
||||
`docs/tasks/PROGRESS-ondemand-transit-date-20261109.md`
|
||||
|
||||
## 何时停下来选“需拍板”
|
||||
|
||||
- 本命计算并没有显式的 node_mode 或 position_mode,按需工具无法“与本命同口径”时。要决定的是:产品默认口径定为哪一组,以及由谁定。
|
||||
- 引擎做不到按请求切换 position_mode,而要做到就必须改冻结文件时。
|
||||
- 接入新工具必须改 `stream-agent-response.ts` 的合同判定,而且改动会影响现有本命计算流程时。
|
||||
Reference in New Issue
Block a user