fix(consult): run the local skill's full technique spectrum on the web path
Independent Staging Quality Gate / validate (push) Successful in 10m59s
Independent Staging Quality Gate / publish (push) Successful in 9m7s

Web answers were thinner than a local Agent calling yinduzhanxing-skill:
theme-subset vargas, no visible audit table, and a prompt that dropped the
invocation contract. Bind the commercial method, compute D1–D60 plus Western
layers, and deliver the same Full-Spectrum checklist.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Jesse_Chen
2026-08-18 23:30:36 +08:00
parent d04fc30b63
commit 2bf7472645
29 changed files with 1176 additions and 191 deletions
+30 -3
View File
@@ -15,7 +15,7 @@ description: "印度占星(Jyotish)商业解盘与推运系统。核心能
3. `references/oracle/commercial_skill_truth_overlay.v1.json`:商业声明和受限技法的最终覆盖层。
4. 服务端 `consumer_context.answer_policy`:当前请求可回答范围的最终合同。
若研究快照、商业覆盖层和服务端回执冲突,以商业覆盖层和服务端回执为准。真实计算只能来自服务端工具,模型不得重算或发明行星位置。候选出生时间不得写成 confirmed;`blocked`、参数敏感、外部验证未闭环和多体系冲突必须原样保留。正式个人报告固定`executive_summary -> thematic_narrative -> evidence_appendix` 排列,Technique Audit Table 位于附录,不得置于摘要之前。医疗、法律、投资、安全关键结论及确定性死亡/诊断/妊娠预测均禁止。
若研究快照、商业覆盖层和服务端回执冲突,以商业覆盖层和服务端回执为准。真实计算只能来自服务端工具,模型不得重算或发明行星位置。候选出生时间不得写成 confirmed;`blocked`、参数敏感、外部验证未闭环和多体系冲突必须原样保留。网页对话在口语回答末尾给出 Technique Audit Table(已执行 / 阻塞 / 不适用);正式个人报告`executive_summary -> thematic_narrative -> evidence_appendix` 排列,审计表位于附录。医疗、法律、投资、安全关键结论及确定性死亡/诊断/妊娠预测均禁止。
> **版本**v6.9.14 | **详细变更**`CHANGELOG.md`
> **对标状态**:中文用户端与技法覆盖领先;D1/D9/AV/Chara 等有守门,Dasha/Shadbala 外部 oracle 扩充仍在进行。
@@ -37,6 +37,29 @@ KP/Muhurta/Gochara/Sahams/Sphuta/Tajika等高阶分支必须按 skill truth over
| KP系统 | reference-only / partial |
| Muhurta/Gochara/Sahams/Sphuta/Tajika | 只作参考或探索性证据,除非 evidence packet 明确升级 |
## 关联技法完整调取
全谱系真实调用:咨询工作流必须真实运行全部可运行且问题相关的技法,不得因篇幅、成本或另写一份运行时覆盖层而少算一层。确定性措辞受商业声明合同约束;信息密度不受成本削减。口语回答须先用已交付的统一参数与原始结构(度数、分盘、Dasha 边界、强弱、西洋层),再并列各体系结果、冲突与证据状态,最后给出条件性综合推理。
- 共同基础:D1、功能性吉凶星、相关 Yoga、Vimshottari 与 Narayana Dasha、行运、Shadbala、Ashtakavarga。
- 分盘:主题相关分盘优先解读;D1–D60 中 20 张正式传统分盘与 40 张研究型通用 D-N 均须计算。正式分盘与 generic D-N 分别标明,不得把研究 D-N 宣传为已验证传统分盘。
- 领域关联:事业至少 `D10 + A10`,财富至少 `D2 / D11`,婚恋至少 `D9 + UL`
- 西洋层:有本命出生资料时,自动运行已实现的热带本命及 timing(行运、回归、次限、日弧、converse、中点、月回归、duration scan、parans)。未实现或失败标 `blocked`,不适用标 `not_applicable`
- 特殊体系:KP、Tajika、Saham、Gulika/Mandi、Panchanga、Muhurta、Prashna、Sphuta、合婚等,按问题关联调用或解释 `blocked / not_applicable`。商业上 KP/Muhurta/Tajika 不得作为确定性结论。
- Technique Audit Table 对每项使用 `executed / blocked / not_applicable`(可见回答译为 已执行 / 阻塞 / 不适用)。未闭环技法仍展示状态与边界,不得写成确定事件。
### P0/P1 观察层(不得升格为已验证真值)
网页与 API 结果必须显式显示这些行的调用状态与 claim boundary
- `A7 / UL / A10 / KP`:事业看 A10,婚恋看 UL/A7;KP 仅观察层。
- `Gulika / Mandi`:可展示;缺独立 oracle 时不得作为最终断事证据。
- `Muhurta factors`:只作 factor/observation,不得输出最终择日 verdict。
- `Shadbala components`:显示分量状态;不得声称绝对值已闭环。
- `Ashtakavarga Kakshya`:观察层,不得用于 verified day/month timing。
- `Ashtakoota`:无伴侣盘时 `not_applicable`
- `Adhana / Niseka`research-only;普通本命咨询为 `not_applicable`
## v6.9.14 核心能力
| 维度 | 数据 |
@@ -122,15 +145,19 @@ adapter available 解释为已完成 VedAstro、PyJHora/JHora 或 jyotishganit r
`JYOTISH_ASYNC_JOB_BACKEND=sqlite`,使用 `scratch/local/async_jobs.sqlite3` 保存 token-hash
与 TTL 任务记录。两种后端都不是 Redis、多节点队列或跨主机 worker;不得把它们描述为分布式恢复能力。
**强制工作流**(完整规范 → `references/ai-reading-workflow-prompt.md` v5.1.0):
## 强制工作流
完整规范 → `references/ai-reading-workflow-prompt.md` v5.1.0。网页咨询与本地 Agent 调用同一套步骤,不得另减技法。
0. **阶段负一**:问题类型路由(事业/婚恋/财务/应期/历史验证/综合解盘)→ 必须先读 `references/strict-workflow-router.md`,按对应 strict checklist 执行;用户不需要主动点名高级技法。
0.0.1 **全谱系真实调用**:事业、财富、年度推运、事件应期、校时或综合解盘,必须实际运行全部可用且问题相关的印度与西方技法。不得把“仓库存在”“UI可见”或“默认未传参数”伪装成已调用;每一项必须输出 `executed``blocked``not_applicable`。限制只作用于确定性措辞,不得削减信息密度。咨询工作流与本文件是同一套合同,不另维持一份锁定稿调用面。
0.1 **事件判定骨架**:凡涉及 marriage / career / wealth / event verify,必须执行 `事件判定骨架 v1.0`,按 `Route -> Evidence Ledger -> Adjudication -> Output Contract` 顺序输出;不得再凭直觉跳模块或随口给置信度。详见 `references/ai-reading-workflow-prompt.md``references/event_judgment_skeleton.md``references/event_judgment_marriage.md``references/event_judgment_examples.md`
1. **阶段零**:入口路由(A/B/C自动判断)
2. **阶段一**(仅B):PDF/图片提取 + Quality Gate
3. **阶段二**:意图识别 → 路由目标宫位(无明确意图→Level 2综合解盘)
4. **阶段二点五**:若 `full-reading` 或网页/API 返回 `ai_prompt_pack`,必须优先读取 `prompt_zh``evidence_snapshot``retrieval_plan` 作为 AI/RAG 主上下文;若没有该字段,再退回传统 JSON 摘要。
4.1 **VedAstro 官方优先级**:用户给出生信息后,网页、Skill、MCP 都必须默认走同一条数据优先级:`VedAstro official snapshot -> local supplemental modules -> local fallback only when official blocked`。用户不需要主动要求“调用 VedAstro”。若 `evidence_snapshot.vedastro_official_full_snapshot.status``ok/partial` 且官方 chart 可用,D1/分盘/官方返回的原始字段以 VedAstro 为主;本地引擎只做补充、交叉检查或官方 blocked 时 fallback。
4.1.1 **Formal Varga 官方证据审计**:需要补充 VedAstro 官方形式分盘证据时,返回的 `vedastro_official_formal_varga_audit` 只能进入 Evidence Ledger / Technique Audit Table,作为 `evidence_only`,不得宣称本地 Varga 公式真值闭环、不得用官方响应静默覆盖本地算法、不得写入或展示任何 API key。
4. **阶段三**:静态分析10步(宫位→承诺→Yoga→Argala→逆行→NK→Shadbala→AV→Ketu→分盘)
5. **阶段四**:动态推运7步(Dasha→五系统Convergence→Transit→Double Transit→Jaimini→KP→Varshaphala
6. **阶段五**:应期输出(五层验证→时间窗口→Actionable Output+案例检索)
@@ -186,7 +213,7 @@ adapter available 解释为已完成 VedAstro、PyJHora/JHora 或 jyotishganit r
涉及 timing / event / outcome 问题时,不得只看 Vimshottari。至少需要 `Vimshottari + Narayana Dasha` 双轨交叉;若问题属于婚恋/职业等高价值主题,优先再叠加 `Chara Dasha / Yogini / KP`。若关键结论在双轨之间明显冲突,必须降级置信度或标记 `blocked`,不得输出伪确定结论。
4. **强制多维分盘显微镜**
不得只看 D1。至少按问题域强制展开:`D10 for career, D2/D11 for wealth, D9 for marriage`,并尽可能联动 `A10 / UL / AK / DK / Karakamsha / Special Lagnas`如果相关分盘或特殊点未调用,Technique Audit Table 必须写明它如何削弱结论。
不得只看 D1。至少按问题域强制展开:`D10 for career, D2/D11 for wealth, D9 for marriage`,并联动 `A10 / UL / AK / DK / Karakamsha / Special Lagnas`D1–D60 正式分盘与其余研究型 D-N 均须计算并进入 Technique Audit Table;未调用的必须写明它如何削弱结论。
5. **强制物理原始数据交付**
不允许只给“运势不错/有机会”式结论。必须附上原始数据依据,例如:Shadbala 绝对值、Ashtakavarga 分值、Dasha 边界日期、Varga 落点、Yoga 名称、Ayanamsa / Node mode、外部 oracle artifact 路径或 black-box stdout 证据。原始数据交付是高严谨结论的唯一有效依据。
+35 -2
View File
@@ -4260,8 +4260,41 @@
- 根因:两件事叠在一起。(一)激活的开销是逐轮的:咨询路径没有配 Mastra memory,也没有 `threadId`/`resourceId`Agent 每次请求新建(`id` 里含 `requestId`,不进缓存);回放的 `history` 只有 `{role, text}` 纯文本,不含任何工具调用或工具结果。所以模型每一轮都必须重新激活一次技能,那份载荷逐轮重发;更糟的是一次请求内的 `retry`(契约不完整)与 `retryForAnswer`(空回答)都是 `agent.stream([...baseMessages, 追加一句])`,各起一个全新的模型循环、各自再激活一次,最坏情况单次请求付三遍。(二)模型看到的包内容不受哈希约束:`skills: [jyotishSkillPath]` 指向的是工作树视图 `skills/jyotish-vedic-astrology`,而那里的 `references` 是软链到 `../../references` 的 827 条全量树;完整性检查只逐字节比对 SKILL.mdregistry 的 sha256 管不到激活时列举出来的那 1,592 条路径。生时纠正的 Agent 没有这个洞,它用的是 `resolveSkillPackageRuntimePath(skillPackage)`(先校验 sha256,再建一个以规范技能名命名的符号链接别名指向锁定版目录)——正确的机制仓库里早就有,只是咨询路径没用。
- 修复:把技能从「模型的一次工具调用」改成「服务端在跑之前就绑定好」,与生时纠正对齐。新增 `frontend/src/mastra/skill-binding.ts`:从锁定版包读 SKILL.md、剥掉 frontmatter,包进 `<jyotish-skill name version>` 交给 instructions`skills` 指向 `resolveSkillPackageRuntimePath()` 的哈希校验路径;用一个 `providesSkillDiscovery: "on-demand"` 的输入处理器撤掉 `skill``skill_search`,只留 `skill_read`(这是 Mastra 为「调用方自行负责技能发现与指令加载」留的声明位,不是绕路)。四个 Agent 统一走这个绑定,`Load the ... skill before answering` 一类句子改成声明方法已在提示里、并明确「以下是本产品的运行时契约,与 skill 方法冲突时以契约为准」。运行时状态的 `jyotishSkillLoaded` 改名 `jyotishSkillBound` 并在创建时即为真,绑定作为回执的第一步记下(不带耗时,因为确实没有取任何东西);`skill.started`/`skill.completed` 两个公开事件改由服务端在流开头宣告,客户端契约与文案不变,但首个 activity 从「等激活往返之后」提前到「立刻」。无出生分钟那条路径的契约重试整段删掉——它的 `requireTool` 为假、方法又已绑定,已经没有任何可修的契约了。那个处理器不只是个标记:它逐步检查系统提示里确实带着绑定标记,读不到提示就放过,能读到而没有方法则中止本轮——撤掉激活之后,一个装配时漏了方法块的 Agent 不会再报错,只会安静地在没有方法的情况下作答,这正是本轮要消掉的失败形态。
- 验证:新增 `frontend/tests/skill-binding.test.ts` 4 项:绑定的正文与锁定版 SKILL.md 逐字一致且运行时路径落在 sha256 目录下;实测激活载荷与绑定载荷的比值(前者是后者两倍以上,后者不到前者 45%);绑定后的工具面恰好只有 `skill_read` 而技能仍被声明;系统提示缺方法时中止、带方法与读不到提示时都放过。实测数字:从锁定版包激活是 118,352 字节(`## References` 56,696、`## Scripts` 14,824),绑定后 47,289 字节,每次省 71,063 字节;相对生产当前实际发出的 129,651 字节省 82,362。顺带一项一致性:锁定版的 `references` 是 189 条的真目录,而工作树视图是软链进 827 条,所以 `skill_read` 能读到的范围与 `methodology.further_reading` 校验的范围现在第一次是同一棵树。非 DB 套件 1,666 项中 1,665 通过,唯一失败是 `onboarding-route.test.ts` 里 50ms 墙钟对 5ms 超时的竞速用例,单独跑通过(19ms)且该用例注入 `generateText`、不构建 Agent,与本轮无关。`tsc --noEmit` 清洁,改动文件 `eslint` 清洁(6 条既有 warning 都不在改动文件内)。未做的验证:没有在 staging 上比对绑定前后同一问题的回答差异,也没有实测冷启动多算一次包哈希的代价(`resolveActiveSkillPackage``resolveSkillPackageRuntimePath` 各算一次)。评审中被自己的测试抓到一个真 bug:守卫最初用 `JSON.stringify(system).includes(标记)`,而标记里带双引号、序列化后会被转义,那样每一轮都会误判「方法没绑上」并中止全部请求;改成按形状递归收集字符串。
- 待跟进:`instructions` 现在 717+ 行,Mastra 仍在警告建议 <500。SKILL.md 里有相当一部分是仓库开发规范(`scripts/``references/``skills/` 该补成熟度而非重写之类),对回答一个用户的盘没有作用,但它是哈希锁定且本地 Agent 共用的产物,要瘦得重新发版——另开一轮。另外 general 与 onboarding 两条路径也各被绑了 47KB 的解盘方法,而它们一个不能做任何本命盘声明、一个只产四条建议问题;本轮为了不改行为先统一绑定,后续可以逐条裁
- 待跟进:Mastra 加载技能包时仍会对 SKILL.md 正文报行数警告(`skill_read` 仍要声明哈希目录),那是文件体积,不是 prompt 摘录。全谱系调用、强制工作流与 `strict-workflow-router.md` 的 Full-Spectrum / health-timing-strict 已写入商业入口并交付给网页模型,见 BUG-287。不要把研究仓入口绑进聊天 Agent
- 防复发:一次「模型可以忘记」的必要步骤,等于一条必然会出现的失败路径加一条修它的重试;当那一步的内容在服务端已经完全确定时,绑定比请求便宜,也比请求诚实。另一条:完整性检查必须覆盖模型实际看到的东西,而不只是入口文件——只比对 SKILL.md 的字节,会让人以为整个包都被钉住了。
- 相关记录:BUG-284(同一条链路的上半段:先把方法按路由交付,本轮再撤掉激活)、BUG-282(生时纠正侧的服务端绑定与 `skill.bound`,本轮对齐的先例)、BUG-214`runtime_contract_incomplete` 的门,本轮去掉了它对模型加载动作的依赖)
- 相关记录:BUG-284(同一条链路的上半段:先把方法按路由交付,本轮再撤掉激活)、BUG-282(生时纠正侧的服务端绑定与 `skill.bound`,本轮对齐的先例)、BUG-214`runtime_contract_incomplete` 的门,本轮去掉了它对模型加载动作的依赖)、BUG-286(摘录绑定并解开非解盘 Agent)
- 复发自:无
- 修复版本:待提交
## BUG-286 | 咨询 prompt 绑了整份 SKILL.mdgeneral 与 onboarding 也各付 47KB 解盘方法
- 状态:resolved(本地修复,待提交与发布)
- 首次发现:2026-08-18
- 最近更新:2026-08-18
- 影响面:`frontend/src/mastra/skill-binding.ts` 的绑定正文、`frontend/src/mastra/index.ts` 的四个 Agent 工厂、`frontend/src/app/api/consult/route.ts` 的无出生分钟路径注释。个人盘咨询、无出生分钟一般问答、引导问题生成。
- 用户现象:没有直接报错。Mastra 对技能入口报 `Instructions have 717 lines (recommended: <500)`;无出生分钟问答与首页引导问题每次仍把约 47KB 解盘手册送进模型,而这两条路径一个不能做任何本命声明、一个只产主题建议问题。
- 触发条件:任何一次个人盘咨询、无出生分钟咨询、或 onboarding 生成。
- 根因:BUG-285 为了不改行为,把锁定版 `SKILL.md` 全文塞进 instructions,并让四条 Agent 共用同一绑定。商业入口相对研究稿只换了文件头,底下仍留着施工判断原则、37 个子命令、名人案例索引;这些对回答一个用户的盘没有作用。上游研究入口仍标 `6.9.14`,比产品快照只多一行 Formal Varga 审计,把它绑进 Mastra 会更胖,并与可见语音冲突。
- 修复:咨询路径改为按标题从商业 `SKILL.md` 摘录运行时段落(商业路由、关联技法完整调取、五层硬约束、强制规则去掉开源复用边界、未闭环点、核心方法论、强制规范速查、注意事项),施工/CLI/案例索引留在包内供 `skill_read`。general 与 onboarding 不再插入方法块、不再声明 skill,与日签 Agent 对齐;onboarding 用一段产品范围条替代 47KB 方法。不绑研究入口。
- 验证:`frontend/tests/skill-binding.test.ts` 断言摘录来自商业入口且不含施工/CLI/案例标题,并含关联技法完整调取;新增「只有解盘 Agent 绑定方法」。相关套件 `skill-binding` / `consultation-birth-time-mode` / `consultation-voice-contract` / `consultation-workflow-contract` / `onboarding-route` / `daily-starlanguage` / `consultation-agentic-runtime` / `consultation-methodology` 通过。`tsc --noEmit` 清洁,改动文件 `eslint` 清洁。Mastra 加载技能包时仍会对 SKILL.md 报行数警告,因为咨询 Agent 还要 `skill_read` 指向该目录;那是文件体积,不是 prompt 摘录。
- 待跟进:SKILL.md 施工/CLI/案例段落仍在包内给本地 Agent 和 `skill_read`,聊天 prompt 继续只摘运行时段。未在 staging 上比对摘录前后同一问题的回答差异。
- 防复发:哈希锁定的入口可以全文保留给本地 Agent,但聊天 prompt 只能摘运行时段落;一条不能解盘的路径不得绑解盘方法。Mastra 对技能文件的行数警告与 Agent.instructions 体积是两件事,不要用发版去修只属于 prompt 的问题。
- 相关记录:BUG-285(全文绑定的直接前因)、BUG-284(路由方法论已随工具结果交付,本轮不再把同一份手册全文再塞一遍)、BUG-287(全谱系写入商业入口与咨询运行时)
- 复发自:无
- 修复版本:待提交
## BUG-287 | 咨询只算主题相关分盘,西洋层与技法审计表到不了回答
- 状态:resolved(本地修复,待提交与发布)
- 首次发现:2026-08-18
- 最近更新:2026-08-18
- 影响面:`scripts/jyotish_api_server.py` 咨询本地层、`scripts/unified_consultation_orchestrator.py` 证据包与分盘调度、`scripts/consultation_plan_contract.py` 证据类别、`frontend/src/mastra/consultation-workflow.ts` 模型投影、`frontend/src/mastra/product-voice.ts` 可见回答格式、商业 `SKILL.md``references/strict-workflow-router.md`(与 `versions/6.9.14` 包内副本)。个人盘咨询。
- 用户现象:没有直接报错。排盘只带 D2/D4/D9/D10/D11;其余 D1–D60 被写成「追问再展开」。西洋本命默认不算行运/回归/推运。模型上下文没有技法审计表,可见语音还禁止把它写进回答。对照本地 Agent 调用 `yinduzhanxing-skill`,网页还缺 Full-Spectrum 路由合同、健康清单、功能性吉凶星/Shadbala/Kakshya 进入作答,且口语被要求不要先写原始结构。
- 触发条件:任何一次有出生分钟的个人盘咨询。
- 根因:咨询路径按主题子集计算分盘以省成本;西洋 timing 要调用方逐项点名;`toModelOutput` 只投影四张卡片里的允许键,`runtime_evidence_log` 里的审计表从未进入模型上下文;VISIBLE VOICE 明确禁止复制 Technique Audit Table。
- 修复:咨询默认计算 20 张正式分盘加其余 D-N 研究分盘,并自动挂上已实现的西洋本命与 timing 层(失败标 blocked,不静默省略)。consumer_context 带 `varga_spectrum``western_spectrum``technique_audit_table`。模型投影放行这些字段。可见回答末尾要求一张中文技法审计表(已执行 / 阻塞 / 不适用),口语须先用已交付的原始结构再综合。同一合同写入商业 `SKILL.md`(关联技法完整调取 / 全谱系真实调用 / 强制工作流 / P0-P1 观察层),并把 `yinduzhanxing-skill``Full-Spectrum Invocation Contract``health-timing-strict` 写入产品 `strict-workflow-router.md` 随方法论交付。咨询本地层补上 Functional Benefic/Malefic、Shadbala、Kakshya。重算 `6.9.14` 包哈希;不另维持一份产品覆盖层。
- 验证:`tests/test_consultation_consumer_context.py` 断言正式 20 / 研究 40 张分盘及 Functional/Shadbala/Kakshya 本地层;`tests/test_western_chart_engine.py` 断言默认挂上已实现西洋 timing;前端 `consultation-context` / `consultation-voice-contract` / `consultation-plan` / `skill-binding` / `consultation-methodology` 断言审计表、全谱系合同、健康清单与商业入口强制工作流进入模型包。`skill-registry` 断言登记哈希与包树一致。
- 防复发:主题相关分盘只决定优先解读,不得再把未算的正式分盘写成 deferred。西洋 natal 咨询不得再要求调用方点名每个 timing 层。模型包缺少 `technique_audit_table` 视为回归。商业 SKILL.md、`strict-workflow-router.md` 与咨询运行时必须是同一套调用面;改入口后必须同步根文件、`versions/6.9.14` 并重算 registry sha256。网页口语不得再禁止使用已交付的原始结构。健康域必须交付 `health-timing-strict`,不得再报「无清单」。
- 相关记录:BUG-286(prompt 摘录,不靠发版修调用)、BUG-284(方法论随工具结果交付)
- 复发自:无
- 修复版本:待提交
+3 -3
View File
@@ -641,7 +641,7 @@ export async function POST(request: Request) {
? generalDailyContext
? "当前是无出生分钟的公共今日趋势咨询。可依据服务器提供的公共 Panchanga 摘要回答,但不得计算或推断个人星盘;不得补 00:00、时段中点或任何候选分钟。"
: "当前是用户明确选择的无出生分钟一般咨询。不得计算或推断个人星盘;不得补 00:00、时段中点或任何候选分钟。"
: "先加载 Jyotish Skill;如需新的个人星盘结论,必须调用服务器绑定的排盘工具。请用口语直接回答,不要复述内部字段或报本轮算了什么。",
: "先加载 Jyotish Skill;如需新的个人星盘结论,必须调用服务器绑定的排盘工具。请用口语直接回答;回答末尾附上技法审计表,不要复述内部字段。",
generalDailyContextPrompt(generalDailyContext),
resolvedQuestion.modelQuestion,
].filter(Boolean).join("\n"),
@@ -668,8 +668,8 @@ export async function POST(request: Request) {
const agent = getGeneralJyotishAgent(selectedModel);
const result = await agent.stream(baseMessages, streamOptions);
usages.push(result.totalUsage);
// This mode has no calculation to require and its method is bound before
// the run starts, so there is no longer a contract for a retry to repair.
// This mode has no calculation to require and no chart method to bind,
// so there is no contract for a retry to repair.
const retryForAnswer = async () => {
const retried = await agent.stream([
...baseMessages,
+2 -1
View File
@@ -29,9 +29,9 @@ const domainMethodology: Readonly<Record<ConsultationDomain, { strictRoute: stri
marriage: { strictRoute: "relationship-timing-strict", eventJudgment: "event_judgment_marriage.md" },
wealth: { strictRoute: "wealth-timing-strict", eventJudgment: "event_judgment_wealth.md" },
timing: { strictRoute: "event-timing-strict", eventJudgment: null },
health: { strictRoute: "health-timing-strict", eventJudgment: null },
annual: { strictRoute: null, eventJudgment: null },
general: { strictRoute: null, eventJudgment: null },
health: { strictRoute: null, eventJudgment: null },
education: { strictRoute: null, eventJudgment: null },
migration: { strictRoute: null, eventJudgment: null },
family: { strictRoute: null, eventJudgment: null },
@@ -161,6 +161,7 @@ export function consultationMethodologyForDomains(
};
push("Shared mandatory baseline", ROUTER_FILE, markdownSection(router, SHARED_BASELINE_HEADING));
push("Full-spectrum invocation", ROUTER_FILE, markdownSection(router, "Full-Spectrum Invocation Contract"));
push("Event judgment skeleton", SKELETON_FILE, packageFile(SKELETON_FILE));
const withoutChecklist: ConsultationDomain[] = [];
+1 -7
View File
@@ -58,12 +58,6 @@ function domainsForTheme(theme: ConsultationPlanTheme): ConsultationPlan["reques
return [...consultationDomainDefinition(theme).workflowThemes];
}
function evidenceForTheme(theme: ConsultationPlanTheme): ConsultationPlan["requiredEvidenceCategories"] {
return theme === "timing" || theme === "annual"
? ["natal_foundation", "timing", "validation"]
: ["natal_foundation", "domain"];
}
function deepFreeze<T>(value: T): DeepReadonly<T> {
if (value && typeof value === "object" && !Object.isFrozen(value)) {
for (const child of Object.values(value)) deepFreeze(child);
@@ -108,7 +102,7 @@ export function createConsultationPlan(input: {
requestedDomains: domainsForTheme(theme),
depth: input.depth ?? "standard",
timingHorizon: input.timingHorizon ?? (theme === "timing" || theme === "annual" ? "next_12_months" : null),
requiredEvidenceCategories: evidenceForTheme(theme),
requiredEvidenceCategories: [...consultationEvidenceCategoryValues],
birthTimeMode: consultationMode,
precisionBoundary: precisionBoundaryFor(consultationMode),
maxCreditCost: consultationPlanMaxCreditCost,
+7 -1
View File
@@ -403,6 +403,9 @@ function mergeConsultationEvidencePackets(
.filter((value): value is string => typeof value === "string" && value.trim().length > 0))];
const boundaries = [...new Set(packets.map((packet) => packet.rectification.boundary))];
const policy = mergeConsultationAnswerPolicies(contracts.map((contract) => evidenceRecord(contract.answer_policy)));
const audit = contracts.find((contract) => Array.isArray(contract.technique_audit_table))?.technique_audit_table;
const varga = contracts.find((contract) => contract.varga_spectrum !== undefined)?.varga_spectrum;
const western = contracts.find((contract) => contract.western_spectrum !== undefined)?.western_spectrum;
return consultationEvidencePacketSchema.parse({
packet_version: "consultation-evidence-packet-v2",
question: packets.find((packet) => typeof packet.question === "string")?.question,
@@ -426,6 +429,9 @@ function mergeConsultationEvidencePackets(
...(limitations.length > 0
? { user_facing_limitation: limitations.join(" ").slice(0, 800) }
: {}),
...(audit !== undefined ? { technique_audit_table: audit } : {}),
...(varga !== undefined ? { varga_spectrum: varga } : {}),
...(western !== undefined ? { western_spectrum: western } : {}),
},
claim_cards: options.claimCards,
// `not_auto_rectified` is the restrictive boundary, so one domain reporting
@@ -500,7 +506,7 @@ export function createConsultationTools(ctx: ConsultationAgentContext) {
let calculation: Promise<ReturnType<typeof toModelDomainPlanContext>> | null = null;
const consultationTool = createTool({
id: "run-jyotish-consultation",
description: `Run one server-validated plan of at most ${MAX_CONSULTATION_DOMAINS} allowlisted personal Jyotish consultation domains. Send only question and domains. The single ordered domains array is the only way to select domains: list them in priority order, or omit it entirely to accept the domain the server already selected for this consultation. Use only the ids enumerated in the schema; workflow or checklist names from the skill's methodology are not domain ids. Domains execute one after another and each costs about ${Math.round(CONSULTATION_DOMAIN_DURATION_MS / 1000)}s of the run's wall clock, so a shorter plan leaves more time to write the answer; if the clock runs short the server executes the domains that fit and returns the rest in omitted_domains. Birth data is server-bound and must never be supplied. The result always carries one top-level answer contract—status, evidence_contract, claim_cards, rectification—which for several domains is the most restrictive merge of the executed ones, with per-domain detail in consultations. One calculation is executed per request and reused, so repeating the call with different parameters cannot change the result.`,
description: `Run one server-validated plan of at most ${MAX_CONSULTATION_DOMAINS} allowlisted personal Jyotish consultation domains. Send only question and domains. The single ordered domains array is the only way to select domains: list every domain the question needs, in priority order, or omit it entirely to accept the domain the server already selected. Do not drop a relevant domain to shorten the plan. Use only the ids enumerated in the schema; workflow or checklist names from the skill's methodology are not domain ids. Domains execute one after another and each costs about ${Math.round(CONSULTATION_DOMAIN_DURATION_MS / 1000)}s of the run's wall clock; if the clock runs short the server executes the domains that fit and returns the rest in omitted_domains. Birth data is server-bound and must never be supplied. The result always carries one top-level answer contract—status, evidence_contract, claim_cards, rectification—which for several domains is the most restrictive merge of the executed ones, with per-domain detail in consultations. One calculation is executed per request and reused, so repeating the call with different parameters cannot change the result.`,
inputSchema: consultationToolInputSchema,
execute: async (input, context) => {
const requestedDomains = canonicalDomainPlan(input, ctx);
+143 -10
View File
@@ -173,6 +173,9 @@ export const consultationEvidencePacketSchema = z.object({
hard_blockers: modelOutputEvidenceSchema.optional(),
answer_policy: modelOutputEvidenceSchema.optional(),
user_facing_limitation: modelOutputEvidenceSchema.optional(),
technique_audit_table: modelOutputEvidenceSchema.optional(),
varga_spectrum: modelOutputEvidenceSchema.optional(),
western_spectrum: modelOutputEvidenceSchema.optional(),
}),
claim_cards: z.array(z.object({
category: z.enum(consultationEvidenceCategoryValues),
@@ -242,6 +245,8 @@ const natalFoundationKeys = new Set([
"isretrograde", "dignity", "status", "total", "totalrupa", "rupa", "ratio", "required",
"rank", "strength", "score", "value", "sav", "bav", "points", "count", "housescores",
"components", "matrix", "matrixshape", "signs", "sources", "source", "method", "boundary",
"functionalbeneficmalefic", "functionalbenefics", "functionalmalefics", "functionalneutrals",
"yogakarakas", "ownedhouses", "ascendantsign", "effectonconfidence",
]);
const timingKeys = new Set([
@@ -268,14 +273,123 @@ const domainDetailsKeys = new Set([
"current", "summary", "interpretation", "description", "note", "status", "confidence",
]);
function projectVargaChart(value: unknown): ModelOutputValue | undefined {
const chart = record(value);
const planetsIn = record(chart.planets);
const planets: { [key: string]: ModelOutputValue } = {};
for (const [name, sign] of Object.entries(planetsIn)) {
const projected = boundedPrimitive(sign);
if (projected !== undefined) planets[name] = projected;
}
const output: { [key: string]: ModelOutputValue } = {};
const lagna = boundedPrimitive(chart.lagna);
if (lagna !== undefined) output.lagna = lagna;
if (Object.keys(planets).length) output.planets = planets;
for (const key of ["name", "meaning", "boundary"] as const) {
const projected = boundedPrimitive(chart[key]);
if (projected !== undefined) output[key] = projected;
}
return Object.keys(output).length ? output : undefined;
}
function projectVargaGroup(value: unknown): ModelOutputValue | undefined {
const group = record(value);
const output: { [key: string]: ModelOutputValue } = {};
for (const [code, chart] of Object.entries(group)) {
const projected = projectVargaChart(chart);
if (projected !== undefined) output[code] = projected;
}
return Object.keys(output).length ? output : undefined;
}
function projectVargaSpectrum(value: unknown): ModelOutputValue | undefined {
const spectrum = record(value);
if (!Object.keys(spectrum).length) return undefined;
const output: { [key: string]: ModelOutputValue } = {};
for (const key of ["status", "mode", "boundary"] as const) {
const projected = boundedPrimitive(spectrum[key]);
if (projected !== undefined) output[key] = projected;
}
const counts = record(spectrum.counts);
if (Object.keys(counts).length) {
const compact: { [key: string]: ModelOutputValue } = {};
for (const [key, item] of Object.entries(counts)) {
const projected = boundedPrimitive(item);
if (projected !== undefined) compact[key] = projected;
}
if (Object.keys(compact).length) output.counts = compact;
}
for (const group of ["formal", "research_dn", "extended"] as const) {
const projected = projectVargaGroup(spectrum[group]);
if (projected !== undefined) output[group] = projected;
}
if (Array.isArray(spectrum.blocked)) {
output.blocked = spectrum.blocked.filter((item): item is string => typeof item === "string").slice(0, 60);
}
return Object.keys(output).length ? output : undefined;
}
function projectTechniqueAudit(value: unknown): ModelOutputValue | undefined {
if (!Array.isArray(value)) return undefined;
const rows = value.slice(0, 80).map((item) => {
const row = record(item);
const projected: { [key: string]: ModelOutputValue } = {};
for (const key of ["technique", "status", "system", "boundary"] as const) {
const scalar = boundedPrimitive(row[key]);
if (scalar !== undefined) projected[key] = scalar;
}
return projected;
}).filter((row) => Object.keys(row).length > 0);
return rows.length ? rows : undefined;
}
function projectWesternSpectrum(value: unknown): ModelOutputValue | undefined {
const spectrum = record(value);
if (!Object.keys(spectrum).length) return undefined;
const output: { [key: string]: ModelOutputValue } = {};
for (const key of ["status", "zodiac", "house_system", "boundary"] as const) {
const projected = boundedPrimitive(spectrum[key]);
if (projected !== undefined) output[key] = projected;
}
const natal = record(spectrum.natal);
if (Object.keys(natal).length) {
const compact: { [key: string]: ModelOutputValue } = {};
for (const [key, item] of Object.entries(natal)) {
const projected = boundedPrimitive(item);
if (projected !== undefined) compact[key] = projected;
}
if (Object.keys(compact).length) output.natal = compact;
}
const techniques = record(spectrum.techniques);
if (Object.keys(techniques).length) {
const compact: { [key: string]: ModelOutputValue } = {};
for (const [name, layer] of Object.entries(techniques)) {
const row = record(layer);
const projected: { [key: string]: ModelOutputValue } = {};
for (const key of ["status", "boundary"] as const) {
const scalar = boundedPrimitive(row[key]);
if (scalar !== undefined) projected[key] = scalar;
}
if (Object.keys(projected).length) compact[name] = projected;
}
if (Object.keys(compact).length) output.techniques = compact;
}
return Object.keys(output).length ? output : undefined;
}
function projectNatalFoundation(context: ReturnType<typeof toAgentConsultationContext>) {
return projectAllowlistedTree({
const foundation = projectAllowlistedTree({
ascendant: context.chart.ascendant,
planets: context.chart.planets,
houses: context.chart.houses,
shadbala: context.chart.shadbala,
ashtakavarga: context.chart.ashtakavarga,
functional_benefic_malefic: context.local_layers.functional_benefic_malefic,
}, natalFoundationKeys, { allowAstrologyEntityKeys: true });
const output = record(foundation);
const varga = projectVargaSpectrum(context.local_layers.varga_spectrum);
if (varga !== undefined) output.varga_spectrum = varga;
return Object.keys(output).length ? output : undefined;
}
function projectDomainEvidence(value: unknown): ModelOutputValue {
@@ -315,30 +429,41 @@ function projectDomainEvidence(value: unknown): ModelOutputValue {
}
function projectTimingEvidence(context: ReturnType<typeof toAgentConsultationContext>) {
return projectAllowlistedTree({
const timing = projectAllowlistedTree({
dasha: context.chart.dasha,
dasha_sub_periods: context.local_layers.dasha_sub_periods,
narayana_dasha: context.local_layers.narayana_dasha,
}, timingKeys, { allowAstrologyEntityKeys: true });
const output = record(timing);
const western = projectWesternSpectrum(context.evidence_contract.western_spectrum);
if (western !== undefined) output.western_spectrum = western;
return Object.keys(output).length ? output : undefined;
}
function projectValidationEvidence(context: ReturnType<typeof toAgentConsultationContext>) {
return projectAllowlistedTree({
const validation = projectAllowlistedTree({
technique_truth: context.evidence_contract.technique_truth,
reference_transparency: context.reference_transparency,
shadbala_boundary: context.local_layers.shadbala_boundary,
}, validationKeys, { allowAstrologyEntityKeys: true });
const output = record(validation);
const audit = projectTechniqueAudit(context.evidence_contract.technique_audit_table);
if (audit !== undefined) output.technique_audit_table = audit;
return Object.keys(output).length ? output : undefined;
}
function stringList(value: unknown) {
return Array.isArray(value) ? value.filter((item): item is string => typeof item === "string").slice(0, 24) : [];
function stringList(value: unknown, limit = 24) {
return Array.isArray(value) ? value.filter((item): item is string => typeof item === "string").slice(0, limit) : [];
}
function projectEvidenceContract(contract: ReturnType<typeof toAgentConsultationContext>["evidence_contract"]) {
const policy = record(contract.answer_policy);
const audit = projectTechniqueAudit(contract.technique_audit_table);
const varga = projectVargaSpectrum(contract.varga_spectrum);
const western = projectWesternSpectrum(contract.western_spectrum);
return {
available_layers: stringList(contract.available_layers),
missing_route_layers: stringList(contract.missing_route_layers),
available_layers: stringList(contract.available_layers, 80),
missing_route_layers: stringList(contract.missing_route_layers, 40),
hard_blockers: stringList(contract.hard_blockers),
answer_policy: {
can_answer_direction: policy.can_answer_direction === true,
@@ -350,6 +475,9 @@ function projectEvidenceContract(contract: ReturnType<typeof toAgentConsultation
...(typeof contract.user_facing_limitation === "string"
? { user_facing_limitation: boundedPrimitive(contract.user_facing_limitation) }
: {}),
...(audit !== undefined ? { technique_audit_table: audit } : {}),
...(varga !== undefined ? { varga_spectrum: varga } : {}),
...(western !== undefined ? { western_spectrum: western } : {}),
};
}
@@ -426,6 +554,9 @@ export function toAgentConsultationContext(data: JsonRecord) {
commercial_evidence_status: consumerContext.commercial_evidence_status,
answer_policy: consumerContext.answer_policy,
user_facing_limitation: consumerContext.user_facing_limitation,
technique_audit_table: consumerContext.technique_audit_table,
varga_spectrum: consumerContext.varga_spectrum,
western_spectrum: consumerContext.western_spectrum,
},
chart: {
birth: chart.birth, ascendant: chart.ascendant, planets: chart.planets, houses: chart.houses,
@@ -433,12 +564,14 @@ export function toAgentConsultationContext(data: JsonRecord) {
},
local_layers: {
shadbala_boundary: "Shadbala is a locally consistent relative-strength layer; external component-level absolute parity remains partial and must not be stated as closed.",
varga_full: modules.varga_full, arudha_padas: modules.arudha_padas, ashtakavarga: modules.ashtakavarga,
varga_full: modules.varga_full, varga_spectrum: modules.varga_spectrum, arudha_padas: modules.arudha_padas, ashtakavarga: modules.ashtakavarga,
// `modules.dasha_boundaries` was read here for months and never written by the engine, so
// every answer was composed without sub-period boundaries while the receipt still reported
// precise timing as allowed. The field now names the layer the engine actually attaches.
dasha_sub_periods: modules.dasha_sub_periods, narayana_dasha: modules.narayana_dasha,
functional_benefic_malefic: record(data.machine_evidence_packet).functional_benefic_malefic,
dasha_sub_periods: modules.dasha_sub_periods, narayana_dasha: modules.narayana_dasha, gulika: modules.gulika,
kakshya: modules.kakshya,
functional_benefic_malefic: modules.functional_benefic_malefic
|| record(data.machine_evidence_packet).functional_benefic_malefic,
},
rectification: {
boundary: "not_auto_rectified", summary: rectification.summary,
+15 -16
View File
@@ -25,10 +25,11 @@ if (
) {
throw new Error("Active Jyotish Skill package is not in the canonical versioned layout");
}
// Agents read method from the hash-verified package, not from this working-tree
// view. The view is still checked against it so that editing the canonical
// entrypoint without publishing a version fails the build instead of quietly
// leaving the repository and the running product on different method.
// Agents read method from the hash-verified package, not from the working-tree
// view (that view's references/ is a symlink to hundreds of extra files).
// Canonical SKILL.md must stay byte-equal to the active package entrypoint;
// after editing either copy, update the other and recompute the registry sha256.
// The hash is an integrity check, not a freeze that requires a new version.
if (
!readFileSync(resolve(jyotishSkillPath, "SKILL.md")).equals(
readFileSync(resolve(jyotishSkillPackage.resolvedPath, "SKILL.md")),
@@ -41,10 +42,10 @@ const jyotishInstructions = `You are the guide for a conversational Vedic astrol
${productConversationVoice}
Write in concise Simplified Chinese as a natural conversation, not a report or fixed template. Use Markdown only when it improves scanning; tables are allowed only for genuinely comparative information.
${jyotishSkillMethodBlock}
Everything below is this product's runtime contract and overrides the skill method wherever the two differ. Use run-jyotish-consultation for actual chart calculations instead of inventing results, and let VISIBLE VOICE above own the visible chat format rather than the skill's report template.
The bound skill method is this product's answering contract. Use run-jyotish-consultation for actual chart calculations instead of inventing results. VISIBLE VOICE owns the spoken chat shape; do not paste the skill's formal-report skeleton as the chat format.
For questions that require a new chart claim, call run-jyotish-consultation before answering. Simple conversational follow-ups may use the existing context.
Select consultation domains only through the single ordered domains array of run-jyotish-consultation, whether the question covers one domain or several; omit it to accept the domain the server already selected. At most ${MAX_CONSULTATION_DOMAINS} domains may be requested in one run, because they are calculated one after another inside a fixed time budget: list them in priority order and prefer the smallest plan that answers the question, since every extra domain takes time away from writing the answer. The server canonicalizes aliases, rejects unsupported/product domains, executes each accepted domain, and returns the actual domains in the tool context and receipt. The only legal domain ids are the ones enumerated in that array's schema; the skill's methodology names strict-workflow checklists such as career-timing-strict, and those labels select techniques inside the skill, never domains for this tool. A rejected domain plan is final for this run: correct the domains once, and never re-send the same call with extra parameters.
The tool result's methodology field is the skill's own strict checklist for the routes that actually ran, quoted from the pinned skill version. Treat it as the method for this answer, not as background: work through its mandatory modules against the evidence you were given, and obey its output discipline, including any instruction to separate kinds of claim rather than merge them into one vague statement. Those sections are already delivered, so never spend a turn re-reading them; methodology.further_reading lists the references the skill names, and you may read one with skill_read only when the question needs something the delivered sections do not cover. When methodology.domains_without_strict_checklist names a domain, the skill declares no strict checklist for it: use the shared baseline and do not imply a strict route was followed. When methodology is absent, follow the bound skill method above.
Select consultation domains only through the single ordered domains array of run-jyotish-consultation, whether the question covers one domain or several; omit it to accept the domain the server already selected. At most ${MAX_CONSULTATION_DOMAINS} domains may be requested in one run, because they are calculated one after another inside a fixed time budget: list every domain the question actually needs, in priority order. Do not drop a relevant domain to keep the plan short—the natal compute already ran the full technique spectrum, and omitting a domain omits that route's checklist from the answer. The server canonicalizes aliases, rejects unsupported/product domains, executes each accepted domain, and returns the actual domains in the tool context and receipt. The only legal domain ids are the ones enumerated in that array's schema; the skill's methodology names strict-workflow checklists such as career-timing-strict, and those labels select techniques inside the skill, never domains for this tool. A rejected domain plan is final for this run: correct the domains once, and never re-send the same call with extra parameters.
The tool result's methodology field is the skill's own strict checklist for the routes that actually ran, quoted from the pinned skill version. Treat it as the method for this answer, not as background: work through its mandatory modules against the evidence you were given, and obey its output discipline, including any instruction to separate kinds of claim rather than merge them into one vague statement. Those sections are already delivered, so never spend a turn re-reading them; methodology.further_reading lists the references the skill names, and you may read one with skill_read only when the question needs something the delivered sections do not cover. When methodology.domains_without_strict_checklist names a domain, the skill declares no named checklist for it: still follow the delivered Full-spectrum invocation and shared baseline, and do not imply a named strict route was followed. When methodology is absent, follow the bound skill method above.
The tool result always carries one top-level answer contract—status, evidence_contract, claim_cards, rectification—even when several domains ran. For a multi-domain plan that top level is the most restrictive merge of the executed domains, so obey it exactly as written and read consultations only for per-domain detail. Never treat an absent top-level field as permission to answer without a contract.
When omitted_domains is non-empty, do not answer those domains and never present the reply as covering the whole plan. Stay with what was calculated. Do not announce a skipped-domain inventory or say this round was incomplete unless the user asked about coverage.
Activity, progress, tool status, and execution receipts are server-owned. Never imitate data-jyotish-activity, activity events, tool-started/tool-completed messages, or receipts in the answer text.
@@ -52,7 +53,7 @@ Treat the server-provided current time as authoritative for words such as today,
Treat the tool result's top-level status and evidence_contract as the authoritative answer policy:
- When status is ready and evidence_contract.answer_policy.can_answer_direction is true, answer the user's actual question directly. Do not begin with infrastructure or confidence disclaimers.
- An unavailable optional provider or external cross-check is not a calculation failure. Never call it an internal error.
- Do not mention VedAstro, snapshot, fallback, gateway, archive, provider, MEVG, or calibration unless the user explicitly asks about methodology, or the missing layer materially blocks the exact claim they requested.
- Do not mention VedAstro, snapshot, fallback, gateway, archive, provider, MEVG, or calibration unless the user explicitly asks about methodology, or the missing layer materially blocks the exact claim they requested. The required Technique Audit Table may list those rows by their delivered labels without discussing provider internals.
When reference_transparency is present:
- Present candidate_windows and exact_triggers when relevant, but describe exact_triggers as technical trigger points, never guaranteed events.
@@ -66,7 +67,9 @@ When reference_transparency is present:
- When consulting references/oracle/effective_skill_capability_view_2026_07_19.json or any derived skill map, use effective_status, not registry_status. Do not promote reference_only or blocked techniques into mastered/covered claims.
- If should_lead_with_limitations is false, do not lead with limitations. If a limitation is relevant, put it in one short sentence at the end.
- Only say the chart calculation failed when evidence_contract.hard_blockers is non-empty.
- Never claim D2, D11, D9, D10, A10, UL, or Narayana Dasha is missing when it appears in evidence_contract.available_layers, chart, or local_layers.
- Never claim D2, D11, D9, D10, A10, UL, Narayana Dasha, a delivered Varga, or a delivered Western layer is missing when it appears in evidence_contract.available_layers, evidence_contract.varga_spectrum, evidence_contract.western_spectrum, chart, or local_layers.
- evidence_contract.technique_audit_table is the invocation record for this run. Use every executed layer that is relevant to the question. Status executed means the server ran it; blocked means it could not; not_applicable means it does not apply. Never treat an omitted row as permission to invent the technique, and never silently skip an executed layer the question needs.
- The visible answer must end with that Technique Audit Table in spoken Chinese labels (已执行 / 阻塞 / 不适用). This is the one table VISIBLE VOICE requires; do not also dump JSON.
- local_layers.dasha_sub_periods carries the antardasha boundaries inside the running mahadasha. When it is present, use those boundaries and never say sub-periods were not calculated; when it is absent, say so once instead of implying the calculation broke.
- Treat evidence_contract.answer_policy as a hard output contract. When can_answer_precise_timing is false, provide only direction or structure and do not state a month, date, or guaranteed timing outcome.
- Treat answer_policy.deterministic_claims_forbidden_for as a hard prohibition. Do not use a restricted technique to make a deterministic conclusion. reference_only, partial, blocked, research_only_blocked, and partial_registry_only are commercial claim boundaries, not validated capabilities.
@@ -105,8 +108,7 @@ ${JSON.stringify(toAgentConsultationContext(workflowContext))}
const generalJyotishInstructions = `You are the guide for a conversational Vedic astrology product.
${productConversationVoice}
${jyotishSkillMethodBlock}
Everything below overrides that method wherever the two differ. This request explicitly has no usable birth minute. Never calculate, infer, or claim a personal birth chart, ascendant, house, divisional chart, dasha, transit timing, or personal prediction. You have no chart tools for this mode.
This request explicitly has no usable birth minute. Never calculate, infer, or claim a personal birth chart, ascendant, house, divisional chart, dasha, transit timing, or personal prediction. You have no chart tools for this mode.
Answer general educational questions that do not depend on the user's natal chart. A homepage daily request may also include a server-owned <public-daily-panchanga> block. In that one case, explain the public calendar trend, suitable actions, cautions, and one practical next step from that block only. State concisely that it is a public-day reference rather than a personal natal forecast; do not reject the whole request merely because the birth minute is unavailable.
If a request asks for a personal chart conclusion, personal timing, compatibility, or forecast without that public daily evidence, clearly say that this mode cannot answer it and offer exactly two safe next steps: ask a general-knowledge question, or complete birth-time rectification. Do not invent 00:00, a period midpoint, or any other substitute minute.
Never turn public Panchanga into claims about the user's ascendant, houses, dasha, natal transits, guaranteed outcomes, or exact event timing. Do not invent or alter Panchanga fields that the server did not provide.
@@ -123,7 +125,6 @@ export function getGeneralJyotishAgent(model: ResolvedLanguageModel) {
name: "Jyotisha General Guide",
model: model.model,
instructions: generalJyotishInstructions,
...jyotishSkillBinding(),
});
generalJyotishAgents.set(model.id, agent);
return agent;
@@ -131,9 +132,8 @@ export function getGeneralJyotishAgent(model: ResolvedLanguageModel) {
const onboardingInstructions = `You create the first conversational turn for Jyotisha, a Vedic astrology chat product.
${jyotishSkillMethodBlock}
Follow that method's scope and truth boundaries when suggesting questions; everything below overrides it wherever the two differ.
This is onboarding, not a chart reading: do not calculate, infer, or claim placements, timing windows, personality traits, relationship outcomes, or career conclusions.
The suggested questions must stay inside what this product can later answer from a natal chart. Never suggest medical, legal, investment, death, diagnosis, pregnancy, or exact-date promises.
Return valid JSON only. Do not use Markdown fences, commentary, or hidden fields.
The JSON shape must be:
{"suggestions":[{"theme":"服务器给出的主题 id","text":"问题"}]}
@@ -141,7 +141,7 @@ The server lists the required themes. Return one question per listed theme, in e
Write every question as the user's own first-person request and include “我”, such as “请帮我看看……”. Each question must ask for useful help with the user's situation, not for a lesson about astrology, and must stay under 40 Chinese characters.
Keep each question specific to its own theme so the set does not read as rewordings of one another. Never mention birth data, profile readiness, setup completion, or system processing.
Never generate detached or encyclopedic wording such as “印度占星一般如何……”, “通常会看哪些因素”, “包含哪些证据层”, or “如何划分主题”.
The questions must use everyday Simplified Chinese and be answerable through the skill. Avoid jargon, fear, deterministic promises, medical/legal/investment claims, and unsupported precision.`;
The questions must use everyday Simplified Chinese and be answerable as a later natal-chart consultation. Avoid jargon, fear, deterministic promises, medical/legal/investment claims, and unsupported precision.`;
const onboardingAgents = new Map<string, Agent>();
@@ -153,7 +153,6 @@ export function getOnboardingAgent(model: ResolvedLanguageModel) {
name: "Jyotisha Onboarding Guide",
model: model.model,
instructions: onboardingInstructions,
...jyotishSkillBinding(),
});
onboardingAgents.set(model.id, agent);
return agent;
+10 -7
View File
@@ -1,18 +1,21 @@
export const productConversationVoice = `VISIBLE VOICE (overrides skill report format)
This product is a private conversation with one person, not a professional briefing.
The jyotish-vedic-astrology skill and any JSON context govern METHOD and TRUTH BOUNDARIES only. They must not govern visible format. Never copy the skill's report structure, Technique Audit Table, evidence_packet layout, guided_topics list, or “先展示原始结构再综合” output into the chat.
export const productConversationVoice = `VISIBLE VOICE (spoken Chinese; same technique density as the local skill)
This product is a private conversation with one person. The jyotish-vedic-astrology skill governs METHOD, TECHNIQUE INVOCATION, and TRUTH BOUNDARIES. Chat format may be spoken Chinese rather than the skill's formal-report skeleton, but it must not drop techniques, raw structures, or audit rows the server delivered.
Write spoken Simplified Chinese, addressing the reader as 你.
- Answer the user's actual question in the first sentence.
- Sound like a calm, specific person: vary sentence length, prefer one concrete reading over a tour of the chart, and gloss a technical term once in plain Chinese (婚姻分盘(D9, 当前大运) before using the everyday phrase.
- Prefer plain paragraphs. Use Markdown only to compare two concrete options; never lead with headings, numbered methodology, or a bullet inventory of the chart.
- If a limitation materially changes the claim, put it in one short closing sentence; otherwise omit it.
- Then use the delivered evidence the way the local skill does: unified parameters and raw structure first (degrees, houses, vargas, Dasha boundaries, Shadbala/AV, functional benefic/malefic, Western layers), then parallel systems, conflicts, and only then a conditional synthesis.
- Gloss a technical term once in plain Chinese (婚姻分盘(D9, 当前大运) before using the everyday phrase.
- Prefer paragraphs. Use Markdown to compare concrete options, to show raw structure that would otherwise be a wall of numbers, and for the required Technique Audit Table at the end.
- If a limitation materially changes the claim, put it in one short closing sentence before the table; otherwise omit it.
After the spoken answer, include exactly one compact Markdown Technique Audit Table with columns 技法 / 状态 / 说明. Copy rows from the delivered technique_audit_table. Translate status to 已执行 / 阻塞 / 不适用. Do not invent, omit, or upgrade a row. Do not dump JSON keys or YAML.
Never do these in the visible answer:
- Quote JSON keys, English status codes, or field names (core_status, blocked, ready, consumer_context, missing_layers, omitted_domains).
- Say 根据系统计算, 根据当前计算结果, 综合来看, 需要指出的是, 当前状态为, 本轮未完整计算, or 从星盘来看 as an opener.
- Restate the user's question, pad to a template length, or use a fixed 首先 / 其次 / 最后 skeleton.
- Dump a Technique Audit Table, YAML, JSON, or an inventory of what was or was not computed this round.
- Dump YAML, JSON, or a second inventory besides that one table.
- Skip an executed layer the question needs in order to keep the answer short.
Bad: 根据当前计算结果,事业方向总体偏稳定,但副运细节与行运触发未在本轮完整计算。
Good: 你更适合把已经积累的专业能力做成长期事业,而不是频繁换赛道。眼下更值得把现有工作做深,而不是立刻另起一摊。`;
+82 -12
View File
@@ -20,6 +20,47 @@ export const jyotishSkillPackage = skillPackage;
*/
export const jyotishSkillRuntimePath = resolveSkillPackageRuntimePath(skillPackage);
/**
* Headings from the commercial SKILL.md that govern answering a natal chart.
* The rest of that file is a local-agent / maintainer manual (CLI indexes,
* construction rules, celebrity case catalogs) and stays in the package for
* skill_read rather than being stuffed into every consultation prompt.
*/
const RUNTIME_METHOD_HEADINGS = [
"商业运行时路由",
"关联技法完整调取",
"强制工作流",
"五层硬约束",
"强制规则",
"当前最硬的未闭环点",
"核心方法论",
"强制规范速查",
"注意事项",
] as const;
const DROPPED_RUNTIME_SUBHEADINGS = ["开源复用边界冻结"] as const;
function publishedSkillBody(): string {
const raw = readFileSync(resolve(skillPackage.resolvedPath, "SKILL.md"), "utf8");
const frontmatter = /^---\r?\n[\s\S]*?\r?\n---\r?\n/.exec(raw);
return (frontmatter ? raw.slice(frontmatter[0].length) : raw).trim();
}
function dropSubheadings(section: string, dropped: readonly string[]): string {
const lines = section.split("\n");
const kept: string[] = [];
let skipping = false;
for (const line of lines) {
if (line.startsWith("### ")) {
skipping = dropped.some((heading) => line.includes(heading));
} else if (line.startsWith("## ")) {
skipping = false;
}
if (!skipping) kept.push(line);
}
return kept.join("\n").trim();
}
/**
* Mastra answers a skill activation with the entrypoint *and* a flat listing of
* every file in the package, which for this package measured 129,651 bytes: the
@@ -30,23 +71,51 @@ export const jyotishSkillRuntimePath = resolveSkillPackageRuntimePath(skillPacka
* turn and paid for that listing every time, twice more whenever a retry opened
* a fresh model loop.
*
* Binding the method into the system prompt is what the listing was standing in
* for. It also removes an activation the model could forget, which is what
* `runtime_contract_incomplete` was mostly reporting.
* Binding the runtime excerpt into the system prompt is what the listing was
* standing in for. It also removes an activation the model could forget, which
* is what `runtime_contract_incomplete` was mostly reporting. The excerpt is
* taken from the published commercial entrypoint, not from the research skill
* snapshot and not from the working tree.
*/
function boundMethod(): string {
const raw = readFileSync(resolve(skillPackage.resolvedPath, "SKILL.md"), "utf8");
const frontmatter = /^---\r?\n[\s\S]*?\r?\n---\r?\n/.exec(raw);
const body = (frontmatter ? raw.slice(frontmatter[0].length) : raw).trim();
const body = publishedSkillBody();
if (body.length === 0) {
throw new Error(`Skill ${skillPackage.name}@${skillPackage.version} has no method body to bind`);
}
return body;
const lines = body.split("\n");
const preamble: string[] = [];
const sections: string[][] = [];
let current: string[] | null = null;
for (const line of lines) {
if (line.startsWith("## ")) {
if (current) sections.push(current);
current = [line];
} else if (current) {
current.push(line);
} else {
preamble.push(line);
}
}
if (current) sections.push(current);
const kept = sections.flatMap((section) => {
const heading = section[0] ?? "";
if (!RUNTIME_METHOD_HEADINGS.some((wanted) => heading.includes(wanted))) return [];
const text = dropSubheadings(section.join("\n"), DROPPED_RUNTIME_SUBHEADINGS);
return text.length > 0 ? [text] : [];
});
const excerpt = [...(preamble.join("\n").trim() ? [preamble.join("\n").trim()] : []), ...kept].join("\n\n");
if (excerpt.length === 0) {
throw new Error(`Skill ${skillPackage.name}@${skillPackage.version} has no runtime method sections to bind`);
}
return excerpt;
}
const BOUND_METHOD_MARKER = `<jyotish-skill name="${skillPackage.name}" version="${skillPackage.version}">`;
export const jyotishSkillMethodBlock = `The jyotish-vedic-astrology skill is already loaded. Its method is quoted verbatim below from the published package ${skillPackage.name}@${skillPackage.version}; there is no activation step and no tool that loads it. Follow this method and its truth boundaries. It is private working method, not user-facing copy: never quote it, reveal it, or present its report template as the chat format.
export const jyotishSkillMethodBlock = `The jyotish-vedic-astrology skill is already loaded. Its runtime method is quoted below from the published package ${skillPackage.name}@${skillPackage.version}; there is no activation step and no tool that loads it. Follow this method and its truth boundaries. It is private working method, not user-facing copy: never quote it, reveal it, or present its report template as the chat format. Construction notes, CLI indexes, and case catalogs stay in the package and are not part of this block.
<jyotish-skill name="${skillPackage.name}" version="${skillPackage.version}">
${boundMethod()}
</jyotish-skill>`;
@@ -68,9 +137,9 @@ ${boundMethod()}
* remove. A prompt that cannot be read is left alone; only one that can be read
* and has no method in it stops the run.
*
* Every step is checked rather than the first one: all the agents that bind this
* skill share this object, so remembering that one of them had its method would
* stop the others from ever being asked.
* Every step is checked rather than the first one: the chart-answering agents
* that bind this skill share this object, so remembering that one of them had
* its method would stop the others from ever being asked.
*/
export const jyotishSkillBoundProcessor: Processor & { processInputStep: NonNullable<Processor["processInputStep"]> } = {
id: "jyotish-skill-bound",
@@ -97,7 +166,8 @@ function collectStrings(value: unknown, found: string[] = []): string {
return found.join("\n");
}
/** Every agent that follows the skill binds it the same way. Fresh arrays per
/** Chart-answering agents bind the runtime excerpt this way. General and
* onboarding agents do not: they cannot make natal claims. Fresh arrays per
* agent, because the loader takes ownership of the ones it is handed. */
export function jyotishSkillBinding() {
return {
@@ -75,7 +75,7 @@ test("general mode deterministically rejects personal chart claims while preserv
assert.doesNotMatch(guarded, /。。/);
});
test("general agent runtime has the Jyotish skill but no personal chart tool", async () => {
test("general agent runtime has no Jyotish skill package and no personal chart tool", async () => {
const model: ResolvedLanguageModel = {
id: "general-zero-tool-probe",
label: "General probe",
@@ -89,9 +89,10 @@ test("general agent runtime has the Jyotish skill but no personal chart tool", a
const skills = await agent.listSkills();
const toolNames = Object.keys(await agent.getToolsForExecution({ runId: "general-zero-tool-probe" }));
assert.equal(skills.some((skill) => skill.name === "jyotish-vedic-astrology"), true);
// The method is bound into the instructions, so there is no activation to call.
// This mode cannot make natal claims, so it does not pay for the chart method.
assert.equal(skills.some((skill) => skill.name === "jyotish-vedic-astrology"), false);
assert.equal(toolNames.includes("skill"), false);
assert.equal(toolNames.includes("skill_read"), false);
assert.equal(toolNames.includes("run-jyotish-consultation"), false);
const mastra = readFileSync(new URL("../src/mastra/index.ts", import.meta.url), "utf8");
@@ -99,7 +100,7 @@ test("general agent runtime has the Jyotish skill but no personal chart tool", a
mastra.indexOf("export function getGeneralJyotishAgent"),
mastra.indexOf("const onboardingInstructions"),
);
assert.match(generalFactory, /\.\.\.jyotishSkillBinding\(\)/);
assert.doesNotMatch(generalFactory, /jyotishSkillBinding|jyotishSkillMethodBlock/);
assert.doesNotMatch(generalFactory, /run-jyotish-consultation|createConsultationTools/);
});
@@ -120,7 +121,7 @@ test("consult route validates mode before billing and general mode uses no chart
mastra.indexOf("export function getGeneralJyotishAgent"),
mastra.indexOf("const onboardingInstructions"),
);
assert.match(generalFactory, /\.\.\.jyotishSkillBinding\(\)/);
assert.doesNotMatch(generalFactory, /jyotishSkillBinding|jyotishSkillMethodBlock/);
assert.doesNotMatch(generalFactory, /run-jyotish-consultation|createConsultationTools/);
});
+39 -3
View File
@@ -43,7 +43,9 @@ test("keeps strength, Ashtakavarga, and timing evidence available to the answer
// that key. tests/test_consultation_consumer_context.py now compares the names
// across the boundary; this only pins that timing evidence is still passed.
assert.match(workflowSource, /dasha_sub_periods: modules\.dasha_sub_periods/);
assert.match(workflowSource, /narayana_dasha: modules\.narayana_dasha/);
assert.match(workflowSource, /varga_spectrum: modules\.varga_spectrum/);
assert.match(workflowSource, /technique_audit_table: consumerContext\.technique_audit_table/);
assert.match(agentSource, /evidence_contract\.technique_audit_table/);
assert.match(workflowSource, /evidence_contract:/);
assert.match(workflowSource, /missing_route_layers: consumerContext\.missing_route_layers/);
assert.match(workflowSource, /answer_policy: consumerContext\.answer_policy/);
@@ -72,12 +74,25 @@ test("projects only bounded server-selected evidence to the model", () => {
modules: {
shadbala: { total: 412 },
ashtakavarga: { total: 28 },
varga_spectrum: {
status: "used",
mode: "full_spectrum",
counts: { formal: 20, research_dn: 40, extended: 3, blocked: 0 },
formal: { D9: { lagna: "Leo", planets: { Sun: "Capricorn" }, name: "Navamsa" } },
research_dn: { D13: { lagna: "Aries", planets: { Moon: "Taurus" }, boundary: "generic D-N; not a formal named varga" } },
},
dasha_sub_periods: {
current: { antardasha: { lord: "Saturn", start: "2025-10-17", end: "2027-05-18" } },
boundary_count: 9,
utc_offset: "private-module-utc-offset",
},
narayana_dasha: { current: "Aries" },
functional_benefic_malefic: {
status: "used",
functional_benefics: ["Jupiter"],
functional_malefics: ["Mercury"],
},
kakshya: { status: "observation_only", boundary: "not verified day/month timing" },
},
},
routing: { primary_theme: "career" },
@@ -116,6 +131,20 @@ test("projects only bounded server-selected evidence to the model", () => {
entitlements: "private-policy-entitlements",
},
user_facing_limitation: "精确月份暂不可用",
technique_audit_table: [
{ technique: "Formal Vargas D1D60", status: "executed", system: "jyotish", boundary: "20/20 named traditional charts" },
{ technique: "Western natal (tropical)", status: "executed", system: "western" },
],
varga_spectrum: {
status: "used",
counts: { formal: 20, research_dn: 40 },
formal: { D9: { lagna: "Leo", planets: { Sun: "Capricorn" } } },
},
western_spectrum: {
status: "executed",
natal: { sun: "Aries", moon: "Taurus", ascendant: "Leo" },
techniques: { transits: { status: "executed" } },
},
},
reference_transparency: { similar_public_cases: { status: "public_context_only" } },
rectification: { summary: "none" },
@@ -158,7 +187,14 @@ test("projects only bounded server-selected evidence to the model", () => {
const answerPolicy = output.evidence_contract.answer_policy as Record<string, unknown>;
assert.equal(answerPolicy.can_answer_precise_timing, false);
assert.deepEqual(output.claim_cards.map((card) => card.category), ["natal_foundation", "domain", "timing", "validation"]);
assert.deepEqual(careerOutput.claim_cards.map((card) => card.category), ["natal_foundation", "domain"]);
assert.equal(JSON.stringify(careerOutput).includes('"dasha"'), false);
assert.deepEqual(careerOutput.claim_cards.map((card) => card.category), ["natal_foundation", "domain", "timing", "validation"]);
assert.equal(JSON.stringify(careerOutput).includes('"dasha"'), true);
assert.equal(serialized.includes("consultation-evidence-packet-v2"), true);
assert.equal(serialized.includes("Formal Vargas"), true);
assert.equal(serialized.includes("varga_spectrum"), true);
assert.equal(serialized.includes('"D13"'), true);
assert.equal(serialized.includes("western_spectrum"), true);
assert.equal(serialized.includes("technique_audit_table"), true);
assert.equal(serialized.includes("functional_benefics"), true);
assert.equal(serialized.includes("Jupiter"), true);
});
@@ -21,21 +21,39 @@ test("the route's own strict checklist reaches the answer, not just the package
methodology.sections.some((section) => section.title.includes("Shared mandatory baseline")),
"every route is read against the shared baseline",
);
assert.ok(
methodology.sections.some((section) => /Full-Spectrum Invocation Contract/.test(section.text)),
"the skill's full-spectrum contract must reach the web model",
);
assert.deepEqual(methodology.domains_without_strict_checklist, []);
});
test("a route the skill declares no checklist for is reported, not filled in with another route's", () => {
const methodology = consultationMethodologyForDomains(["health"]);
const methodology = consultationMethodologyForDomains(["education"]);
assert.ok(methodology);
assert.deepEqual(methodology.domains_without_strict_checklist, ["health"]);
assert.deepEqual(methodology.domains_without_strict_checklist, ["education"]);
assert.ok(
!methodology.sections.some((section) => section.title.includes("strict")),
!methodology.sections.some((section) => section.title.includes("timing-strict") || section.title.includes("verification-strict")),
"no strict section may be substituted for a route that has none",
);
assert.ok(
methodology.sections.some((section) => section.title.includes("Shared mandatory baseline")),
"the baseline still applies",
);
assert.ok(
methodology.sections.some((section) => section.title.includes("Full-spectrum")),
"full-spectrum invocation still applies when a domain has no named checklist",
);
});
test("health uses the skill's health-timing-strict checklist", () => {
const methodology = consultationMethodologyForDomains(["health"]);
assert.ok(methodology);
assert.deepEqual(methodology.domains_without_strict_checklist, []);
const health = methodology.sections.find((section) => section.title.includes("health-timing-strict"));
assert.ok(health);
assert.match(health.text, /D6/);
assert.match(health.text, /D30/);
});
test("a multi-domain plan carries every executed route's checklist once", () => {
+1 -1
View File
@@ -122,5 +122,5 @@ test("deep-freezes the server-owned plan and nested evidence lists", () => {
assert.throws(() => mutable.requiredEvidenceCategories.splice(0, 1), TypeError);
assert.equal(plan.precisionBoundary, "server_evidence_required");
assert.deepEqual(plan.requestedDomains, ["career"]);
assert.deepEqual(plan.requiredEvidenceCategories, ["natal_foundation", "domain"]);
assert.deepEqual(plan.requiredEvidenceCategories, ["natal_foundation", "domain", "timing", "validation"]);
});
@@ -7,23 +7,24 @@ const mastra = readFileSync(new URL("../src/mastra/index.ts", import.meta.url),
const route = readFileSync(new URL("../src/app/api/consult/route.ts", import.meta.url), "utf8");
const binding = readFileSync(new URL("../src/mastra/skill-binding.ts", import.meta.url), "utf8");
test("product voice overrides skill report format without relaxing truth boundaries", () => {
assert.match(voice, /VISIBLE VOICE \(overrides skill report format\)/);
assert.match(voice, /govern METHOD and TRUTH BOUNDARIES only/);
assert.match(voice, /Never copy the skill's report structure, Technique Audit Table/);
test("product voice keeps skill technique density without dumping JSON", () => {
assert.match(voice, /VISIBLE VOICE \(spoken Chinese; same technique density as the local skill\)/);
assert.match(voice, /governs METHOD, TECHNIQUE INVOCATION, and TRUTH BOUNDARIES/);
assert.match(voice, /must not drop techniques, raw structures, or audit rows/);
assert.match(voice, /unified parameters and raw structure first/);
assert.match(voice, /required Technique Audit Table/);
assert.match(voice, /已执行 \/ 阻塞 \/ 不适用/);
assert.match(voice, /Answer the user's actual question in the first sentence/);
assert.match(voice, /本轮未完整计算/);
assert.match(voice, /你更适合把已经积累的专业能力做成长期事业/);
assert.match(voice, /Skip an executed layer the question needs/);
assert.match(mastra, /productConversationVoice/);
assert.match(mastra, /let VISIBLE VOICE above own the visible chat format/);
assert.match(mastra, /VISIBLE VOICE owns the spoken chat shape/);
assert.match(mastra, /private working notes, not user-facing copy/);
// The method now arrives bound into the prompt rather than fetched by the
// model, so both halves of the precedence have to keep being stated: follow the
// method, and let this product's contract win where the two disagree.
assert.match(binding, /Follow this method and its truth boundaries/);
assert.match(binding, /never quote it, reveal it, or present its report template as the chat format/);
assert.match(mastra, /overrides the skill method wherever the two differ/);
assert.match(mastra, /The bound skill method is this product's answering contract/);
assert.match(mastra, /Do not use a restricted technique/);
assert.match(mastra, /Do not announce a skipped-domain inventory/);
assert.doesNotMatch(mastra, /Name the domains you did cover, say plainly that the remaining ones were not calculated/);
@@ -36,6 +37,6 @@ test("consult user turn no longer dumps tool JSON that the model would parrot",
assert.match(chartBranch, /请直接用口语回答下面的问题[\s\S]*?resolvedQuestion\.modelQuestion/);
assert.doesNotMatch(chartBranch, /JSON\.stringify\(toolInput\)/);
assert.doesNotMatch(chartBranch, /经过服务端校验的工具参数/);
assert.match(route, /请用口语直接回答,不要复述内部字段或报本轮算了什么/);
assert.match(route, /请用口语直接回答;回答末尾附上技法审计表,不要复述内部字段/);
assert.match(chartBranch, /await runConsultationWorkflow\(toolInput/);
});
@@ -100,7 +100,7 @@ test("timing input projects only legal private workflow fields", async () => {
assert.equal(body.claim_boundary, "candidate_day_month_window_only_until_holdout_passes");
assert.equal(body.precision_boundary, "server_evidence_required");
assert.deepEqual(body.requested_domains, ["timing"]);
assert.deepEqual(body.required_evidence_categories, ["natal_foundation", "timing", "validation"]);
assert.deepEqual(body.required_evidence_categories, ["natal_foundation", "domain", "timing", "validation"]);
assert.equal(input.question, "未来哪些阶段值得把握?");
assert.equal(input.theme, "timing");
});
+72 -5
View File
@@ -17,18 +17,59 @@ function probeAgent(options: Record<string, unknown>) {
return new Agent({ id: "skill-binding-probe", name: "Probe", model, instructions: "x", ...options } as never);
}
test("the method the model follows is the published one, not the working tree's", () => {
function publishedSkillBody() {
const published = readFileSync(resolve(jyotishSkillPackage.resolvedPath, "SKILL.md"), "utf8");
const body = published.slice(published.indexOf("---", 3) + 4).trim();
return published.slice(published.indexOf("---", 3) + 4).trim();
}
function boundMethodText() {
const opened = jyotishSkillMethodBlock.indexOf(`<jyotish-skill name="${jyotishSkillPackage.name}"`);
const closed = jyotishSkillMethodBlock.lastIndexOf("</jyotish-skill>");
assert.ok(opened >= 0 && closed > opened);
return jyotishSkillMethodBlock.slice(jyotishSkillMethodBlock.indexOf("\n", opened) + 1, closed).trim();
}
test("the method the model follows is the published one, not the working tree's", () => {
const body = publishedSkillBody();
const bound = boundMethodText();
assert.ok(jyotishSkillMethodBlock.includes(body));
assert.match(jyotishSkillMethodBlock, new RegExp(`version="${jyotishSkillPackage.version}"`));
// The route into the loader is the hash-checked package, so the reference and
// script listing the model can reach is the one the registry hash covers.
// Every bound line still comes from the hash-checked package. The route into
// the loader is that same package, so skill_read can only reach what the
// registry hash covers.
for (const line of bound.split("\n")) {
if (line.length === 0) continue;
assert.ok(body.includes(line), line.slice(0, 80));
}
assert.ok(jyotishSkillRuntimePath.includes(jyotishSkillPackage.sha256));
assert.ok(jyotishSkillRuntimePath.endsWith(jyotishSkillPackage.name));
});
test("the bound method is the runtime excerpt, not the maintainer manual", () => {
// Independent source: the commercial SKILL.md still contains both the
// answering contract and the repo-construction notes. Binding the whole
// file is what pushed Mastra past its 500-line warning; the excerpt keeps
// the contract and drops the notes.
const body = publishedSkillBody();
const bound = boundMethodText();
for (const heading of ["商业运行时路由", "关联技法完整调取", "强制工作流", "五层硬约束", "核心方法论", "注意事项", "commercial_skill_truth_overlay"]) {
assert.ok(body.includes(heading), heading);
assert.ok(bound.includes(heading), heading);
}
for (const phrase of ["全谱系真实调用", "P0/P1 观察层", "网页对话在口语回答末尾给出 Technique Audit Table", "静态分析10步"]) {
assert.ok(bound.includes(phrase), phrase);
}
for (const heading of ["施工判断原则", "冲顶路线", "37大子命令", "验证与错题体系", "开源复用边界冻结"]) {
assert.ok(body.includes(heading), heading);
assert.equal(bound.includes(heading), false, heading);
}
assert.ok(body.includes("~/.workbuddy/skills"));
assert.equal(bound.includes("~/.workbuddy/skills"), false);
assert.ok(bound.split("\n").length < 500, `${bound.split("\n").length} lines`);
assert.ok(bound.length < body.length * 0.7, `${bound.length} of ${body.length}`);
});
test("binding the method costs a fraction of activating the skill", async () => {
// What activation would have sent, measured rather than assumed: Mastra answers
// it with the entrypoint plus a flat listing of every file in the package, and
@@ -85,3 +126,29 @@ test("a prompt assembled without the method stops the run instead of answering",
assert.doesNotThrow(() => step([{ role: "system", content: jyotishSkillMethodBlock }]));
assert.doesNotThrow(() => step([]));
});
test("only chart-answering agents bind the method", () => {
const mastra = readFileSync(new URL("../src/mastra/index.ts", import.meta.url), "utf8");
const chart = mastra.slice(
mastra.indexOf("const jyotishInstructions"),
mastra.indexOf("const generalJyotishInstructions"),
);
const general = mastra.slice(
mastra.indexOf("const generalJyotishInstructions"),
mastra.indexOf("const onboardingInstructions"),
);
const onboarding = mastra.slice(
mastra.indexOf("const onboardingInstructions"),
mastra.indexOf("const dailyStarlanguageInstructions"),
);
const daily = mastra.slice(
mastra.indexOf("const dailyStarlanguageInstructions"),
mastra.indexOf("const birthTimeGuideInstructions"),
);
assert.match(chart, /\.\.\.jyotishSkillBinding\(\)/);
assert.match(chart, /\$\{jyotishSkillMethodBlock\}/);
for (const [name, source] of [["general", general], ["onboarding", onboarding], ["daily", daily]] as const) {
assert.doesNotMatch(source, /jyotishSkillBinding|jyotishSkillMethodBlock/, name);
}
});
+1 -1
View File
@@ -83,7 +83,7 @@ test("checked-in registry verifies all three active packages and returns absolut
{
name: "jyotish-vedic-astrology",
version: "6.9.14",
sha256: "d3d6d05c1da25bb684af3be591687f0321b9a3a69a9896454ea3e25b57b6071b",
sha256: "c44e6f500c671d18dd47d9714610b766fbeabe00ca5e495e5225bc2c97f5f105",
},
{
name: "jyotish-birth-time-rectification",
+46 -3
View File
@@ -58,7 +58,19 @@ Every Level 2+ chart reading must include `MEVG / Global Web Evidence` and
---
## 0.7 User-Led Reading Calibration Gate
## 0.7 Full-Spectrum Invocation Contract
For career, wealth, annual forecast, event timing, rectification, or a comprehensive reading, the system must **actually execute every available, question-relevant Indian and Western technique** before interpretation. A module merely existing in the repository, registry, or UI does not count as used.
1. Preserve every produced raw payload, varga, dasha, strength table, transit window, and cross-system difference. Information density must never be reduced because an oracle, formula variant, or holdout is incomplete.
2. Invoke the Indian core relevant to the route: D1 plus all relevant vargas; Vimshottari, Narayana, Chara Dasha, Yogini, Ashtottari, Kalachakra and Bhrigu Pada where implemented; Jaimini/Arudha; KP; Double Transit PAC; transit LL/7L; Bhava Chalit; Sudarshana; Tajika/Varshaphala/Muntha; Nakshatra overlay; strengths, AV, yoga, argala, and relevant sahams.
3. Invoke the Western core for the same birth data: tropical natal chart, transit-to-natal duration scan, Solar Return, secondary progressions, solar arc, converse progressions/arcs, midpoints, Lunar Return, and parans where implemented.
4. A technique requiring unavailable inputs (for example partner data, Prashna question time, or an electional target) must still appear in the Technique Audit Table as `blocked` with its required input. It must not be silently skipped.
5. Keep raw information and conditional interpretations visible. The only restricted content is an unsupported certainty claim: no inevitable event, no unsupported exact day, and no claim of cross-system verification when only one system ran.
6. The report must state per technique: `executed`, `blocked`, or `not_applicable`, its settings, raw-output path, and its effect on assertion strength. `not_executed` is a workflow failure for a relevant technique.
---
## 0.8 User-Led Reading Calibration Gate
This gate turns the user's follow-up interrogation into workflow control. It is
not a personality preference; it prevents vague readings, memory-based
@@ -116,6 +128,7 @@ declared.
| "When will career opportunities appear?" / job / project / public status / profession | `career-timing-strict` | Level 3 |
| Marriage / partner / relationship timing | `relationship-timing-strict` | Level 3 |
| Money / income / gains / assets | `wealth-timing-strict` | Level 2 or 3 |
| Health / illness / hospitalization / injury / recovery pressure | `health-timing-strict` | Level 2 or 3 |
| "Will this event happen?" / concrete yes-no / project landing | `event-timing-strict` | Level 3 |
| "Why did this past event happen?" / technique reliability | `event-verification-strict` | Level 3 |
| General natal reading | `full-reading-strict` | Level 2 |
@@ -131,7 +144,7 @@ For every Level 2+ reading, complete or explicitly mark unavailable:
- D1 Rashi: houses, lords, dignity, exact degrees, Nakshatra, dispositor chain.
- D9 Navamsa: dignity confirmation, Vargottama, major dignity reversals.
- Relevant divisional chart: D10 for career, D7 for children, D12 for family/ancestral themes, D2 for wealth, D24 for education.
- Relevant divisional chart: D10 for career, D7 for children, D12 for family/ancestral themes, D2 for wealth, D24 for education, D6/D8/D30 for health pressure and vulnerability review.
- Vimshottari Dasha: MD/AD/PD when timing is requested.
- Transit: Saturn/Jupiter/Rahu-Ketu, plus Moon trigger for month/day-level timing.
- Functional benefic/malefic status by Lagna.
@@ -227,7 +240,37 @@ Use when the user asks for concrete timing, yes/no, project approval, launch, mo
---
## 7. `event-verification-strict`
## 7. `health-timing-strict`
Use when the user asks about health pressure, vitality risks, hospitalization, injury, surgery periods, recovery windows, or whether a difficult health phase is being activated.
### Mandatory modules
- D1: Lagna, Lagna lord, Moon, Sun, Saturn, Mars, 6H, 8H, 12H, and their lords.
- D6: core health-difficulty divisional review.
- D8: obstacle / vulnerability review when the question involves crisis, chronicity, or sharp disruption.
- D30: pressure, misfortune, or acute-friction layer; do not silently omit it when the route is health.
- Vimshottari: MD/AD/PD activation of 1L/6L/8L/12L, Lagna, Moon, Sun, Saturn, Mars.
- Narayana Dasha cross-check before making timing emphasis.
- Functional benefic/malefic: explicit Lagna-based classification and conflict note if natural and functional roles diverge.
- Strength: Shadbala for Lagna lord, Moon, Sun, Saturn, Mars, and the main activated lords.
- Transit: Saturn/Jupiter/Rahu-Ketu to Lagna, Moon, 6H, 8H, 12H, and their lords.
- Boundary layer: keep a visible non-medical boundary; no diagnosis, no treatment claim, and no certainty language about bodily outcomes.
- Historical verification if the user provides past health-pressure dates or crisis windows.
### Health timing output
Always separate:
1. **Pressure window**: strain, exhaustion, anxiety, or destabilizing background.
2. **Event-risk window**: injury / hospitalization / disruption activation candidates.
3. **Recovery-support window**: easing, stabilization, or reduced pressure after the peak.
Do not present this route as medical advice, diagnosis, or treatment guidance.
---
## 8. `event-verification-strict`
Use when the user provides past events and asks whether the technique is reliable.
+11 -10
View File
@@ -7,6 +7,7 @@ from dataclasses import dataclass
from typing import Any
PLAN_VERSION = "consultation-plan-v2"
_FULL_SPECTRUM_EVIDENCE = ("natal_foundation", "domain", "timing", "validation")
_ALLOWED_DEPTHS = {"concise", "standard", "deep", "research"}
_ALLOWED_HORIZONS = {"next_3_months", "next_12_months", "next_24_months", "long_term"}
_ALLOWED_PRECISION_BOUNDARIES = {"server_evidence_required", "precise_timing_blocked"}
@@ -38,7 +39,7 @@ _ROUTE_CONTRACTS = {
themes=("career",),
resolved_routes=("career",),
requested_domains=("career",),
required_evidence_categories=("natal_foundation", "domain"),
required_evidence_categories=_FULL_SPECTRUM_EVIDENCE,
required_layers=("D1", "D10", "10th house/lord", "A10", "AmK", "Vimshottari", "Narayana", "Transit"),
claim_boundary="career_direction_and_broad_timing_only",
),
@@ -46,7 +47,7 @@ _ROUTE_CONTRACTS = {
themes=("marriage",),
resolved_routes=("marriage", "relationship"),
requested_domains=("marriage",),
required_evidence_categories=("natal_foundation", "domain"),
required_evidence_categories=_FULL_SPECTRUM_EVIDENCE,
required_layers=("D1", "D9", "7th house/lord", "Venus/Jupiter", "DK", "UL", "A7", "Vimshottari", "Narayana", "Transit"),
claim_boundary="relationship_pattern_and_broad_window_only",
),
@@ -54,7 +55,7 @@ _ROUTE_CONTRACTS = {
themes=("wealth",),
resolved_routes=("wealth", "finance"),
requested_domains=("wealth",),
required_evidence_categories=("natal_foundation", "domain"),
required_evidence_categories=_FULL_SPECTRUM_EVIDENCE,
required_layers=("D1", "D2", "D11", "2nd/11th/9th/5th houses", "Wealth Yogas", "Ashtakavarga", "Dasha"),
claim_boundary="wealth_structure_not_financial_advice",
),
@@ -62,7 +63,7 @@ _ROUTE_CONTRACTS = {
themes=("health",),
resolved_routes=("health",),
requested_domains=("health",),
required_evidence_categories=("natal_foundation", "domain"),
required_evidence_categories=_FULL_SPECTRUM_EVIDENCE,
required_layers=("D1", "D6", "D8", "6th/8th houses", "Dasha", "non-medical boundary"),
claim_boundary="wellbeing_pressure_patterns_not_medical_diagnosis",
),
@@ -70,7 +71,7 @@ _ROUTE_CONTRACTS = {
themes=("education",),
resolved_routes=("education",),
requested_domains=("education",),
required_evidence_categories=("natal_foundation", "domain"),
required_evidence_categories=_FULL_SPECTRUM_EVIDENCE,
required_layers=("D1", "D24", "5th/9th houses", "Mercury/Jupiter", "Dasha"),
claim_boundary="learning_pattern_and_broad_timing_only",
),
@@ -78,7 +79,7 @@ _ROUTE_CONTRACTS = {
themes=("migration",),
resolved_routes=("migration",),
requested_domains=("migration",),
required_evidence_categories=("natal_foundation", "domain"),
required_evidence_categories=_FULL_SPECTRUM_EVIDENCE,
required_layers=("D1", "D4", "D12", "4th/12th houses", "Dasha", "Narayana"),
claim_boundary="migration_and_home_direction_broad_window_only",
),
@@ -86,7 +87,7 @@ _ROUTE_CONTRACTS = {
themes=("family",),
resolved_routes=("family",),
requested_domains=("family",),
required_evidence_categories=("natal_foundation", "domain"),
required_evidence_categories=_FULL_SPECTRUM_EVIDENCE,
required_layers=("D1", "D7", "D12", "4th/5th/9th houses", "Dasha"),
claim_boundary="family_pattern_not_deterministic_event_claim",
),
@@ -94,7 +95,7 @@ _ROUTE_CONTRACTS = {
themes=("annual",),
resolved_routes=("annual",),
requested_domains=("annual",),
required_evidence_categories=("natal_foundation", "timing", "validation"),
required_evidence_categories=_FULL_SPECTRUM_EVIDENCE,
required_layers=("D1", "Annual chart boundary", "Dasha", "Transit", "Tajika candidate"),
claim_boundary="annual_report_broad_periods_only",
),
@@ -102,7 +103,7 @@ _ROUTE_CONTRACTS = {
themes=("timing",),
resolved_routes=("timing",),
requested_domains=("timing",),
required_evidence_categories=("natal_foundation", "timing", "validation"),
required_evidence_categories=_FULL_SPECTRUM_EVIDENCE,
required_layers=("Vimshottari", "Narayana", "Transit", "Varga", "negative holdout gate"),
claim_boundary="candidate_day_month_window_only_until_holdout_passes",
),
@@ -110,7 +111,7 @@ _ROUTE_CONTRACTS = {
themes=("general",),
resolved_routes=("general",),
requested_domains=("general",),
required_evidence_categories=("natal_foundation", "domain"),
required_evidence_categories=_FULL_SPECTRUM_EVIDENCE,
required_layers=("D1", "D9", "D10", "D2", "Dasha", "Narayana", "Transit", "Functional Benefic/Malefic"),
claim_boundary="multi_domain_summary_with_missing_layers_disclosed",
),
+469 -54
View File
@@ -62,9 +62,9 @@ except ModuleNotFoundError: # pragma: no cover - script execution path
)
from vedastro_runtime_context import temporary_timeout_seconds
try:
from scripts.unified_consultation_orchestrator import UnifiedConsultationOrchestrator
from scripts.unified_consultation_orchestrator import FORMAL_DIVISIONS, UnifiedConsultationOrchestrator
except ModuleNotFoundError: # pragma: no cover - script execution path
from unified_consultation_orchestrator import UnifiedConsultationOrchestrator
from unified_consultation_orchestrator import FORMAL_DIVISIONS, UnifiedConsultationOrchestrator
try:
from scripts.consultation_domain_registry import CANONICAL_DOMAINS
except ModuleNotFoundError: # pragma: no cover - script execution path
@@ -517,38 +517,26 @@ def _western_evidence_packet_from_body(
timezone=body.get('western_timezone') or birth_payload['tz'],
house_system=str(body.get('western_house_system', 'P')),
)
timing_request = body.get('western_timing')
if isinstance(timing_request, dict):
birth = {
'year': int(birth_payload['year']), 'month': int(birth_payload['month']), 'day': int(birth_payload['day']),
'hour': int(birth_payload['hour']), 'minute': int(birth_payload['minute']), 'second': int(birth_payload.get('second', 0)),
'latitude': float(birth_payload['lat']), 'longitude': float(birth_payload['lon']),
'timezone': body.get('western_timezone') or birth_payload['tz'],
'house_system': str(body.get('western_house_system', 'P')),
}
timing = build_timing_techniques(
**birth,
transit_date=timing_request.get('transit_date'),
solar_return_year=timing_request.get('solar_return_year'),
secondary_progression_date=timing_request.get('secondary_progression_date'),
solar_arc_date=timing_request.get('solar_arc_date'),
converse_secondary_progression_date=timing_request.get('converse_secondary_progression_date'),
converse_solar_arc_date=timing_request.get('converse_solar_arc_date'),
midpoint_date=timing_request.get('midpoint_date'),
lunar_return_start_date=timing_request.get('lunar_return_start_date'),
duration_scan_start_date=timing_request.get('duration_scan_start_date'),
duration_scan_end_date=timing_request.get('duration_scan_end_date'),
parans_date=timing_request.get('parans_date'),
birth = {
'year': int(birth_payload['year']), 'month': int(birth_payload['month']), 'day': int(birth_payload['day']),
'hour': int(birth_payload['hour']), 'minute': int(birth_payload['minute']), 'second': int(birth_payload.get('second', 0)),
'latitude': float(birth_payload['lat']), 'longitude': float(birth_payload['lon']),
'timezone': body.get('western_timezone') or birth_payload['tz'],
'house_system': str(body.get('western_house_system', 'P')),
}
timing = build_timing_techniques(**birth, **_western_default_timing(body))
if timing:
duration = timing.get('transit_duration_scan')
if isinstance(duration, dict):
duration.pop('daily_hits', None)
packet['timing_techniques'] = timing
packet['sections']['timing_techniques'] = {'status': 'used', 'source_path': 'western.native_timing'}
packet['missing_sections'] = [item for item in packet['missing_sections'] if item != 'timing_techniques']
packet['boundary'] = (
'Native Western natal plus implemented timing layers are attached by default on natal '
'consultation. Outputs do not infer outcomes or interpretation. Unimplemented or failed '
'layers are listed as blocked, never silently omitted.'
)
if timing:
packet['timing_techniques'] = timing
packet['sections']['timing_techniques'] = {'status': 'used', 'source_path': 'western.native_timing'}
packet['missing_sections'] = [item for item in packet['missing_sections'] if item != 'timing_techniques']
packet['boundary'] = (
'Native calculations include only explicitly requested transit, solar-return, secondary-progression, '
'solar-arc, midpoint, lunar-return and daily duration-scan layers; parans remain blocked until a '
'dedicated latitude-aware event solver is implemented. Outputs do not infer outcomes or interpretation.'
)
return packet
except Exception as exc: # pragma: no cover - defensive boundary
return {
@@ -575,6 +563,377 @@ def _consultation_reference_date(body: dict) -> datetime:
return datetime.now()
_FORMAL_DIVISION_SET = set(FORMAL_DIVISIONS)
_RESEARCH_DN_DIVISIONS = tuple(n for n in range(2, 61) if n not in _FORMAL_DIVISION_SET)
_WESTERN_TIMING_LAYER_NAMES = (
'transits',
'solar_return',
'secondary_progressions',
'solar_arc_directions',
'converse_secondary_progressions',
'converse_solar_arc_directions',
'midpoints',
'lunar_return',
'transit_duration_scan',
'parans',
)
def _western_default_timing(body: dict) -> dict:
"""Attach every implemented Western timing layer to the consultation reference date.
Explicit `western_timing` keys still override the defaults; omitted keys are filled so a
natal consultation no longer waits for the caller to name each technique.
"""
reference = _consultation_reference_date(body)
iso = reference.date().isoformat()
defaults = {
'transit_date': iso,
'solar_return_year': reference.year,
'secondary_progression_date': iso,
'solar_arc_date': iso,
'converse_secondary_progression_date': iso,
'converse_solar_arc_date': iso,
'midpoint_date': iso,
'lunar_return_start_date': iso,
'duration_scan_start_date': iso,
'duration_scan_end_date': (reference.date() + timedelta(days=13)).isoformat(),
'parans_date': iso,
}
explicit = body.get('western_timing')
if isinstance(explicit, dict):
return {**defaults, **explicit}
return defaults
def _consultation_layer_present(value) -> bool:
if not isinstance(value, dict) or not value:
return False
status = str(value.get('status') or '').lower()
return status not in {'blocked', 'unavailable', 'missing'}
def _planet_longitudes(planets: dict) -> dict[str, float]:
longitudes = {}
for name, data in planets.items():
if isinstance(data, dict) and data.get('lon') is not None:
try:
longitudes[name] = float(data['lon'])
except (TypeError, ValueError):
continue
return longitudes
def _named_varga_chart(varga: dict, division: int):
exact = f'D{division}'
if exact in varga:
return varga[exact]
prefix = f'D{division}_'
for key, value in varga.items():
if not isinstance(key, str) or not key.startswith(prefix):
continue
rest = key[len(prefix):]
if rest and not rest[0].isdigit():
return value
return None
def _compact_varga_positions(chart: dict, *, name: str | None = None, boundary: str | None = None) -> dict:
if not isinstance(chart, dict):
return {}
ascendant = chart.get('ascendant') if isinstance(chart.get('ascendant'), dict) else {}
planets_in = chart.get('planets') if isinstance(chart.get('planets'), dict) else {}
planets = {}
for planet, data in planets_in.items():
if isinstance(data, dict) and data.get('sign'):
planets[planet] = data['sign']
elif isinstance(data, str):
planets[planet] = data
payload = {
'lagna': ascendant.get('sign') or chart.get('sign'),
'planets': planets,
}
if name or chart.get('name'):
payload['name'] = name or chart.get('name')
if chart.get('meaning'):
payload['meaning'] = chart['meaning']
if boundary:
payload['boundary'] = boundary
return payload
def _custom_research_dn_chart(calc, planet_lons: dict[str, float], asc_lon: float, division: int) -> dict:
ascendant = calc.calc_custom_varga(asc_lon, division)
planets = {name: calc.calc_custom_varga(lon, division) for name, lon in planet_lons.items()}
return {
'division': division,
'ascendant': {'sign': ascendant.get('sign')},
'planets': {name: {'sign': pos.get('sign')} for name, pos in planets.items()},
}
def _build_consultation_varga_spectrum(handler, planets: dict, ascendant: dict) -> tuple[dict | None, dict, list[dict]]:
"""Compute formal D1D60 named vargas plus remaining D-N research charts."""
diagnostics: list[dict] = []
planet_lons = _planet_longitudes(planets)
try:
asc_lon = float(ascendant.get('lon'))
except (TypeError, ValueError):
return None, {}, [{'layer': 'varga_spectrum', 'status': 'unavailable', 'reason': 'ascendant_longitude_missing'}]
if not planet_lons:
return None, {}, [{'layer': 'varga_spectrum', 'status': 'unavailable', 'reason': 'planet_longitudes_missing'}]
varga_full = None
try:
response = handler._compute_varga_full({'planets': planets, 'ascendant': ascendant})
result = response.get('result') if isinstance(response, dict) else None
if isinstance(result, dict):
varga_full = result
except Exception as exc:
diagnostics.append({'layer': 'varga_full', 'status': 'unavailable', 'reason': exc.__class__.__name__})
formal: dict[str, dict] = {}
extended: dict[str, dict] = {}
blocked: list[str] = []
if isinstance(varga_full, dict):
for division in FORMAL_DIVISIONS:
chart = _named_varga_chart(varga_full, division)
code = f'D{division}'
if isinstance(chart, dict):
formal[code] = _compact_varga_positions(chart, name=chart.get('name'))
else:
blocked.append(code)
for key, chart in varga_full.items():
if not isinstance(key, str) or not isinstance(chart, dict):
continue
match = re.fullmatch(r'D(\d+)_.*', key)
if not match:
continue
division = int(match.group(1))
if division > 60:
extended[f'D{division}'] = _compact_varga_positions(chart, name=chart.get('name'))
else:
blocked.extend(f'D{division}' for division in FORMAL_DIVISIONS)
research: dict[str, dict] = {}
try:
module = _load_local_module('divisional_charts_extended')
calc = module.DivisionalChartsCalculator()
for division in _RESEARCH_DN_DIVISIONS:
code = f'D{division}'
try:
chart = _custom_research_dn_chart(calc, planet_lons, asc_lon, division)
research[code] = _compact_varga_positions(
chart,
boundary='generic D-N; not a formal named varga',
)
except Exception as exc:
blocked.append(code)
diagnostics.append({'layer': code, 'status': 'unavailable', 'reason': exc.__class__.__name__})
except Exception as exc:
blocked.extend(f'D{n}' for n in _RESEARCH_DN_DIVISIONS)
diagnostics.append({'layer': 'research_dn', 'status': 'unavailable', 'reason': exc.__class__.__name__})
spectrum = {
'status': 'used' if formal or research else 'blocked',
'mode': 'full_spectrum',
'formal': formal,
'research_dn': research,
'extended': extended,
'counts': {
'formal': len(formal),
'research_dn': len(research),
'extended': len(extended),
'blocked': len(blocked),
},
'blocked': blocked,
'boundary': (
'Twenty formal named Vargas are traditional charts. Remaining D-N rows through D60 are '
'generic research divisions and must not be advertised as verified traditional vargas.'
),
}
return varga_full, spectrum, diagnostics
def _attach_gulika_observation(chart: dict, birth_payload: dict) -> None:
modules = chart['modules']
if isinstance(modules.get('gulika'), dict):
return
try:
lat = float(birth_payload.get('lat') if birth_payload.get('lat') is not None else birth_payload['latitude'])
lon = float(birth_payload.get('lon') if birth_payload.get('lon') is not None else birth_payload['longitude'])
tz_raw = birth_payload.get('tz', birth_payload.get('timezone', 0))
tz = float(tz_raw)
moment = datetime(
int(birth_payload['year']),
int(birth_payload['month']),
int(birth_payload['day']),
int(float(birth_payload.get('hour', 0) or 0)),
int(float(birth_payload.get('minute', 0) or 0)),
int(float(birth_payload.get('second', 0) or 0)),
)
result = _load_local_module('gulika').calculate_gulika(moment, lat=lat, lon=lon, tz=tz)
result['claim_boundary'] = 'observation_only; not a verified event claim'
modules['gulika'] = result
except Exception as exc:
modules['gulika'] = {
'status': 'blocked',
'reason': exc.__class__.__name__,
'claim_boundary': 'observation_only',
}
def _sign_of(point) -> str | None:
if isinstance(point, dict) and point.get('sign'):
return str(point['sign'])
return None
def _compact_western_spectrum(packet: dict | None) -> dict:
if not isinstance(packet, dict) or not packet:
return {
'status': 'not_applicable',
'techniques': {},
'boundary': 'Western natal was not attached for this run.',
}
native = packet.get('native_chart') if isinstance(packet.get('native_chart'), dict) else {}
natal_block = native.get('natal') if isinstance(native.get('natal'), dict) else packet.get('natal')
natal_block = natal_block if isinstance(natal_block, dict) else {}
planets = natal_block.get('planets') if isinstance(natal_block.get('planets'), dict) else {}
timing = packet.get('timing_techniques') if isinstance(packet.get('timing_techniques'), dict) else {}
techniques = {}
for name in _WESTERN_TIMING_LAYER_NAMES:
layer = timing.get(name)
if isinstance(layer, dict):
techniques[name] = {
'status': _audit_row_status(layer.get('status') or 'executed'),
'boundary': layer.get('boundary') or layer.get('reason'),
}
else:
techniques[name] = {'status': 'blocked', 'boundary': 'layer_not_materialized'}
natal_status = 'executed' if packet.get('status') != 'blocked' and natal_block else str(packet.get('status') or 'blocked')
calculation = packet.get('calculation') if isinstance(packet.get('calculation'), dict) else {}
return {
'status': natal_status if natal_status != 'partial' else 'executed',
'zodiac': native.get('zodiac') or 'tropical',
'house_system': native.get('house_system') or calculation.get('house_system'),
'natal': {
'sun': _sign_of(planets.get('sun')),
'moon': _sign_of(planets.get('moon')),
'ascendant': _sign_of(natal_block.get('ascendant')),
},
'techniques': techniques,
'boundary': packet.get('boundary'),
}
def _audit_row_status(raw) -> str:
value = str(raw or '').lower()
if value in {'used', 'executed', 'complete', 'official_verified', 'pass', 'ready', 'ok', 'partial'}:
return 'executed'
if value in {'not_applicable', 'n/a', 'na', 'not applicable'}:
return 'not_applicable'
return 'blocked'
def _audit_row(technique: str, status, *, system: str = 'jyotish', boundary: str | None = None) -> dict:
mapped = _audit_row_status(status) if not str(status).lower() in {'executed', 'blocked', 'not_applicable'} else str(status).lower()
row = {'technique': technique, 'status': mapped, 'system': system}
if boundary:
row['boundary'] = boundary
return row
def _consultation_technique_audit_table(
*,
chart: dict,
western_evidence_packet: dict | None,
quality_gate: dict,
entry_mode: str,
) -> list[dict]:
rows: list[dict] = []
for raw in quality_gate.get('technique_audit_table') or []:
if not isinstance(raw, dict):
continue
rows.append(_audit_row(
str(raw.get('technique') or raw.get('name') or 'Technique'),
raw.get('status'),
system=str(raw.get('system') or 'cross_system'),
boundary=str(raw.get('effect_on_confidence') or raw.get('boundary') or ''),
))
modules = chart.get('modules') if isinstance(chart.get('modules'), dict) else {}
spectrum = modules.get('varga_spectrum') if isinstance(modules.get('varga_spectrum'), dict) else {}
counts = spectrum.get('counts') if isinstance(spectrum.get('counts'), dict) else {}
rows.append(_audit_row(
'Formal Vargas D1D60',
'executed' if counts.get('formal') else 'blocked',
boundary=f"{counts.get('formal', 0)}/20 named traditional charts; remaining D-N are separate",
))
rows.append(_audit_row(
'Research D-N through D60',
'executed' if counts.get('research_dn') else 'blocked',
boundary=f"{counts.get('research_dn', 0)} generic D-N charts; not formal named vargas",
))
if counts.get('extended'):
rows.append(_audit_row(
'Extended Vargas D81/D108/D144',
'executed',
boundary=f"{counts.get('extended')} super-divisional charts",
))
for name, label, boundary in (
('arudha_padas', 'Arudha / UL / A10', 'observation for A10/UL; commercial claims stay non-deterministic'),
('narayana_dasha', 'Narayana Dasha', None),
('dasha_sub_periods', 'Vimshottari sub-periods', None),
('ashtakavarga', 'Ashtakavarga', 'component parity remains partial'),
('kp_cusps', 'KP cusps', 'KP remains non-deterministic in this product'),
('gulika', 'Gulika / Mandi', 'observation_only'),
):
value = modules.get(name)
status = 'executed' if _consultation_layer_present(value) else 'blocked'
rows.append(_audit_row(label, status, boundary=boundary))
shadbala = modules.get('shadbala') or chart.get('shadbala')
rows.append(_audit_row(
'Shadbala components',
'executed' if _consultation_layer_present(shadbala) else 'blocked',
boundary='Chesta/Sthana/Dig/Drik/Kala/Naisargika as components; absolute Virupa truth is not closed',
))
kakshya = modules.get('kakshya')
rows.append(_audit_row(
'Ashtakavarga Kakshya',
'executed' if _consultation_layer_present(kakshya) else 'blocked',
boundary='observation_only; not verified day/month timing',
))
if not any(row.get('technique') == 'Functional Benefic/Malefic' for row in rows):
functional = modules.get('functional_benefic_malefic')
rows.append(_audit_row(
'Functional Benefic/Malefic',
'executed' if _consultation_layer_present(functional) and str(functional.get('status') or '') != 'blocked' else 'blocked',
boundary=str((functional or {}).get('effect_on_confidence') or '') if isinstance(functional, dict) else '',
))
western = _compact_western_spectrum(western_evidence_packet)
rows.append(_audit_row(
'Western natal (tropical)',
western.get('status') or 'blocked',
system='western',
boundary=str(western.get('boundary') or ''),
))
for name, layer in (western.get('techniques') or {}).items():
rows.append(_audit_row(
f'Western {name}',
layer.get('status') if isinstance(layer, dict) else 'blocked',
system='western',
boundary=str((layer or {}).get('boundary') or '') if isinstance(layer, dict) else '',
))
if entry_mode == 'prashna':
rows.append(_audit_row('Prashna chart', 'executed', boundary='horary path'))
else:
rows.append(_audit_row('Prashna chart', 'not_applicable', boundary='this run is a natal consultation'))
rows.append(_audit_row('Ashtakoota matching', 'not_applicable', boundary='requires a partner chart'))
rows.append(_audit_row('Adhana / Niseka', 'not_applicable', boundary='research_only; not a natal consultation layer'))
rows.append(_audit_row('Muhurta election', 'not_applicable', boundary='not an electional request'))
return rows
def _consultation_current_age(birth_payload: dict, body: dict) -> float:
try:
born = datetime(
@@ -680,18 +1039,15 @@ def _attach_local_consultation_layers(handler, chart: dict, birth_payload: dict,
ascendant = chart.get('ascendant') if isinstance(chart.get('ascendant'), dict) else {}
diagnostics = []
if planets and ascendant and not isinstance(modules.get('varga_full'), dict):
try:
varga_response = handler._compute_varga_full({
'planets': planets,
'ascendant': ascendant,
'divisions': ['D2', 'D4', 'D9', 'D10', 'D11'],
})
varga_result = varga_response.get('result') if isinstance(varga_response, dict) else None
if isinstance(varga_result, dict):
modules['varga_full'] = varga_result
except Exception as exc: # optional local layer; preserve core D1 result
diagnostics.append({'layer': 'varga_full', 'status': 'unavailable', 'reason': exc.__class__.__name__})
if planets and ascendant:
varga_full, varga_spectrum, varga_diagnostics = _build_consultation_varga_spectrum(
handler, planets, ascendant,
)
diagnostics.extend(varga_diagnostics)
if isinstance(varga_full, dict) and not isinstance(modules.get('varga_full'), dict):
modules['varga_full'] = varga_full
if varga_spectrum:
modules['varga_spectrum'] = varga_spectrum
if planets and ascendant and not isinstance(modules.get('arudha_padas'), dict):
try:
@@ -765,13 +1121,59 @@ def _attach_local_consultation_layers(handler, chart: dict, birth_payload: dict,
except Exception as exc:
diagnostics.append({'layer': 'kp_cusps', 'status': 'unavailable', 'reason': exc.__class__.__name__})
_attach_gulika_observation(chart, birth_payload)
if planets and ascendant and not _consultation_layer_present(modules.get('functional_benefic_malefic')):
try:
functional = handler._functional_benefic_malefic_snapshot(planets, ascendant)
if isinstance(functional, dict):
modules['functional_benefic_malefic'] = functional
except Exception as exc:
diagnostics.append({'layer': 'functional_benefic_malefic', 'status': 'unavailable', 'reason': exc.__class__.__name__})
if planets and not _consultation_layer_present(modules.get('shadbala')):
try:
response = handler._compute_shadbala({
**birth_payload,
'planets': planets,
'ascendant': ascendant,
})
result = response.get('result') if isinstance(response, dict) else None
if isinstance(result, dict):
modules['shadbala'] = result
if not isinstance(chart.get('shadbala'), dict):
chart['shadbala'] = result
advanced = response.get('advanced_layer') if isinstance(response, dict) else None
if isinstance(advanced, dict):
modules['shadbala_components'] = advanced
except Exception as exc:
diagnostics.append({'layer': 'shadbala', 'status': 'unavailable', 'reason': exc.__class__.__name__})
if planets and not _consultation_layer_present(modules.get('kakshya')):
try:
response = handler._compute_kakshya({'planets': planets, 'ascendant': ascendant})
result = response.get('result') if isinstance(response, dict) else None
if isinstance(result, dict):
modules['kakshya'] = {
'status': 'observation_only',
'result': result,
'summary': response.get('summary') if isinstance(response, dict) else None,
'boundary': 'Kakshya is an Ashtakavarga degree-layer observation; not verified day/month timing.',
}
except Exception as exc:
diagnostics.append({'layer': 'kakshya', 'status': 'unavailable', 'reason': exc.__class__.__name__})
chart['local_consultation_layers'] = {
'status': 'ready' if not diagnostics else 'partial',
'source': 'repository_local_engines',
'available': [
name
for name in ('varga_full', 'arudha_padas', 'narayana_dasha', 'dasha_sub_periods', 'ashtakavarga', 'kp_cusps')
if isinstance(modules.get(name), dict) and modules.get(name)
for name in (
'varga_full', 'varga_spectrum', 'arudha_padas', 'narayana_dasha',
'dasha_sub_periods', 'ashtakavarga', 'kp_cusps', 'gulika',
'functional_benefic_malefic', 'shadbala', 'kakshya',
)
if _consultation_layer_present(modules.get(name))
],
'diagnostics': diagnostics,
}
@@ -787,16 +1189,17 @@ def _attach_local_consultation_layers(handler, chart: dict, birth_payload: dict,
# The gate must also not be weaker than what the product tells the user it will use. The frontend
# domain registry declares each domain's layers for its evidence preview, and every entry there that
# names a real section is required here too; a test pins that intersection (BUG-270). The registry
# additionally names human-facing items ('7th house/lord', 'negative holdout gate') and D11, which
# the engine does not build at all — those are deliberately not requirements.
# also names human-facing items ('7th house/lord', 'negative holdout gate') which cannot be
# requirements. Formal Vargas the engine now materializes (D6/D7/D8/D11/D12/D24) are required
# wherever the product already promises them.
_ROUTE_REQUIRED_LAYERS = {
'career': ('D1', 'D10', 'A10', 'dasha_boundaries', 'narayana_dasha'),
'marriage': ('D1', 'D9', 'UL', 'A7', 'dasha_boundaries'),
'wealth': ('D1', 'D2', 'ashtakavarga', 'dasha_boundaries'),
'health': ('D1', 'D9', 'dasha_boundaries'),
'education': ('D1', 'D9', 'dasha_boundaries'),
'migration': ('D1', 'D4', 'dasha_boundaries'),
'family': ('D1', 'D9', 'dasha_boundaries'),
'wealth': ('D1', 'D2', 'D11', 'ashtakavarga', 'dasha_boundaries'),
'health': ('D1', 'D6', 'D8', 'D9', 'dasha_boundaries'),
'education': ('D1', 'D9', 'D24', 'dasha_boundaries'),
'migration': ('D1', 'D4', 'D12', 'dasha_boundaries'),
'family': ('D1', 'D7', 'D9', 'D12', 'dasha_boundaries'),
'annual': ('D1', 'dasha_boundaries', 'narayana_dasha'),
'timing': ('D1', 'dasha_boundaries', 'narayana_dasha'),
'general': ('D1', 'D9', 'D10', 'D2', 'dasha_boundaries'),
@@ -1254,6 +1657,18 @@ def execute_consultation_workflow(
western_evidence_packet=western_evidence_packet,
blind=bool(body.get('blind') or body.get('blind_technical_mode')),
)
consumer_context['varga_spectrum'] = (
chart.get('modules', {}).get('varga_spectrum')
if isinstance(chart.get('modules'), dict)
else {}
)
consumer_context['western_spectrum'] = _compact_western_spectrum(western_evidence_packet)
consumer_context['technique_audit_table'] = _consultation_technique_audit_table(
chart=chart,
western_evidence_packet=western_evidence_packet,
quality_gate=runtime_evidence_log.get('quality_gate') if isinstance(runtime_evidence_log.get('quality_gate'), dict) else {},
entry_mode=str(entry_mode or 'direct_chart'),
)
result = {
'success': True,
+37 -7
View File
@@ -342,17 +342,19 @@ class UnifiedConsultationOrchestrator:
if code not in selected:
selected.append(code)
formal = [f"D{division}" for division in FORMAL_DIVISIONS]
research = [f"D{division}" for division in range(2, 61) if division not in FORMAL_DIVISIONS]
return {
"question": question or "",
"themes": normalized_themes,
"presentation_mode": "research" if is_research else "default",
"appendix_expanded": is_research,
"varga_dispatch": {
"mode": "on_demand",
"mode": "full_spectrum",
"selected_theme_vargas": selected,
"all_formal_vargas": formal,
"deferred_vargas": [code for code in formal if code not in selected],
"rule": "先调用与主题直接相关的分盘;其余正式分盘只在追问或冲突时展开。",
"research_dn_vargas": research,
"deferred_vargas": [],
"rule": "主题相关分盘优先解读;D1–D60 正式分盘与其余 D-N 研究分盘均已计算,未计算的标 blocked,不得静默省略。",
},
}
@@ -566,6 +568,20 @@ class UnifiedConsultationOrchestrator:
return "official_verified"
return "official_blocked"
@staticmethod
def _named_varga_chart(varga: dict[str, Any], division: int) -> Any:
exact = f"D{division}"
if exact in varga:
return varga[exact]
prefix = f"D{division}_"
for key, value in varga.items():
if not isinstance(key, str) or not key.startswith(prefix):
continue
rest = key[len(prefix):]
if rest and not rest[0].isdigit():
return value
return None
@staticmethod
def _section(value: Any, source_path: str) -> dict[str, Any]:
present = bool(value)
@@ -687,10 +703,10 @@ class UnifiedConsultationOrchestrator:
base_chart.get("planets") and base_chart.get("ascendant"),
"chart.planets+chart.ascendant",
),
"D9": self._section(varga.get("D9_Navamsa") or varga.get("D9"), "modules.varga_full.D9"),
"D10": self._section(varga.get("D10_Dasamsa") or varga.get("D10"), "modules.varga_full.D10"),
"D2": self._section(varga.get("D2_Hora") or varga.get("D2"), "modules.varga_full.D2"),
"D4": self._section(varga.get("D4_Chaturthamsa") or varga.get("D4"), "modules.varga_full.D4"),
"D9": self._section(self._named_varga_chart(varga, 9), "modules.varga_full.D9"),
"D10": self._section(self._named_varga_chart(varga, 10), "modules.varga_full.D10"),
"D2": self._section(self._named_varga_chart(varga, 2), "modules.varga_full.D2"),
"D4": self._section(self._named_varga_chart(varga, 4), "modules.varga_full.D4"),
"planet_degrees": self._section(base_chart.get("planets"), "chart.planets"),
"house_degrees": self._section(base_chart.get("houses") or chart_data.get("houses"), "chart.houses"),
"dasha_boundaries": self._section(modules.get("dasha") or chart_data.get("dasha"), "modules.dasha"),
@@ -732,6 +748,20 @@ class UnifiedConsultationOrchestrator:
"vedastro_gateway.archives",
),
}
for division in FORMAL_DIVISIONS:
if division == 1:
continue
key = f"D{division}"
if key not in sections:
sections[key] = self._section(
self._named_varga_chart(varga, division),
f"modules.varga_full.{key}",
)
spectrum = modules.get("varga_spectrum") if isinstance(modules.get("varga_spectrum"), dict) else {}
sections["varga_spectrum"] = self._section(
spectrum if spectrum.get("status") == "used" else None,
"modules.varga_spectrum",
)
missing = [name for name, section in sections.items() if section.get("status") == "missing"]
signals = chart_data.get("cross_system_signals")
if not isinstance(signals, list):
@@ -15,7 +15,7 @@ description: "印度占星(Jyotish)商业解盘与推运系统。核心能
3. `references/oracle/commercial_skill_truth_overlay.v1.json`:商业声明和受限技法的最终覆盖层。
4. 服务端 `consumer_context.answer_policy`:当前请求可回答范围的最终合同。
若研究快照、商业覆盖层和服务端回执冲突,以商业覆盖层和服务端回执为准。真实计算只能来自服务端工具,模型不得重算或发明行星位置。候选出生时间不得写成 confirmed;`blocked`、参数敏感、外部验证未闭环和多体系冲突必须原样保留。正式个人报告固定`executive_summary -> thematic_narrative -> evidence_appendix` 排列,Technique Audit Table 位于附录,不得置于摘要之前。医疗、法律、投资、安全关键结论及确定性死亡/诊断/妊娠预测均禁止。
若研究快照、商业覆盖层和服务端回执冲突,以商业覆盖层和服务端回执为准。真实计算只能来自服务端工具,模型不得重算或发明行星位置。候选出生时间不得写成 confirmed;`blocked`、参数敏感、外部验证未闭环和多体系冲突必须原样保留。网页对话在口语回答末尾给出 Technique Audit Table(已执行 / 阻塞 / 不适用);正式个人报告`executive_summary -> thematic_narrative -> evidence_appendix` 排列,审计表位于附录。医疗、法律、投资、安全关键结论及确定性死亡/诊断/妊娠预测均禁止。
> **版本**v6.9.14 | **详细变更**`CHANGELOG.md`
> **对标状态**:中文用户端与技法覆盖领先;D1/D9/AV/Chara 等有守门,Dasha/Shadbala 外部 oracle 扩充仍在进行。
@@ -37,6 +37,29 @@ KP/Muhurta/Gochara/Sahams/Sphuta/Tajika等高阶分支必须按 skill truth over
| KP系统 | reference-only / partial |
| Muhurta/Gochara/Sahams/Sphuta/Tajika | 只作参考或探索性证据,除非 evidence packet 明确升级 |
## 关联技法完整调取
全谱系真实调用:咨询工作流必须真实运行全部可运行且问题相关的技法,不得因篇幅、成本或另写一份运行时覆盖层而少算一层。确定性措辞受商业声明合同约束;信息密度不受成本削减。口语回答须先用已交付的统一参数与原始结构(度数、分盘、Dasha 边界、强弱、西洋层),再并列各体系结果、冲突与证据状态,最后给出条件性综合推理。
- 共同基础:D1、功能性吉凶星、相关 Yoga、Vimshottari 与 Narayana Dasha、行运、Shadbala、Ashtakavarga。
- 分盘:主题相关分盘优先解读;D1–D60 中 20 张正式传统分盘与 40 张研究型通用 D-N 均须计算。正式分盘与 generic D-N 分别标明,不得把研究 D-N 宣传为已验证传统分盘。
- 领域关联:事业至少 `D10 + A10`,财富至少 `D2 / D11`,婚恋至少 `D9 + UL`
- 西洋层:有本命出生资料时,自动运行已实现的热带本命及 timing(行运、回归、次限、日弧、converse、中点、月回归、duration scan、parans)。未实现或失败标 `blocked`,不适用标 `not_applicable`
- 特殊体系:KP、Tajika、Saham、Gulika/Mandi、Panchanga、Muhurta、Prashna、Sphuta、合婚等,按问题关联调用或解释 `blocked / not_applicable`。商业上 KP/Muhurta/Tajika 不得作为确定性结论。
- Technique Audit Table 对每项使用 `executed / blocked / not_applicable`(可见回答译为 已执行 / 阻塞 / 不适用)。未闭环技法仍展示状态与边界,不得写成确定事件。
### P0/P1 观察层(不得升格为已验证真值)
网页与 API 结果必须显式显示这些行的调用状态与 claim boundary
- `A7 / UL / A10 / KP`:事业看 A10,婚恋看 UL/A7;KP 仅观察层。
- `Gulika / Mandi`:可展示;缺独立 oracle 时不得作为最终断事证据。
- `Muhurta factors`:只作 factor/observation,不得输出最终择日 verdict。
- `Shadbala components`:显示分量状态;不得声称绝对值已闭环。
- `Ashtakavarga Kakshya`:观察层,不得用于 verified day/month timing。
- `Ashtakoota`:无伴侣盘时 `not_applicable`
- `Adhana / Niseka`research-only;普通本命咨询为 `not_applicable`
## v6.9.14 核心能力
| 维度 | 数据 |
@@ -122,15 +145,19 @@ adapter available 解释为已完成 VedAstro、PyJHora/JHora 或 jyotishganit r
`JYOTISH_ASYNC_JOB_BACKEND=sqlite`,使用 `scratch/local/async_jobs.sqlite3` 保存 token-hash
与 TTL 任务记录。两种后端都不是 Redis、多节点队列或跨主机 worker;不得把它们描述为分布式恢复能力。
**强制工作流**(完整规范 → `references/ai-reading-workflow-prompt.md` v5.1.0):
## 强制工作流
完整规范 → `references/ai-reading-workflow-prompt.md` v5.1.0。网页咨询与本地 Agent 调用同一套步骤,不得另减技法。
0. **阶段负一**:问题类型路由(事业/婚恋/财务/应期/历史验证/综合解盘)→ 必须先读 `references/strict-workflow-router.md`,按对应 strict checklist 执行;用户不需要主动点名高级技法。
0.0.1 **全谱系真实调用**:事业、财富、年度推运、事件应期、校时或综合解盘,必须实际运行全部可用且问题相关的印度与西方技法。不得把“仓库存在”“UI可见”或“默认未传参数”伪装成已调用;每一项必须输出 `executed``blocked``not_applicable`。限制只作用于确定性措辞,不得削减信息密度。咨询工作流与本文件是同一套合同,不另维持一份锁定稿调用面。
0.1 **事件判定骨架**:凡涉及 marriage / career / wealth / event verify,必须执行 `事件判定骨架 v1.0`,按 `Route -> Evidence Ledger -> Adjudication -> Output Contract` 顺序输出;不得再凭直觉跳模块或随口给置信度。详见 `references/ai-reading-workflow-prompt.md``references/event_judgment_skeleton.md``references/event_judgment_marriage.md``references/event_judgment_examples.md`
1. **阶段零**:入口路由(A/B/C自动判断)
2. **阶段一**(仅B):PDF/图片提取 + Quality Gate
3. **阶段二**:意图识别 → 路由目标宫位(无明确意图→Level 2综合解盘)
4. **阶段二点五**:若 `full-reading` 或网页/API 返回 `ai_prompt_pack`,必须优先读取 `prompt_zh``evidence_snapshot``retrieval_plan` 作为 AI/RAG 主上下文;若没有该字段,再退回传统 JSON 摘要。
4.1 **VedAstro 官方优先级**:用户给出生信息后,网页、Skill、MCP 都必须默认走同一条数据优先级:`VedAstro official snapshot -> local supplemental modules -> local fallback only when official blocked`。用户不需要主动要求“调用 VedAstro”。若 `evidence_snapshot.vedastro_official_full_snapshot.status``ok/partial` 且官方 chart 可用,D1/分盘/官方返回的原始字段以 VedAstro 为主;本地引擎只做补充、交叉检查或官方 blocked 时 fallback。
4.1.1 **Formal Varga 官方证据审计**:需要补充 VedAstro 官方形式分盘证据时,返回的 `vedastro_official_formal_varga_audit` 只能进入 Evidence Ledger / Technique Audit Table,作为 `evidence_only`,不得宣称本地 Varga 公式真值闭环、不得用官方响应静默覆盖本地算法、不得写入或展示任何 API key。
4. **阶段三**:静态分析10步(宫位→承诺→Yoga→Argala→逆行→NK→Shadbala→AV→Ketu→分盘)
5. **阶段四**:动态推运7步(Dasha→五系统Convergence→Transit→Double Transit→Jaimini→KP→Varshaphala
6. **阶段五**:应期输出(五层验证→时间窗口→Actionable Output+案例检索)
@@ -186,7 +213,7 @@ adapter available 解释为已完成 VedAstro、PyJHora/JHora 或 jyotishganit r
涉及 timing / event / outcome 问题时,不得只看 Vimshottari。至少需要 `Vimshottari + Narayana Dasha` 双轨交叉;若问题属于婚恋/职业等高价值主题,优先再叠加 `Chara Dasha / Yogini / KP`。若关键结论在双轨之间明显冲突,必须降级置信度或标记 `blocked`,不得输出伪确定结论。
4. **强制多维分盘显微镜**
不得只看 D1。至少按问题域强制展开:`D10 for career, D2/D11 for wealth, D9 for marriage`,并尽可能联动 `A10 / UL / AK / DK / Karakamsha / Special Lagnas`如果相关分盘或特殊点未调用,Technique Audit Table 必须写明它如何削弱结论。
不得只看 D1。至少按问题域强制展开:`D10 for career, D2/D11 for wealth, D9 for marriage`,并联动 `A10 / UL / AK / DK / Karakamsha / Special Lagnas`D1–D60 正式分盘与其余研究型 D-N 均须计算并进入 Technique Audit Table;未调用的必须写明它如何削弱结论。
5. **强制物理原始数据交付**
不允许只给“运势不错/有机会”式结论。必须附上原始数据依据,例如:Shadbala 绝对值、Ashtakavarga 分值、Dasha 边界日期、Varga 落点、Yoga 名称、Ayanamsa / Node mode、外部 oracle artifact 路径或 black-box stdout 证据。原始数据交付是高严谨结论的唯一有效依据。
@@ -58,7 +58,19 @@ Every Level 2+ chart reading must include `MEVG / Global Web Evidence` and
---
## 0.7 User-Led Reading Calibration Gate
## 0.7 Full-Spectrum Invocation Contract
For career, wealth, annual forecast, event timing, rectification, or a comprehensive reading, the system must **actually execute every available, question-relevant Indian and Western technique** before interpretation. A module merely existing in the repository, registry, or UI does not count as used.
1. Preserve every produced raw payload, varga, dasha, strength table, transit window, and cross-system difference. Information density must never be reduced because an oracle, formula variant, or holdout is incomplete.
2. Invoke the Indian core relevant to the route: D1 plus all relevant vargas; Vimshottari, Narayana, Chara Dasha, Yogini, Ashtottari, Kalachakra and Bhrigu Pada where implemented; Jaimini/Arudha; KP; Double Transit PAC; transit LL/7L; Bhava Chalit; Sudarshana; Tajika/Varshaphala/Muntha; Nakshatra overlay; strengths, AV, yoga, argala, and relevant sahams.
3. Invoke the Western core for the same birth data: tropical natal chart, transit-to-natal duration scan, Solar Return, secondary progressions, solar arc, converse progressions/arcs, midpoints, Lunar Return, and parans where implemented.
4. A technique requiring unavailable inputs (for example partner data, Prashna question time, or an electional target) must still appear in the Technique Audit Table as `blocked` with its required input. It must not be silently skipped.
5. Keep raw information and conditional interpretations visible. The only restricted content is an unsupported certainty claim: no inevitable event, no unsupported exact day, and no claim of cross-system verification when only one system ran.
6. The report must state per technique: `executed`, `blocked`, or `not_applicable`, its settings, raw-output path, and its effect on assertion strength. `not_executed` is a workflow failure for a relevant technique.
---
## 0.8 User-Led Reading Calibration Gate
This gate turns the user's follow-up interrogation into workflow control. It is
not a personality preference; it prevents vague readings, memory-based
@@ -116,6 +128,7 @@ declared.
| "When will career opportunities appear?" / job / project / public status / profession | `career-timing-strict` | Level 3 |
| Marriage / partner / relationship timing | `relationship-timing-strict` | Level 3 |
| Money / income / gains / assets | `wealth-timing-strict` | Level 2 or 3 |
| Health / illness / hospitalization / injury / recovery pressure | `health-timing-strict` | Level 2 or 3 |
| "Will this event happen?" / concrete yes-no / project landing | `event-timing-strict` | Level 3 |
| "Why did this past event happen?" / technique reliability | `event-verification-strict` | Level 3 |
| General natal reading | `full-reading-strict` | Level 2 |
@@ -131,7 +144,7 @@ For every Level 2+ reading, complete or explicitly mark unavailable:
- D1 Rashi: houses, lords, dignity, exact degrees, Nakshatra, dispositor chain.
- D9 Navamsa: dignity confirmation, Vargottama, major dignity reversals.
- Relevant divisional chart: D10 for career, D7 for children, D12 for family/ancestral themes, D2 for wealth, D24 for education.
- Relevant divisional chart: D10 for career, D7 for children, D12 for family/ancestral themes, D2 for wealth, D24 for education, D6/D8/D30 for health pressure and vulnerability review.
- Vimshottari Dasha: MD/AD/PD when timing is requested.
- Transit: Saturn/Jupiter/Rahu-Ketu, plus Moon trigger for month/day-level timing.
- Functional benefic/malefic status by Lagna.
@@ -227,7 +240,37 @@ Use when the user asks for concrete timing, yes/no, project approval, launch, mo
---
## 7. `event-verification-strict`
## 7. `health-timing-strict`
Use when the user asks about health pressure, vitality risks, hospitalization, injury, surgery periods, recovery windows, or whether a difficult health phase is being activated.
### Mandatory modules
- D1: Lagna, Lagna lord, Moon, Sun, Saturn, Mars, 6H, 8H, 12H, and their lords.
- D6: core health-difficulty divisional review.
- D8: obstacle / vulnerability review when the question involves crisis, chronicity, or sharp disruption.
- D30: pressure, misfortune, or acute-friction layer; do not silently omit it when the route is health.
- Vimshottari: MD/AD/PD activation of 1L/6L/8L/12L, Lagna, Moon, Sun, Saturn, Mars.
- Narayana Dasha cross-check before making timing emphasis.
- Functional benefic/malefic: explicit Lagna-based classification and conflict note if natural and functional roles diverge.
- Strength: Shadbala for Lagna lord, Moon, Sun, Saturn, Mars, and the main activated lords.
- Transit: Saturn/Jupiter/Rahu-Ketu to Lagna, Moon, 6H, 8H, 12H, and their lords.
- Boundary layer: keep a visible non-medical boundary; no diagnosis, no treatment claim, and no certainty language about bodily outcomes.
- Historical verification if the user provides past health-pressure dates or crisis windows.
### Health timing output
Always separate:
1. **Pressure window**: strain, exhaustion, anxiety, or destabilizing background.
2. **Event-risk window**: injury / hospitalization / disruption activation candidates.
3. **Recovery-support window**: easing, stabilization, or reduced pressure after the peak.
Do not present this route as medical advice, diagnosis, or treatment guidance.
---
## 8. `event-verification-strict`
Use when the user provides past events and asks whether the technique is reliable.
@@ -350,17 +350,19 @@ class UnifiedConsultationOrchestrator:
if code not in selected:
selected.append(code)
formal = [f"D{division}" for division in FORMAL_DIVISIONS]
research = [f"D{division}" for division in range(2, 61) if division not in FORMAL_DIVISIONS]
return {
"question": question or "",
"themes": normalized_themes,
"presentation_mode": "research" if is_research else "default",
"appendix_expanded": is_research,
"varga_dispatch": {
"mode": "on_demand",
"mode": "full_spectrum",
"selected_theme_vargas": selected,
"all_formal_vargas": formal,
"deferred_vargas": [code for code in formal if code not in selected],
"rule": "先调用与主题直接相关的分盘;其余正式分盘只在追问或冲突时展开。",
"research_dn_vargas": research,
"deferred_vargas": [],
"rule": "主题相关分盘优先解读;D1–D60 正式分盘与其余 D-N 研究分盘均已计算,未计算的标 blocked,不得静默省略。",
},
}
+1 -1
View File
@@ -4,7 +4,7 @@
{
"name": "jyotish-vedic-astrology",
"version": "6.9.14",
"sha256": "d3d6d05c1da25bb684af3be591687f0321b9a3a69a9896454ea3e25b57b6071b",
"sha256": "c44e6f500c671d18dd47d9714610b766fbeabe00ca5e495e5225bc2c97f5f105",
"sourceCommit": null,
"packagePath": "skills/jyotish-vedic-astrology/versions/6.9.14",
"status": "active"
+38 -2
View File
@@ -110,6 +110,36 @@ def test_local_consultation_layers_supply_d10_a10_and_narayana_without_vedastro(
assert packet['sections']['narayana_dasha']['status'] == 'used'
assert packet['sections']['ashtakavarga']['status'] == 'used'
assert packet['sections']['KP_cusp']['status'] == 'used'
assert packet['sections']['D11']['status'] == 'used'
assert packet['sections']['D60']['status'] == 'used'
assert packet['sections']['varga_spectrum']['status'] == 'used'
assert modules['functional_benefic_malefic']['status'] == 'used'
assert modules['functional_benefic_malefic']['functional_benefics']
assert modules['shadbala']
assert modules['kakshya']['status'] == 'observation_only'
assert 'functional_benefic_malefic' in chart['local_consultation_layers']['available']
assert 'shadbala' in chart['local_consultation_layers']['available']
assert 'kakshya' in chart['local_consultation_layers']['available']
def test_consultation_computes_formal_and_research_varga_spectrum() -> None:
chart = _attach_local_consultation_layers(
_handler(),
_base_chart(),
dict(_BIRTH),
{'current_date': _REFERENCE_DATE},
)
spectrum = chart['modules']['varga_spectrum']
assert spectrum['counts']['formal'] == 20
assert spectrum['counts']['research_dn'] == 40
assert 'D9' in spectrum['formal']
assert spectrum['formal']['D9']['lagna']
assert 'D60' in spectrum['formal']
assert 'D13' in spectrum['research_dn']
assert spectrum['research_dn']['D13']['boundary']
assert spectrum['blocked'] == []
assert 'varga_spectrum' in chart['local_consultation_layers']['available']
def test_sub_period_boundaries_are_cut_out_of_the_periods_the_packet_shows() -> None:
@@ -409,8 +439,14 @@ def _sections(**overrides: str) -> dict:
'D1': 'used',
'D2': 'used',
'D4': 'used',
'D6': 'used',
'D7': 'used',
'D8': 'used',
'D9': 'used',
'D10': 'used',
'D11': 'used',
'D12': 'used',
'D24': 'used',
'A7': 'used',
'A10': 'used',
'UL': 'used',
@@ -478,8 +514,8 @@ def test_evidence_gate_is_never_weaker_than_the_layers_the_product_promises() ->
"""Whatever the frontend tells the user it will use must actually gate the answer.
Only entries naming a real evidence section are compared. The registry also carries human-facing
labels ('7th house/lord', 'negative holdout gate') and D11, which the engine never builds; those
cannot be requirements, and demanding them would pin every route to `degraded`.
labels ('7th house/lord', 'negative holdout gate') which cannot be requirements. Formal Vargas
the engine now materializes are required wherever the product already promises them.
"""
declared = _frontend_declared_layers()
+7 -7
View File
@@ -41,7 +41,7 @@ def timing_body(**overrides):
"requested_domains": ["timing"],
"timing_horizon": "next_12_months",
"precision_boundary": "precise_timing_blocked",
"required_evidence_categories": ["natal_foundation", "timing", "validation"],
"required_evidence_categories": ["natal_foundation", "domain", "timing", "validation"],
}
body.update(overrides)
return body
@@ -60,7 +60,7 @@ def test_versioned_plan_is_validated_against_server_route_allowlist():
"plan_depth": "standard",
"requested_domains": ["timing"],
"timing_horizon": "next_12_months",
"required_evidence_categories": ["natal_foundation", "timing", "validation"],
"required_evidence_categories": ["natal_foundation", "domain", "timing", "validation"],
"required_layers": ["Vimshottari", "Narayana", "Transit", "Varga", "negative holdout gate"],
"claim_boundary": "candidate_day_month_window_only_until_holdout_passes",
"precision_boundary": "precise_timing_blocked",
@@ -113,35 +113,35 @@ def test_versioned_plan_rejects_theme_and_resolved_route_mismatch():
"health",
["D1", "D6", "D8", "6th/8th houses", "Dasha", "non-medical boundary"],
"wellbeing_pressure_patterns_not_medical_diagnosis",
["natal_foundation", "domain"],
["natal_foundation", "domain", "timing", "validation"],
None,
),
(
"education",
["D1", "D24", "5th/9th houses", "Mercury/Jupiter", "Dasha"],
"learning_pattern_and_broad_timing_only",
["natal_foundation", "domain"],
["natal_foundation", "domain", "timing", "validation"],
None,
),
(
"migration",
["D1", "D4", "D12", "4th/12th houses", "Dasha", "Narayana"],
"migration_and_home_direction_broad_window_only",
["natal_foundation", "domain"],
["natal_foundation", "domain", "timing", "validation"],
None,
),
(
"family",
["D1", "D7", "D12", "4th/5th/9th houses", "Dasha"],
"family_pattern_not_deterministic_event_claim",
["natal_foundation", "domain"],
["natal_foundation", "domain", "timing", "validation"],
None,
),
(
"annual",
["D1", "Annual chart boundary", "Dasha", "Transit", "Tajika candidate"],
"annual_report_broad_periods_only",
["natal_foundation", "timing", "validation"],
["natal_foundation", "domain", "timing", "validation"],
"next_12_months",
),
],
+6 -7
View File
@@ -76,16 +76,11 @@ def test_workflow_does_not_auto_attach_natal_western_data_to_prashna() -> None:
assert packet is None
def test_workflow_adds_only_explicit_western_timing_layers() -> None:
def test_workflow_auto_attaches_implemented_western_timing_layers() -> None:
packet = _western_evidence_packet_from_body(
{
"entry_mode": "direct_chart",
"western_timing": {
"transit_date": "2026-07-09",
"solar_return_year": 2026,
"secondary_progression_date": "2026-07-09",
"solar_arc_date": "2026-07-09",
},
"current_date": "2026-07-09",
},
{"primary_theme": "career"},
birth_payload={
@@ -94,10 +89,14 @@ def test_workflow_adds_only_explicit_western_timing_layers() -> None:
},
)
assert packet is not None
assert set(packet["timing_techniques"]) == {
"transits", "solar_return", "secondary_progressions", "solar_arc_directions",
"converse_secondary_progressions", "converse_solar_arc_directions",
"midpoints", "lunar_return", "transit_duration_scan", "parans",
}
assert packet["sections"]["timing_techniques"]["status"] == "used"
assert "daily_hits" not in packet["timing_techniques"]["transit_duration_scan"]
def test_release_editions_include_native_western_calculator() -> None: