From b884b8e7ea7bb930504392ef1bd2082a16009592 Mon Sep 17 00:00:00 2001
From: 732642856 <732642856@qq.com>
Date: Sat, 27 Jun 2026 18:52:10 +0800
Subject: [PATCH] Harden skill rigor constraints and sync reading quality tools
---
AGENTS.md | 54 +++++
SKILL.md | 28 ++-
.../malefic_overread_top10_2026_06_27.md | 52 +++++
jyotish-app/rectification-engine.js | 60 ++++++
jyotish-app/rectification.js | 16 ++
jyotish-app/style.css | 40 ++++
.../birth-time-rectification-decision-tree.md | 203 ++++++++++++++++++
.../interpretation_template_registry.json | 44 ++++
references/quick-reference-guide.md | 3 +-
references/real-reading-quality-checklist.md | 60 ++++++
references/strict-workflow-router.md | 17 ++
scripts/first_oracle_packet_assistant.py | 28 ++-
scripts/jyotish_api_server.py | 65 +++++-
scripts/jyotish_engine.py | 182 ++++++++++++++--
scripts/local_accuracy_report.py | 9 +-
scripts/shadbala.py | 41 ++--
scripts/stage_skill_truth_whitelist.sh | 31 +++
scripts/sync_skill_truth_whitelist.sh | 35 +++
scripts/tajika.py | 53 ++++-
tests/test_api_server_security.py | 58 +++++
tests/test_cli_smoke.py | 31 ++-
tests/test_frontend_productization.py | 28 ++-
tests/test_local_accuracy_report.py | 4 +-
tests/test_oracle_packet_assistant_index.py | 6 +
tests/test_real_reading_quality_checklist.py | 39 ++++
tests/test_rectification_decision_tree_doc.py | 34 +++
tests/test_shadbala_complete.py | 23 ++
tests/test_skill_hard_constraints.py | 39 ++++
tests/test_skill_truth_sync_scripts.py | 17 ++
tests/test_tajika.py | 18 +-
30 files changed, 1262 insertions(+), 56 deletions(-)
create mode 100644 AGENTS.md
create mode 100644 docs/research/malefic_overread_top10_2026_06_27.md
create mode 100644 references/birth-time-rectification-decision-tree.md
create mode 100644 references/real-reading-quality-checklist.md
create mode 100644 scripts/stage_skill_truth_whitelist.sh
create mode 100644 scripts/sync_skill_truth_whitelist.sh
create mode 100644 tests/test_real_reading_quality_checklist.py
create mode 100644 tests/test_rectification_decision_tree_doc.py
create mode 100644 tests/test_skill_hard_constraints.py
diff --git a/AGENTS.md b/AGENTS.md
new file mode 100644
index 00000000..a15edb1b
--- /dev/null
+++ b/AGENTS.md
@@ -0,0 +1,54 @@
+# Jyotish Skill Agent Constraints
+
+本文件是当前项目给协作代理、自动化助手与派生工作流的硬约束补充。它不替代 `SKILL.md`,而是把最容易被偷懒、省略、或在多窗口工作时遗失的高严谨规则单独钉死。
+
+## 1. High-Rigor Override
+
+当用户明确要求以下任一项时,必须进入高严谨模式:
+
+- 不要凭经验泛谈
+- 必须拉满三大开源参照引擎能力
+- 必须提交底层原始数据
+- 必须验证过去案例
+- 必须避免偷工减料
+
+进入该模式后,以下规则全部强制执行:
+
+1. 必须尝试交叉参照 `PyJHora`、`VedAstro`、`jyotishganit`,并保持许可证边界。
+2. 必须优先调用本仓原生实现,不得只用轻量包装脚本代替主链代码。
+3. 涉及 timing / event / outcome,不得只看 `Vimshottari`,至少需要 `Vimshottari + Narayana Dasha` 双轨交叉。
+4. 必须按问题域强制调取相关分盘:
+ - 事业:`D10 + A10`
+ - 财富:`D2 / D11`
+ - 婚恋:`D9 + UL`
+5. 必须交付原始数据依据:度数、Dasha 边界、Shadbala / Ashtakavarga、Yoga 名称、Ayanamsa / Node mode、外部证据路径。
+
+## 2. Functional Benefic/Malefic Hard Constraint
+
+**强制调取 Functional Benefic / Malefic 判定(功能性吉凶星判定)。**
+
+这条约束与 Dasha / 分盘 / 原始数据交付同级,不得省略。
+
+执行要求:
+
+1. 每次进入高严谨模式,必须显式判定当前 Lagna 下的 `functional benefics` 与 `functional malefics`。
+2. 任何关于事业、财富、婚恋、健康、障碍、回报、应期的结论,都不得只依据自然吉凶星(natural benefic/malefic)下判断,必须叠加功能性吉凶星层。
+3. 若某颗星在自然属性与功能属性之间冲突,必须在输出中说明冲突来源,并降低置信度或标记 `blocked`。
+4. 若未调用功能性吉凶星判定,不得声称该次解读完成了高严谨模式。
+5. Technique Audit Table 中必须出现 `Functional Benefic/Malefic` 一行,说明:
+ - `Used / not used / blocked`
+ - 关键功能吉星
+ - 关键功能凶星
+ - 对结论置信度的影响
+
+## 3. Honesty Boundary
+
+以下情况必须明确写成 `blocked` 或降级置信度:
+
+- 外部 oracle 尚未闭环
+- 三大外部参照引擎中有一层无法合法或稳定调用
+- 缺少分盘、Ayanamsa、Node mode 或出生精度
+- 功能性吉凶星层未完成
+- 双重大运或多系统结果发生实质冲突
+
+禁止把内部一致性伪装成“已经全球顶级精度”。
diff --git a/SKILL.md b/SKILL.md
index b919ac53..2ba18dfa 100644
--- a/SKILL.md
+++ b/SKILL.md
@@ -70,6 +70,31 @@ description: 印度占星(Jyotish)专业解盘与推运系统。核心能力
8. **阶段七**:现代措辞包装
9. **阶段八**:输出 Technique Audit Table,逐项声明已调用/未调用/部分可用/缺失模块及其对置信度的影响。
+## 五层硬约束(全球前三引擎强制调用)
+
+当用户明确要求“不要凭经验泛谈”“必须拉满能力”“必须提交底层证据”“要做过去案例验证”“要看全球前三项目全部能力”时,进入 `high-rigor override` 模式。该模式不是建议,而是硬约束:
+
+1. **强制全量能力调用**
+ 必须同时以 `PyJHora`、`VedAstro`、`jyotishganit` 作为外部参照层,结合本仓主引擎分析。即:`PyJHora、VedAstro、jyotishganit` 三者都属于高严谨模式的强制调用面。若其中任一外部层因许可证隔离、运行环境或字段映射缺失而无法调用,必须明示 `blocked`,不得假装已比对完成。
+
+2. **强制原生代码级下潜**
+ 不允许只写轻量包装脚本做表面判断。必须优先调用本仓原生核心实现与其现有入口,例如 `scripts/yoga_engine.py`、`scripts/divisional_charts_extended.py`、`scripts/narayana_dasha.py`、`scripts/jyotish_api_server.py`、`scripts/validate_yoga_accuracy.py` 等现成主链代码。
+
+3. **强制大运双盲交叉**
+ 涉及 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 必须写明它如何削弱结论。
+
+5. **强制物理原始数据交付**
+ 不允许只给“运势不错/有机会”式结论。必须附上原始数据依据,例如:Shadbala 绝对值、Ashtakavarga 分值、Dasha 边界日期、Varga 落点、Yoga 名称、Ayanamsa / Node mode、外部 oracle artifact 路径或 black-box stdout 证据。原始数据交付是高严谨结论的唯一有效依据。
+
+诚实边界:
+
+- 若 `PyJHora / VedAstro / jyotishganit` 中任何一层无法合法或稳定调用,必须明确说明缺口来源。
+- 若外部 oracle 尚未闭环,不得把内部一致性伪装成“全球第一级精度”。
+- 若用户问题只给出模糊数据,必须先降低结论等级,而不是脑补。
+
---
## ⚠️ 强制规则(与"不跳步"同级)
@@ -215,11 +240,12 @@ description: 印度占星(Jyotish)专业解盘与推运系统。核心能力
| **动态推运** | Vimshottari、Chara Dasha(KN Rao Method, covered)、KP、Double Transit、Varshaphala、替代Dasha | `vimshottari_dasha_guide.md` `dasa-convergence-methodology.md` `alternative-dasha-systems.md` |
| **Jaimini静态层** | Chara Karaka、Karakamsha、A1-A12/UL、Graha Pada、Argala/Virodhargala、Special Lagnas(部分) | `jaimini-complete-system.md` `argala-complete-guide.md` `technique-capability-matrix.md` |
| **关系占星** | Koota 36分、Mahendra/Stree Deergha/Vedha/Rajju、D9伴侣、DK、Mangal Dosha、Papasamya、配偶六层确认 | `spouse-multi-layer-methodology.md` `darakaraka-complete-guide.md` `relationship-astrology-guide.md` |
-| **出生时间矫正** | 八大方法、自动化流程、验证报告 | `birth-time-rectification-advanced.md` |
+| **出生时间矫正** | 八大方法、自动化流程、验证报告、分盘调用决策树 | `birth-time-rectification-advanced.md` `birth-time-rectification-decision-tree.md` |
| **PDF读取** | JH/PL PDF全量提取、完整性门、交叉校验 | `pdf-chart-reading-guide.md` `data-bridge-mapping.md` |
| **Prashna问事** | 十步断卦、AL、Sphuta、Sahams、失物查询 | `prashna-complete-guide.md` `single-event-inquiry-protocol.md` |
| **多元技法** | Yogi/Ava Yogi、Tithi Lord、Rashi Tulya Navamsa、BCP、Bhrigu Pada、Pancha Pakshi、Tara Bala、Deha/Jeeva、Moolatrikona、Shodasavarga/Vimsopaka、Ashwini/Abhijit/Ketu星宿专题(需保留成熟度边界) | `yogi-avayogi-system.md` `yogi-asc-tight-orb-wealth-freeze-guide.md` `tithi-lord-relationship-system.md` `tithi-lord-freeze-execution-guide.md` `rtn-high-order-d9-freeze-execution-guide.md` `bhrigu-pada-all-event-freeze-execution-guide.md` `ashwini-abhijit-ketu-nakshatra-freeze-guide.md` `bhrigu-chakra-paddhati.md` `shodasavarga-complete-guide.md` `planetary-dignity-complete-reference.md` `alternative-dasha-systems.md` |
| **精准方法论** | PACDARES框架、九层复合方法、L3矛盾检查、三级置信度 | `precision-reading-methodology.md` |
+| **解读质检** | 真实解读结构质检、参数冻结、分盘强制展开、oracle 诚信边界 | `real-reading-quality-checklist.md` |
| **现代解读** | 现代措辞映射、现代生活场景、常见误判纠错 | `modern-language-guide.md` `common-misconceptions.md` |
| **实战智慧** | ⭐反教条主义经验精华(全球占星师真实案例反馈总结) | `practitioner-wisdom-anti-dogma.md` |
| **验证与错题** | 深度数据审计、技法缺陷与修复、推运反思、15+名人验证案例 | `audit-*` `lessons-learned-*` `verified-celebrity-cases-*` |
diff --git a/docs/research/malefic_overread_top10_2026_06_27.md b/docs/research/malefic_overread_top10_2026_06_27.md
new file mode 100644
index 00000000..a49fd59e
--- /dev/null
+++ b/docs/research/malefic_overread_top10_2026_06_27.md
@@ -0,0 +1,52 @@
+# Natural Malefic Overread Top 10 - 2026-06-27
+
+## 目的
+
+梳理当前项目里最容易把 `Saturn / Mars / Rahu / Ketu` 误读成“天然负面”的高风险点,避免高严谨输出滑回低级断法。
+
+## Top 10 风险点
+
+1. **自然凶星先验压过功能性角色**
+ 当前 `functional_benefics()` 仍是“总行星 - 功能凶星 - 自然凶星”的近似口径,容易让 `Saturn / Mars / Rahu / Ketu` 在某些 Lagna 下的有效角色被过早降权。
+
+2. **Rahu/Ketu 被简化成负面落宫叙事**
+ 如果只说“Rahu 在 10 宫不稳”“Ketu 在 10 宫断裂”,没有同时看 dispositor、题目域、D9/D10 与 Dasha 激活,解读会偏粗暴。
+
+3. **Ketu 双属性只停在规范,不一定进入每次输出**
+ 规范要求同时评估“放手/突破”,但若解释模板没有强制结构,执行层仍可能只讲耗散或断裂。
+
+4. **Saturn 被默认写成压制,而不是结构与长期兑现**
+ 在事业、考试、长期积累、名望兑现里,Saturn 往往不是单纯坏,而是“慢但真”的兑现器。
+
+5. **Mars 被默认写成冲突,而不是执行与竞争力**
+ 在创业、技术、手术、竞技、快速落地场景里,Mars 可能是关键推动器。
+
+6. **功能性吉凶星与题目域没有统一合并模板**
+ 即便 evidence layer 已经有 `functional_benefic_malefic`,如果题目域解释没有强制引用它,输出仍可能回到“见凶星就负面”。
+
+7. **节点口径(Mean / True)信息存在但不一定被解释层显式使用**
+ 如果口径声明没有稳定进入解释文本,用户会看到“节点负面断语”,却不知道基础位置口径。
+
+8. **凶星落题目域宫位时缺少“承诺层”回查**
+ 比如 `Mars in 7/8`、`Saturn aspect 4H`、`Rahu in 10H` 如果不先回查 D1 promise + Varga promise,很容易误判。
+
+9. **Dasha 激活与本命角色的区分不足**
+ 某颗星被激活,不等于它一定以负面方式实现;必须区分“该星在本命里承诺什么”与“当前被触发什么”。
+
+10. **负面叙事比中性/双刃叙事更容易被模板偷懒复用**
+ 若没有强制 safe output pattern,解释层会天然偏向“阻碍/损失/分离”的速写。
+
+## 最高 ROI 的修复方向
+
+1. 将 `Natural vs Functional vs Topic-role vs Activated outcome` 冻结为统一解释模板。
+2. 在高严谨输出里,把 `functional_benefic_malefic` 从证据层提升到主叙事层。
+3. 为 `Saturn / Mars / Rahu / Ketu` 增加题目域安全话术:
+ - 事业
+ - 婚恋
+ - 财富
+ - 年运
+
+## 结论
+
+当前项目在规范层已经明确反对“见凶星就吓人”,但执行层仍有自然凶星被过度负面化的真实风险。
+下一步应该把这一层冻结为模板,而不是继续靠人工记忆维持。
diff --git a/jyotish-app/rectification-engine.js b/jyotish-app/rectification-engine.js
index 798c7cf8..a5caf79a 100644
--- a/jyotish-app/rectification-engine.js
+++ b/jyotish-app/rectification-engine.js
@@ -43,6 +43,26 @@ export const EVENT_COLLECTION_GUIDE = [
{ key: 'mobility', cn: '搬家/远行/移民', en: 'relocation, travel, migration', categories: ['travel'] },
];
+const RECTIFICATION_THEME_VARGAS = {
+ marriage: ['D9'],
+ divorce: ['D9'],
+ relationship: ['D9'],
+ career: ['D10'],
+ job_change: ['D10'],
+ business: ['D10'],
+ fame: ['D10'],
+ child: ['D7'],
+ parents: ['D12'],
+ education: ['D24'],
+ property: ['D4'],
+ finance_pos: ['D2'],
+ finance_neg: ['D2'],
+ health: ['D30'],
+ legal: ['D30'],
+ loss: ['D30'],
+ travel: ['D4'],
+};
+
// ——— 工具函数 ———
export function getHouseLord(ai, h) { return SIGN_LORDS[SIGNS[(ai + h - 1) % 12]]; }
@@ -193,6 +213,44 @@ function summarizeCandidateEvidence(result, events) {
};
}
+function buildRectificationDecisionPlan(events) {
+ const valid = (events || []).filter(evt => evt?.date && EVENT_CATEGORIES[evt.category]);
+ const categorySet = [...new Set(valid.map(evt => evt.category))];
+ const themeVargas = [];
+ for (const category of categorySet) {
+ const vargas = RECTIFICATION_THEME_VARGAS[category] || [];
+ for (const varga of vargas) {
+ if (!themeVargas.includes(varga)) themeVargas.push(varga);
+ }
+ }
+
+ const eventCount = valid.length;
+ const tightenWithSensitive = themeVargas.length > 0;
+ const useD30 = themeVargas.includes('D30');
+ const useD60 = eventCount >= 8 && themeVargas.length >= 2;
+
+ return {
+ principle: 'Dasha 定框,D9/D10 定核心,专项分盘补刀,D30/D60 谨慎后置。',
+ ordered_layers: [
+ { step: 1, label: 'Dasha + dated events', role: '主引擎', reason: '先用真实事件把时间框住,再决定要不要细分盘。' },
+ { step: 2, label: 'D9', role: '核心验证', reason: '婚姻、关系质量、内在成熟路线的第一核心分盘。' },
+ { step: 3, label: 'D10', role: '核心验证', reason: '事业显化、职业兑现、升职和名声路径的第一核心分盘。' },
+ { step: 4, label: themeVargas.length ? themeVargas.join(' / ') : 'D7 / D12 / D24 / D4 / D2', role: '主题补刀', reason: '按用户真实事件主题调用专项分盘,不一股脑全开。' },
+ { step: 5, label: 'Nakshatra / Pada / house shifts', role: '细分钟收口', reason: '用于最后 2-10 分钟缩窄,不适合一开始拍板。' },
+ { step: 6, label: useD30 ? 'D30' : 'D30(仅当有健康/事故/创伤事件)', role: '高敏慎用', reason: '高敏但易过拟合,只在明确压力事件时启用。' },
+ { step: 7, label: useD60 ? 'D60(仅作最后参考)' : 'D60(默认后置)', role: '超高敏慎用', reason: '只在高收敛后作为最后参考,不反向主导校时结论。' },
+ ],
+ selected_theme_vargas: themeVargas,
+ event_count: eventCount,
+ warnings: [
+ ...(eventCount < 5 ? ['事件少于 5 个时,不建议过早依赖高敏感分盘。'] : []),
+ ...(!tightenWithSensitive ? ['当前事件主题不足以触发专项分盘,先补婚恋/事业/家庭/健康类 dated events。'] : []),
+ ...(!useD30 ? ['没有明确健康/事故/创伤事件时,D30 默认不主导结论。'] : []),
+ ...(!useD60 ? ['D60 默认后置,只在高收敛后作最后参考。'] : []),
+ ],
+ };
+}
+
function buildRectificationAudit(best, results, events) {
const second = results[1] || null;
const coverage = summarizeEventCoverage(events);
@@ -280,6 +338,7 @@ export async function runRectification(birth, events, options={}) {
if(results.findIndex(r=>r.offsetMin===0)>0) results[0].correctionEffective=true;
}
const audit = results.length ? buildRectificationAudit(results[0], results, events) : null;
+ const decisionPlan = buildRectificationDecisionPlan(events);
return {
birth,events,options:{rangeMin,stepMin,totalCandidates:offsets.length},
baseChartInfo:{
@@ -290,6 +349,7 @@ export async function runRectification(birth, events, options={}) {
results,bestMatch:results[0]||null,
confidence:calcConf(results,events.length,audit),
audit,
+ decisionPlan,
};
}
diff --git a/jyotish-app/rectification.js b/jyotish-app/rectification.js
index da23f337..ca5b0935 100644
--- a/jyotish-app/rectification.js
+++ b/jyotish-app/rectification.js
@@ -155,6 +155,7 @@ function renderResults(container, result) {
const lang = getLang();
const { bestMatch: bm, confidence: conf, baseChartInfo: base, results: all } = result;
const audit = result.audit || {};
+ const decisionPlan = result.decisionPlan || {};
const confClr = { '高': '#22c55e', '中': '#f59e0b', '低': '#ef4444', '不确定': '#9ca3af' };
const correctedBirth = buildCorrectedBirth(result.birth, bm.offsetMin);
const reportText = buildRectificationReportText(result, correctedBirth);
@@ -212,6 +213,21 @@ function renderResults(container, result) {
证据闭环
${auditCards}
+
+
分盘调用顺序
+
${escapeHtml(decisionPlan.principle || 'Dasha 定框,核心分盘先行,专项分盘后置。')}
+
${(decisionPlan.ordered_layers || []).map(layer => `
+ -
+
+ ${escapeHtml(layer.label)}
+ ${escapeHtml(layer.role)}
+
+ ${escapeHtml(layer.reason)}
+
+ `).join('')}
+ ${(decisionPlan.selected_theme_vargas || []).length ? `
本轮优先专项分盘:${escapeHtml(decisionPlan.selected_theme_vargas.join(' / '))}
` : ''}
+ ${(decisionPlan.warnings || []).length ? `
${decisionPlan.warnings.map(w => `
${escapeHtml(w)}
`).join('')}
` : ''}
+
${t('rect.scoring.title')}
${scoreBars}
diff --git a/jyotish-app/style.css b/jyotish-app/style.css
index 8d81fdb6..9041d2ac 100644
--- a/jyotish-app/style.css
+++ b/jyotish-app/style.css
@@ -4768,6 +4768,46 @@ body { font-family: var(--font-body); background: var(--bg-page); color: var(--t
font-size: 12px;
line-height: 1.55;
}
+.rect-plan { margin-bottom: 16px; }
+.rect-plan-principle {
+ margin: 0 0 10px;
+ color: var(--text-primary);
+ font-size: 13px;
+ line-height: 1.6;
+}
+.rect-plan-list {
+ margin: 0;
+ padding-left: 18px;
+ display: grid;
+ gap: 10px;
+}
+.rect-plan-row {
+ display: flex;
+ align-items: baseline;
+ justify-content: space-between;
+ gap: 12px;
+}
+.rect-plan-row strong {
+ font-size: 13px;
+ color: var(--text-primary);
+}
+.rect-plan-row span {
+ font-size: 11px;
+ color: var(--text-secondary);
+ text-transform: uppercase;
+ letter-spacing: 0;
+}
+.rect-plan-list p {
+ margin: 4px 0 0;
+ font-size: 12px;
+ color: var(--text-secondary);
+ line-height: 1.55;
+}
+.rect-plan-focus {
+ margin: 12px 0 0;
+ font-size: 12px;
+ color: var(--text-primary);
+}
/* Top 10 表格 */
.rect-top-results { margin-bottom: 16px; }
diff --git a/references/birth-time-rectification-decision-tree.md b/references/birth-time-rectification-decision-tree.md
new file mode 100644
index 00000000..7c38cc3b
--- /dev/null
+++ b/references/birth-time-rectification-decision-tree.md
@@ -0,0 +1,203 @@
+# 生时校正分盘调用决策树
+
+> 目标:把生时校正从“所有分盘一起上”改成“按证据价值和过拟合风险分层调用”。
+> 结论先行:**不是所有分盘一股脑上。**
+
+## 1. 总原则
+
+生时校正时,优先级应当是:
+
+1. `Dasha + dated events`
+2. `D9`
+3. `D10`
+4. `D7 / D12 / D24 / D4 / D2` 按主题选
+5. `Nakshatra / Pada / house shifts`
+6. `D30`
+7. `D60`
+
+一句话版本:
+
+**Dasha 定框,D9/D10 定核心,D12/D7/D24 等按题补刀,D30/D60 最后谨慎使用。**
+
+这和当前仓库里已有的校时权重一致:
+
+- `Dasha` 40%
+- `分盘一致性` 35%
+- `宫位变化` 15%
+- `Nakshatra Pada` 10%
+
+对应实现见:
+
+- [jyotish-app/rectification-engine.js](/Users/wuyongnaren/Documents/印度占星/jyotish-app/rectification-engine.js)
+- [references/varga-system-quick-reference.md](/Users/wuyongnaren/Documents/印度占星/references/varga-system-quick-reference.md)
+
+## 2. 第一层:主引擎
+
+### Dasha + dated events
+
+这是第一裁判,不是某一张分盘。
+
+先用真实事件把时间框住:
+
+- 结婚 / 分手 / 离婚
+- 入职 / 升职 / 创业 / 换工作
+- 搬迁 / 出国 / 移民
+- 生育 / 流产 / 子女关键年份
+- 健康危机 / 手术 / 事故
+- 重大财富变化 / 破财
+
+如果没有 dated events,只靠分盘做校时,风险会显著上升。
+
+## 3. 第二层:核心分盘
+
+### D9
+
+最适合验证:
+
+- 婚姻 / 关系质量
+- 亲密关系成熟度
+- 配偶画像
+- 内在命运路线是否与 D1 同向
+
+### D10
+
+最适合验证:
+
+- 事业显化
+- 职业兑现
+- 升职 / 名声 / 社会角色
+- 事业方向切换是否成立
+
+如果用户提供的事件主要是婚恋和事业,**D9 + D10 是最值钱的双核**。
+
+## 4. 第三层:主题分盘补刀
+
+当 Dasha 已经定出大框,但两个候选时间都还能解释时,再按主题调专项分盘:
+
+| 事件主题 | 优先分盘 | 用法 |
+|---|---|---|
+| 婚姻/关系 | `D9` | 核心,不是补充 |
+| 事业/升职/换工作 | `D10` | 核心,不是补充 |
+| 子女/生育 | `D7` | 用于拉开生育相关年份与子女主题差异 |
+| 父母/家族 | `D12` | 辅助裁判,不是第一裁判 |
+| 教育/考试 | `D24` | 学业、考试、研究路线 |
+| 房产/搬迁 | `D4` | 房产、居所、迁移、家庭落点 |
+| 财富/收入变化 | `D2` | 收入、资产、财务结构变化 |
+| 健康/事故/创伤 | `D30` | 只在有明确健康事件时用 |
+
+其中 `D12` 的角色要说清楚:
+
+- 它很有价值
+- 但通常在 `D9/D10` 不能彻底拉开差距时再上
+- 所以它是**辅助裁判**,不是**第一裁判**
+
+## 5. 第四层:细分钟收口层
+
+这一层适合最后 2-10 分钟的缩窄,不适合一开始就拍板:
+
+- `Nakshatra / Pada`
+- 特殊 Lagna
+- 敏感点
+- 分盘上升切换点
+- 宫位变动
+
+它们的作用是:
+
+- 在大框已经基本收敛后
+- 继续缩小到更细的分钟段
+
+不应让这一层反过来主导整体结论。
+
+## 6. 第五层:超高敏层,慎用
+
+### D30
+
+可用,但前提是用户有明确的:
+
+- 健康危机
+- 手术
+- 创伤
+- 事故
+- 极端压力事件
+
+### D60
+
+只适合在已经高度收敛后,作为最后参考。
+
+不要让 `D60` 反过来主导校时结论。原因很简单:
+
+- 对出生时间太敏感
+- 在事件样本不足时特别容易过拟合
+- 很容易出现“解释很漂亮,但时间其实没校准好”
+
+## 7. 实战决策树
+
+### Step 1
+
+先问:有没有 5 个以上带年份或日期的真实事件?
+
+- 没有:先补事件,不急着堆分盘
+- 有:进入 Dasha 定框
+
+### Step 2
+
+事件主要落在哪个主题?
+
+- 婚姻/关系 -> `D9`
+- 事业/升职/换工作 -> `D10`
+- 子女/生育 -> `D7`
+- 父母/家族 -> `D12`
+- 教育/考试 -> `D24`
+- 房产/搬迁 -> `D4`
+- 财富/收入 -> `D2`
+- 健康/事故/创伤 -> `D30`
+
+### Step 3
+
+如果两个候选时间在核心分盘上仍都说得通:
+
+- 再看 `Nakshatra / Pada / house shifts`
+- 再看更窄的分盘上升切换
+
+### Step 4
+
+只有当 Dasha + 核心分盘 + 主题分盘都已经明显收敛后,才允许上 `D60` 做最后参考。
+
+## 8. 时间敏感度提醒
+
+仓库当前已有的分盘时间精度要求:
+
+- `D9 / D10`: `±2分钟`
+- `D2/D3/D4/D7/D12`: `±5分钟`
+- `D30`: `±1分钟`
+- `D60`: `±30秒`
+
+因此:
+
+- `D9/D10` 足够敏感,适合作为校时主力
+- `D30/D60` 过于敏感,适合作为后段验证
+
+## 9. 禁止做法
+
+以下做法不推荐:
+
+- 一开始就把所有分盘全开
+- 用 `D60` 反向主导时间结论
+- 没有 dated events 时过度依赖高敏分盘
+- 用单一分盘推翻 `D1 + Dasha + 核心分盘` 的同向证据
+
+## 10. 落地口径
+
+校时时,默认解释顺序应是:
+
+1. `Dasha + dated events`
+2. `D9`
+3. `D10`
+4. 主题专项分盘
+5. `Nakshatra / Pada / house shifts`
+6. `D30`
+7. `D60`
+
+如果需要一句给用户看的简明口径,就用这句:
+
+**校时主力不是“所有分盘”,而是“Dasha 定框,D9/D10 定核心,D12/D7/D24 等按题补刀,D30/D60 最后谨慎使用”。**
diff --git a/references/interpretation_template_registry.json b/references/interpretation_template_registry.json
index 728cdf9f..e2b2ecd4 100644
--- a/references/interpretation_template_registry.json
+++ b/references/interpretation_template_registry.json
@@ -299,6 +299,50 @@
"Upgrade only when D1 promise, D9 support, UL condition and timing all point in the same direction."
]
},
+ "natural_malefic_functional_role_guardrail": {
+ "title": "Natural Malefic vs Functional Role Guardrail",
+ "status": "frozen_template",
+ "authority_level": "A/B/C layered",
+ "domain": ["interpretation_boundary", "functional_role", "saturn", "mars", "rahu", "ketu"],
+ "source_refs": [
+ "AGENTS.md",
+ "SKILL.md",
+ "references/prediction-boundary-protocol.md",
+ "docs/research/malefic_overread_top10_2026_06_27.md"
+ ],
+ "trigger_patterns": [
+ "Saturn negative",
+ "Mars negative",
+ "Rahu negative",
+ "Ketu negative",
+ "功能性吉凶星",
+ "自然凶星",
+ "Rahu/Ketu 负面",
+ "土星是不是坏",
+ "火星是不是坏"
+ ],
+ "required_cross_checks": [
+ "Separate natural benefic/malefic status from functional role under the current Lagna",
+ "State the topic-domain role before giving any positive or negative conclusion",
+ "Check D1 promise plus the relevant Varga before calling the planet harmful or helpful",
+ "Check Dasha/Transit activation before upgrading from trait to event claim",
+ "For Rahu/Ketu, declare node mode/dispositor and evaluate both breakthrough and loss patterns"
+ ],
+ "confidence_ceiling": "B when natural role, functional role, topic-domain role and timing all agree; C when only natural malefic language is available or the functional role is unresolved.",
+ "forbidden_claims": [
+ "Saturn alone means failure",
+ "Mars alone means relationship destruction",
+ "Rahu or Ketu alone proves damage, scandal or collapse",
+ "A natural malefic label alone overrides functional role, D1 promise or timing"
+ ],
+ "safe_output_patterns": [
+ "Always describe the planet in four layers: natural nature, functional role, topic-domain role and current activated outcome.",
+ "Frame Saturn as delay, responsibility, structure or long-cycle payoff depending on promise and activation, not as automatic negativity.",
+ "Frame Mars as conflict, execution, surgery, competition or entrepreneurial drive depending on context, not as automatic damage.",
+ "Frame Rahu and Ketu as dual-edge amplifiers that can produce breakthrough, disruption, detachment or leakage depending on promise, dispositor and activation.",
+ "If natural and functional meanings conflict, explicitly lower confidence and let the promise/timing layer decide the practical reading."
+ ]
+ },
"pushkara_vargottama_avastha_synthesis": {
"title": "Pushkara / Vargottama / Avastha / RTN Synthesis Template",
"status": "frozen_template",
diff --git a/references/quick-reference-guide.md b/references/quick-reference-guide.md
index 835d895b..e6bd4cc5 100644
--- a/references/quick-reference-guide.md
+++ b/references/quick-reference-guide.md
@@ -414,7 +414,8 @@ SCRIPT=~/.workbuddy/skills/jyotish-vedic-astrology/scripts/jyotish_engine.py
1. **common-misconceptions.md**:印度占星常见误判与冲突问题集(错题本)⭐⭐⭐⭐⭐
2. **modern-language-guide.md**:现代生活措辞指南
3. **birth-time-rectification-advanced.md**:出生时间矫正高级方法论
-4. **birth-time-rectification-cases.md**:出生时间矫正案例集
+4. **birth-time-rectification-decision-tree.md**:生时校正分盘调用决策树(Dasha 定框,D9/D10 定核心)
+5. **birth-time-rectification-cases.md**:出生时间矫正案例集
5. **pdf-chart-reading-guide.md**:PDF星盘读取指南 v3.0
6. **prediction-checklist.md**:预测清单
7. **data-bridge-mapping.md**:数据桥接映射
diff --git a/references/real-reading-quality-checklist.md b/references/real-reading-quality-checklist.md
new file mode 100644
index 00000000..091c8395
--- /dev/null
+++ b/references/real-reading-quality-checklist.md
@@ -0,0 +1,60 @@
+# Real Reading Quality Checklist
+
+> 作用:给真实解读、案例分析、代理执行和高严谨输出做统一质检。
+> 核心原则:**最容易毁掉答案质量的,不是少看一颗星,而是少看一层裁决结构。**
+
+## A. 参数冻结层
+
+1. `Ayanamsa` 是否显式声明?
+2. `Node mode` (`Mean` / `True`) 是否显式声明?
+3. 相关 `house system / bhava policy` 是否声明?
+4. 若参数层有冲突或缺失,是否明确降置信度?
+
+## B. 路由与题目域层
+
+5. 是否先读取并执行了 `strict-workflow-router.md` 对应 route?
+6. 是否按题目域强制展开相关分盘,而不是只看 D1?
+7. 是否把 `covered` 和 `complete` 分开表达,没有偷换成熟度?
+
+## C. 承诺层
+
+8. 是否先看了 D1 的 promise,而不是直接进入细节技法?
+9. 是否检查了题目域核心宫位、宫主、承诺型 Yoga?
+10. 是否避免让单一 Yoga / 单一文章规则越权?
+
+## D. 功能性角色层
+
+11. 是否显式给出 `Functional Benefic/Malefic`?
+12. 是否区分了自然属性、功能属性、题目域角色、当前激活结果?
+13. 对 `Saturn / Mars / Rahu / Ketu` 是否避免了“天然负面”式偷懒断法?
+
+## E. 分盘层
+
+14. 婚恋是否至少展开 `D9 / UL / DK`?
+15. 事业是否至少展开 `D10 / A10 / AmK/Karakamsha`?
+16. 财富是否至少展开 `D2/D11`,并回看 D1/D9/D10 的变现链?
+17. 若使用 `D30 / D60`,是否明确说明这是高敏后置层而非主裁判?
+
+## F. Timing 层
+
+18. 是否至少执行 `Vimshottari + Narayana` 双轨交叉?
+19. 若做具体事件判断,是否加入 `Transit / Double Transit / annual / KP or Chara support`?
+20. 如果 timing 只做到窗口级,而不是日期级,是否说清了边界?
+
+## G. 外部真值与诚信边界
+
+21. 是否清楚声明 `production_tuning_allowed` 的当前状态?
+22. 是否把外部 oracle 未闭环的前线(Dasha boundary / Shadbala absolute / Tajika annual)说清楚?
+23. 是否避免把内部一致性伪装成传统软件级真值冻结?
+
+## H. 输出层
+
+24. 是否输出 `Technique Audit Table` 或等价审计表?
+25. 是否给出 `used / partial / blocked / not used` 级别说明?
+26. 是否给出置信度,而不是只给断语?
+27. 是否把 must-not-overclaim 边界说清楚?
+
+## 一句话用法
+
+只要这张表里有 3-4 项没过,就不要把输出包装成“高严谨结论”。
+只要 `参数层 + 承诺层 + 分盘层 + timing 层 + 外部真值边界` 没一起过,就不能把 skill 说成“已经无疏漏封顶”。
diff --git a/references/strict-workflow-router.md b/references/strict-workflow-router.md
index c25bb17d..07c241ce 100644
--- a/references/strict-workflow-router.md
+++ b/references/strict-workflow-router.md
@@ -12,6 +12,23 @@ The user should not need to ask for Chara Dasha, A10, Argala, Shadbala, Ashtakav
Every non-trivial reading must end with a **Technique Audit Table**.
+## 0.5 High-Rigor Override
+
+When the user asks for maximum rigor, no guessing, past-event verification, raw evidence, or full parity with top open-source Jyotish engines, the reading enters **High-Rigor Override**.
+
+Under this override, the engine must:
+
+1. Attempt cross-reference against `PyJHora / VedAstro / jyotishganit`, while respecting license boundaries.
+2. Use native repository implementations instead of lightweight summary scripts whenever a native module already exists.
+3. Require `Vimshottari + Narayana Dasha` before making major timing claims; if they materially conflict, mark the conclusion `blocked` or downgrade confidence.
+4. Force relevant divisional microscope layers:
+ - `D10` + `A10` for career
+ - `D2/D11` for wealth
+ - `D9` + `UL` for relationship
+5. Include raw outputs: exact degrees, dasha boundaries, Shadbala / AV values, Yoga names, ayanamsa / node mode, and external evidence pointers where available.
+
+If any of the above cannot be completed because of missing birth precision, external oracle gaps, license quarantine, adapter failure, or missing field mapping, the report must explicitly say `blocked` rather than silently continuing with a fake-complete conclusion.
+
---
## 1. Question router
diff --git a/scripts/first_oracle_packet_assistant.py b/scripts/first_oracle_packet_assistant.py
index 27184fc0..0de44a2d 100644
--- a/scripts/first_oracle_packet_assistant.py
+++ b/scripts/first_oracle_packet_assistant.py
@@ -26,6 +26,8 @@ FRONTS = {
],
"operator_card": "docs/benchmark/dasha_steve_jobs_first_packet_operator_card.md",
"packet_template": "references/oracle/evidence_packet_templates/dasha_steve_jobs_lahiri_first_packet_only.json",
+ "oracle_file": "references/oracle/dasha_shadbala_oracle_cases.json",
+ "apply_script": "scripts/oracle_collection_queue.py",
"external_sources": [
"JHora Vimshottari Dasha screen",
"PyJHora black-box dasha output",
@@ -43,6 +45,8 @@ FRONTS = {
],
"operator_card": "docs/benchmark/tajika_einstein_1905_first_packet_operator_card.md",
"packet_template": "references/oracle/artifacts/pending_packets/external_template_einstein_varshaphala_1905_lahiri.json",
+ "oracle_file": "references/oracle/tajika_annual_oracle_cases.json",
+ "apply_script": "scripts/tajika_annual_oracle_queue.py",
"external_sources": [
"JHora Varshaphala/Tajika annual chart screen",
"PyJHora black-box annual output",
@@ -60,6 +64,8 @@ FRONTS = {
],
"operator_card": "docs/benchmark/shadbala_redacted_place_raman_first_packet_operator_card.md",
"packet_template": "references/oracle/evidence_packet_templates/shadbala_redacted_place_raman_first_packet.json",
+ "oracle_file": "references/oracle/dasha_shadbala_oracle_cases.json",
+ "apply_script": "scripts/oracle_collection_queue.py",
"external_sources": [
"JHora Shadbala component table screenshot",
"PyJHora black-box shadbala output",
@@ -69,6 +75,24 @@ FRONTS = {
}
+def _fallback_apply_command(config: dict[str, Any]) -> str:
+ return (
+ f"python3 {config['apply_script']} "
+ f"--oracle-file {config['oracle_file']} "
+ f"--apply-packet {config['packet_template']} "
+ "--format json"
+ )
+
+
+def _fallback_validate_command(config: dict[str, Any]) -> str:
+ queue_path = "/tmp/jyotish_oracle_queue_filled.json"
+ return (
+ f"python3 scripts/oracle_collection_queue.py --oracle-file {config['oracle_file']} "
+ f"--format json > {queue_path} && "
+ f"python3 scripts/oracle_evidence_validator.py --queue-file {queue_path}"
+ )
+
+
def _group_missing_fields(missing_fields: list[str]) -> dict[str, Any]:
metadata_fields = [field for field in missing_fields if field.startswith("metadata.")]
target_fields = [field for field in missing_fields if field.startswith("target.")]
@@ -177,8 +201,8 @@ def build_report(front: str) -> dict[str, Any]:
first = status.get("first_priority") or {
"case_id": packet.get("case_id"),
"capture_id": packet.get("capture_id"),
- "apply_command": "",
- "validate_command": "",
+ "apply_command": _fallback_apply_command(config),
+ "validate_command": _fallback_validate_command(config),
"packet_path": config["packet_template"],
}
packet_missing = _packet_missing(config["packet_template"])
diff --git a/scripts/jyotish_api_server.py b/scripts/jyotish_api_server.py
index 35ea3621..10718df9 100644
--- a/scripts/jyotish_api_server.py
+++ b/scripts/jyotish_api_server.py
@@ -189,9 +189,21 @@ class JyotishAPIHandler(BaseHTTPRequestHandler):
path = urlparse(self.path).path
try:
if path == '/api/health':
+ swisseph_available = False
+ swisseph_version = None
+ try:
+ import swisseph as swe
+ swisseph_available = True
+ swisseph_version = getattr(swe, 'version', None) or getattr(swe, '__version__', None)
+ except Exception:
+ swisseph_available = False
+ swisseph_version = None
self._json({
'status': 'ok',
'version': '6.9.14',
+ 'swisseph_available': swisseph_available,
+ 'swisseph_version': swisseph_version,
+ 'ayanamsa_default': 'lahiri',
'modules': 'Chart/KP/Synastry/Prashna/Remedies/Dasha/Varga/Jaimini/Ashtakavarga/Shadbala/Yoga/Aspects/Tajika/Muhurta/BhavaChalit/BhavaBala/Sudarshana/Nakshatra/Transit/RectificationGate/CaseValidation/DivisionalYoga/Kakshya',
})
elif path == '/api/cities':
@@ -359,6 +371,19 @@ class JyotishAPIHandler(BaseHTTPRequestHandler):
self._check_range(key, number, min_value, max_value)
return number
+
+ def _parse_timezone(self, body, lat, lon, year, month, day, hour, minute, second):
+ tz = body.get('tz')
+ if tz is not None and tz != "":
+ return self._get_float(body, 'tz', 8, -14, 14)
+ from timezone_utils import infer_timezone
+ from datetime import datetime
+ try:
+ dt = datetime(int(year), int(month), int(day), int(hour), int(minute), int(second))
+ except Exception:
+ dt = datetime.utcnow()
+ return infer_timezone(lat, lon, dt)
+
def _get_float(self, body, key, default, min_value=None, max_value=None):
value = body.get(key, default)
try:
@@ -1355,7 +1380,7 @@ class JyotishAPIHandler(BaseHTTPRequestHandler):
second = self._get_birth_second(body)
lat = self._get_float(body, 'lat', 39.9, -90, 90)
lon = self._get_float(body, 'lon', 116.4, -180, 180)
- tz = self._get_float(body, 'tz', 8, -14, 14)
+ tz = self._parse_timezone(body, lat, lon, year, month, day, hour, minute, second)
try:
datetime(year, month, day, int(hour), int(minute), int(second))
except ValueError as e:
@@ -1555,7 +1580,7 @@ class JyotishAPIHandler(BaseHTTPRequestHandler):
second = self._get_birth_second(body)
lat = self._get_float(body, 'lat', 39.9, -90, 90)
lon = self._get_float(body, 'lon', 116.4, -180, 180)
- tz = self._get_float(body, 'tz', 8, -14, 14)
+ tz = self._parse_timezone(body, lat, lon, year, month, day, hour, minute, second)
try:
datetime(year, month, day, int(hour), int(minute), int(second))
except ValueError as e:
@@ -1800,6 +1825,7 @@ class JyotishAPIHandler(BaseHTTPRequestHandler):
planets = chart.get('planets') or {}
dasha = chart.get('dasha') or {}
shadbala = chart.get('shadbala') or {}
+ functional_layer = self._functional_benefic_malefic_snapshot(planets, ascendant)
top_strength = sorted(
[
{
@@ -1868,6 +1894,7 @@ class JyotishAPIHandler(BaseHTTPRequestHandler):
'strength': {
'shadbala_ranking': top_strength,
},
+ 'functional_benefic_malefic': functional_layer,
'quality_boundary': {
'external_oracle_status': 'D1/D9/VedAstro longitude boundary covered; Dasha/Shadbala external absolute calibration still requires multi-source oracle expansion.',
},
@@ -1892,6 +1919,34 @@ class JyotishAPIHandler(BaseHTTPRequestHandler):
},
}
+ def _functional_benefic_malefic_snapshot(self, planets, ascendant):
+ try:
+ from yoga_engine import YogaContext
+ asc_sign = ascendant.get('sign')
+ if not asc_sign or not isinstance(planets, dict):
+ raise ValueError('missing ascendant sign or planets')
+ context = YogaContext(planets, asc_sign)
+ benefics = context.functional_benefics()
+ malefics = context.functional_malefics()
+ return {
+ 'status': 'used',
+ 'ascendant': asc_sign,
+ 'functional_benefics': benefics,
+ 'functional_malefics': malefics,
+ 'effect_on_confidence': (
+ '高严谨模式下必须叠加功能性吉凶星;若与自然吉凶属性冲突,'
+ '应降低置信度并在 Technique Audit Table 中显式说明。'
+ ),
+ }
+ except Exception as exc:
+ return {
+ 'status': 'blocked',
+ 'ascendant': ascendant.get('sign'),
+ 'functional_benefics': [],
+ 'functional_malefics': [],
+ 'effect_on_confidence': f'未完成功能性吉凶星判定,需降低高严谨结论置信度: {exc}',
+ }
+
def _detect_yogas(self, planets, asc_idx):
yogas = []
KENDRA = {1,4,7,10}
@@ -2870,7 +2925,7 @@ class JyotishAPIHandler(BaseHTTPRequestHandler):
day = self._get_int(body, 'day', 1, 1, 31)
lat = self._get_float(body, 'lat', body.get('birth_lat', 0), -90, 90)
lon = self._get_float(body, 'lon', body.get('birth_lon', 0), -180, 180)
- tz = self._get_float(body, 'tz', body.get('birth_tz', 0), -14, 14)
+ tz = self._parse_timezone(body, lat, lon, year, month, day, hour, minute, second)
hour_decimal = self._birth_hour_decimal(birth_hour, birth_minute, birth_second)
solar_lon = planets.get('Sun', {}).get('lon', 0)
base_planets = base_result.get('planets', {}) if isinstance(base_result, dict) else {}
@@ -2925,7 +2980,7 @@ class JyotishAPIHandler(BaseHTTPRequestHandler):
birth_dt = self._parse_birth_datetime(body)
lat = self._get_float(body, 'lat', body.get('birth_lat', 0), -90, 90)
lon = self._get_float(body, 'lon', body.get('birth_lon', 0), -180, 180)
- tz = self._get_float(body, 'tz', body.get('birth_tz', 0), -14, 14)
+ tz = self._parse_timezone(body, lat, lon, year, month, day, hour, minute, second)
solar_return = _load_local_module('solar_return')
report = solar_return.solar_return_full_report(
birth_dt.year,
@@ -3539,7 +3594,7 @@ class JyotishAPIHandler(BaseHTTPRequestHandler):
second = self._get_birth_second(body)
lat = self._get_float(body, 'lat', body.get('birth_lat', 0), -90, 90)
lon = self._get_float(body, 'lon', body.get('birth_lon', 0), -180, 180)
- tz = self._get_float(body, 'tz', body.get('birth_tz', 0), -14, 14)
+ tz = self._parse_timezone(body, lat, lon, year, month, day, hour, minute, second)
datetime(year, month, day, int(hour), int(minute), int(second))
import swisseph as swe
hour_ut = self._birth_hour_decimal(hour, minute, second) - tz
diff --git a/scripts/jyotish_engine.py b/scripts/jyotish_engine.py
index 2ab92f26..4c55708f 100644
--- a/scripts/jyotish_engine.py
+++ b/scripts/jyotish_engine.py
@@ -701,17 +701,152 @@ def _planet_snapshot(planets, planet_name):
def _oracle_progress_snapshot():
- return {
- 'scope': 'external_oracle_evidence_validation',
- 'collection_queue': 'external_oracle_collection_queue',
- 'total_packets': 5,
- 'valid_packets': 0,
- 'ready_for_calibration': 0,
- 'production_tuning_allowed': False,
- 'artifact_policy': 'references/oracle/artifacts/',
- 'promotion_rule': 'external_verified requires source_artifact, filled target values, and non-local-engine external evidence.',
- 'boundary': 'Dasha/Shadbala absolute values are not externally calibrated until enough packets pass validation.',
- }
+ try:
+ import json
+ import subprocess
+ import sys
+ import tempfile
+ from pathlib import Path
+
+ root = Path(__file__).resolve().parents[1]
+ oracle_file = root / 'references' / 'oracle' / 'dasha_shadbala_oracle_cases.json'
+ with tempfile.NamedTemporaryFile('w+', suffix='.json', delete=True, encoding='utf-8') as handle:
+ queue = subprocess.run(
+ [sys.executable, 'scripts/oracle_collection_queue.py', '--oracle-file', str(oracle_file), '--format', 'json'],
+ cwd=root,
+ text=True,
+ capture_output=True,
+ timeout=90,
+ check=False,
+ )
+ if queue.returncode != 0:
+ raise RuntimeError(queue.stderr.strip() or queue.stdout.strip())
+ handle.write(queue.stdout)
+ handle.flush()
+ validation = subprocess.run(
+ [sys.executable, 'scripts/oracle_evidence_validator.py', '--queue-file', handle.name],
+ cwd=root,
+ text=True,
+ capture_output=True,
+ timeout=90,
+ check=False,
+ )
+ if validation.returncode != 0:
+ raise RuntimeError(validation.stderr.strip() or validation.stdout.strip())
+ report = json.loads(validation.stdout)
+ summary = report.get('summary', {})
+ return {
+ 'scope': 'external_oracle_evidence_validation',
+ 'collection_queue': 'external_oracle_collection_queue',
+ 'total_packets': summary.get('total_packets', 0),
+ 'valid_packets': summary.get('valid_packets', 0),
+ 'ready_for_calibration': summary.get('ready_for_calibration', 0),
+ 'production_tuning_allowed': summary.get('production_tuning_allowed', False),
+ 'artifact_policy': 'references/oracle/artifacts/',
+ 'promotion_rule': 'external_verified requires source_artifact, filled target values, and non-local-engine external evidence.',
+ 'boundary': report.get('boundary', 'Dasha/Shadbala absolute values are not externally calibrated until enough packets pass validation.'),
+ }
+ except Exception as exc:
+ return {
+ 'scope': 'external_oracle_evidence_validation',
+ 'collection_queue': 'external_oracle_collection_queue',
+ 'total_packets': 0,
+ 'valid_packets': 0,
+ 'ready_for_calibration': 0,
+ 'production_tuning_allowed': False,
+ 'artifact_policy': 'references/oracle/artifacts/',
+ 'promotion_rule': 'external_verified requires source_artifact, filled target values, and non-local-engine external evidence.',
+ 'boundary': f'Oracle progress unavailable: {exc}',
+ }
+
+
+def _functional_benefic_malefic_snapshot(planets, ascendant):
+ try:
+ from yoga_engine import YogaContext
+ asc_sign = ascendant.get('sign') if isinstance(ascendant, dict) else None
+ if not asc_sign or not isinstance(planets, dict):
+ raise ValueError('missing ascendant sign or planets')
+ context = YogaContext(planets, asc_sign)
+ benefics = context.functional_benefics()
+ malefics = context.functional_malefics()
+ return {
+ 'status': 'used',
+ 'ascendant': asc_sign,
+ 'functional_benefics': benefics,
+ 'functional_malefics': malefics,
+ 'effect_on_confidence': (
+ '高严谨模式下必须叠加功能性吉凶星;若与自然吉凶属性冲突,'
+ '应降低置信度并在 Technique Audit Table 中显式说明。'
+ ),
+ }
+ except Exception as exc:
+ return {
+ 'status': 'blocked',
+ 'ascendant': ascendant.get('sign') if isinstance(ascendant, dict) else None,
+ 'functional_benefics': [],
+ 'functional_malefics': [],
+ 'effect_on_confidence': f'未完成功能性吉凶星判定,需降低高严谨结论置信度: {exc}',
+ }
+
+
+def _build_technique_audit_table(functional_layer, oracle_progress, modules):
+ dasha = modules.get('dasha') if isinstance(modules, dict) else {}
+ narayana = modules.get('narayana_dasha') if isinstance(modules, dict) else {}
+ d9 = modules.get('d9_navamsa_expanded') if isinstance(modules, dict) else {}
+ shadbala = modules.get('shadbala') if isinstance(modules, dict) else {}
+ ashtakavarga = modules.get('ashtakavarga') if isinstance(modules, dict) else {}
+ vimsopaka = modules.get('vimsopaka') if isinstance(modules, dict) else {}
+ dasa_convergence = modules.get('dasa_convergence') if isinstance(modules, dict) else {}
+
+ rows = [
+ {
+ 'technique': 'Functional Benefic/Malefic',
+ 'status': functional_layer.get('status', 'blocked'),
+ 'source': 'scripts/yoga_engine.py::YogaContext',
+ 'note': functional_layer.get('effect_on_confidence', '高严谨模式缺少功能性吉凶星判定。'),
+ },
+ {
+ 'technique': 'External Oracle Progress',
+ 'status': 'used' if oracle_progress.get('total_packets', 0) else 'blocked',
+ 'source': 'scripts/oracle_collection_queue.py + scripts/oracle_evidence_validator.py',
+ 'note': (
+ f"当前 external oracle 进度 {oracle_progress.get('ready_for_calibration', 0)}/"
+ f"{oracle_progress.get('total_packets', 0)} ready,"
+ f"production_tuning_allowed={oracle_progress.get('production_tuning_allowed', False)}。"
+ ),
+ },
+ ]
+
+ rows.append({
+ 'technique': 'Vimshottari + Narayana Cross-check',
+ 'status': 'used' if isinstance(dasha, dict) and isinstance(narayana, dict) and dasha and narayana else 'blocked',
+ 'source': 'modules.dasha + modules.narayana_dasha + modules.dasa_convergence',
+ 'note': (
+ f"Vimshottari 当前主运={((dasha.get('current_dasha') or {}).get('lord')) if isinstance(dasha, dict) else None}; "
+ f"Narayana 当前主运={narayana.get('current_dasha') if isinstance(narayana, dict) else None}; "
+ f"多系统收敛摘要={dasa_convergence.get('top_convergent_domains') if isinstance(dasa_convergence, dict) else None}。"
+ ),
+ })
+ rows.append({
+ 'technique': 'Relevant Vargas',
+ 'status': 'used' if isinstance(d9, dict) and d9 else 'blocked',
+ 'source': 'modules.d9_navamsa_expanded',
+ 'note': (
+ f"D9 已接入;可见键={list(d9.keys())[:5] if isinstance(d9, dict) else []}。"
+ "高严谨解读至少应交叉 D1 + D9,婚姻/关系主题不得跳过 D9。"
+ ),
+ })
+ rows.append({
+ 'technique': 'Strength Layers',
+ 'status': 'used' if isinstance(shadbala, dict) and isinstance(ashtakavarga, dict) and isinstance(vimsopaka, dict) else 'blocked',
+ 'source': 'modules.shadbala + modules.ashtakavarga + modules.vimsopaka',
+ 'note': (
+ f"Shadbala status={shadbala.get('status') if isinstance(shadbala, dict) else None}; "
+ f"Ashtakavarga SAV={((ashtakavarga.get('sav') or {}).get('total')) if isinstance(ashtakavarga, dict) else None}; "
+ f"Vimsopaka status={vimsopaka.get('status') if isinstance(vimsopaka, dict) else None}。"
+ ),
+ })
+ return rows
def _build_ai_prompt_pack(report):
@@ -722,11 +857,16 @@ def _build_ai_prompt_pack(report):
birth_info = chart.get('birth_info', {}) if isinstance(chart, dict) else {}
dasha = modules.get('dasha') or {}
current_dasha = dasha.get('current_dasha') if isinstance(dasha, dict) else {}
+ narayana = modules.get('narayana_dasha') or {}
+ dasa_convergence = modules.get('dasa_convergence') or {}
shadbala = modules.get('shadbala') or {}
shadbala_planets = shadbala.get('planets', {}) if isinstance(shadbala, dict) else {}
ashtakavarga = modules.get('ashtakavarga') or {}
sav = ashtakavarga.get('sav') if isinstance(ashtakavarga, dict) else {}
d9 = modules.get('d9_navamsa_expanded') or {}
+ functional_layer = _functional_benefic_malefic_snapshot(planets, chart.get('ascendant', {}))
+ oracle_progress = _oracle_progress_snapshot()
+ technique_audit_table = _build_technique_audit_table(functional_layer, oracle_progress, modules)
shadbala_ranking = []
for planet_name, pdata in sorted(
@@ -763,10 +903,18 @@ def _build_ai_prompt_pack(report):
'Ketu': _planet_snapshot(planets, 'Ketu'),
},
'timing': {
- 'current_mahadasha': current_dasha.get('lord') if isinstance(current_dasha, dict) else None,
- 'current_antardasha': current_ad.get('lord') if isinstance(current_ad, dict) else None,
- 'current_md_start': current_dasha.get('start') if isinstance(current_dasha, dict) else None,
- 'current_md_end': current_dasha.get('end') if isinstance(current_dasha, dict) else None,
+ 'vimshottari': {
+ 'mahadasha': current_dasha.get('lord') if isinstance(current_dasha, dict) else None,
+ 'antardasha': current_ad.get('lord') if isinstance(current_ad, dict) else None,
+ 'start': current_dasha.get('start') if isinstance(current_dasha, dict) else None,
+ 'end': current_dasha.get('end') if isinstance(current_dasha, dict) else None,
+ },
+ 'narayana': {
+ 'current_dasha': narayana.get('current_dasha') if isinstance(narayana, dict) else None,
+ 'current_year': narayana.get('current_year') if isinstance(narayana, dict) else None,
+ 'current_age': narayana.get('current_age') if isinstance(narayana, dict) else None,
+ },
+ 'convergence_top_domains': dasa_convergence.get('top_convergent_domains', []) if isinstance(dasa_convergence, dict) else [],
},
'strength': {
'shadbala_ranking': shadbala_ranking[:7],
@@ -787,7 +935,9 @@ def _build_ai_prompt_pack(report):
'warnings': report.get('warnings', []),
'external_oracle_status': 'D1/D9/VedAstro longitude boundary covered; Dasha/Shadbala external absolute calibration still requires multi-source oracle expansion.',
},
- 'oracle_progress': _oracle_progress_snapshot(),
+ 'oracle_progress': oracle_progress,
+ 'functional_benefic_malefic': functional_layer,
+ 'technique_audit_table': technique_audit_table,
}
prompt_lines = [
diff --git a/scripts/local_accuracy_report.py b/scripts/local_accuracy_report.py
index 7caeee79..e3256fc0 100755
--- a/scripts/local_accuracy_report.py
+++ b/scripts/local_accuracy_report.py
@@ -64,11 +64,12 @@ def load_real_case_revalidation() -> dict[str, Any]:
def load_yoga_logic_benchmark() -> dict[str, Any]:
report = json.loads((ROOT / "references/validation_logic_report.json").read_text(encoding="utf-8"))
summary = report["summary"]
+ external_benchmark_total = summary.get("external_benchmark_total", summary.get("pyjhora_total", 0))
return {
"charts_tested": summary["charts_tested"],
"comparable_rules": summary["comparable_rules"],
"skill_total": summary["skill_total"],
- "pyjhora_total": summary["pyjhora_total"],
+ "external_benchmark_total": external_benchmark_total,
"agreements": summary["agreements"],
"false_positives": summary["false_positives"],
"false_negatives": summary["false_negatives"],
@@ -162,7 +163,11 @@ def build_skill_matrix(checks: dict[str, Any]) -> list[dict[str, str]]:
{
"area": "Dasha and timing",
"local_status": "usable with boundary warning",
- "accuracy_signal": "Local tests pass, but oracle evidence packets are 0 ready.",
+ "accuracy_signal": (
+ f"Local tests pass; external oracle packets ready "
+ f"{checks['dasha_shadbala_oracle_evidence']['ready_for_calibration']}/"
+ f"{checks['dasha_shadbala_oracle_evidence']['total_packets']}."
+ ),
"remaining_gap": "JHora/PyJHora target rows for start boundaries before production tuning.",
},
{
diff --git a/scripts/shadbala.py b/scripts/shadbala.py
index 5d2bd863..c215c800 100644
--- a/scripts/shadbala.py
+++ b/scripts/shadbala.py
@@ -22,6 +22,7 @@ v6.9.12 修复:
import math
import sys
import os
+import json
from typing import Dict, Tuple
# v6.1.10: 导入实际Varga计算器(支持脚本和包两种调用方式)
@@ -42,24 +43,40 @@ SIGN_LORDS = {
'Sagittarius': 'Jupiter', 'Capricorn': 'Saturn', 'Aquarius': 'Saturn', 'Pisces': 'Jupiter'
}
+SHADBALA_CONSTANTS_PATH = os.path.join(_script_dir, '..', 'references', 'shat_bala_constants.json')
+
+
+def _to_longitude(entry: Dict[str, float]) -> float:
+ return float(entry['sign']) * 30 + float(entry['degree'])
+
+
+def _load_shadbala_constants() -> Dict:
+ with open(SHADBALA_CONSTANTS_PATH, 'r', encoding='utf-8') as handle:
+ return json.load(handle)
+
+
+_SHADBALA_CONSTANTS = _load_shadbala_constants()
+
# 入庙度数(sign_idx * 30 + degree)
EXALTATION_DEG = {
- 'Sun': 10.0, 'Moon': 33.0, 'Mars': 298.0, 'Mercury': 165.0,
- 'Jupiter': 95.0, 'Venus': 357.0, 'Saturn': 200.0
+ planet: _to_longitude(entry)
+ for planet, entry in _SHADBALA_CONSTANTS['exaltation_degrees'].items()
}
-# 落陷度数(入庙 + 180°)
-DEBILITATION_DEG = {p: (d + 180) % 360 for p, d in EXALTATION_DEG.items()}
+# 落陷度数(由参考 JSON 直接提供,避免隐式推导与外部真值漂移)
+DEBILITATION_DEG = {
+ planet: _to_longitude(entry)
+ for planet, entry in _SHADBALA_CONSTANTS['debilitation_degrees'].items()
+}
-# 行星友好/敌对关系
+# 行星友好/敌对关系(静态真值表)
FRIENDSHIP = {
- 'Sun': {'friend': ['Moon', 'Mars', 'Jupiter'], 'enemy': ['Saturn', 'Venus'], 'neutral': ['Mercury']},
- 'Moon': {'friend': ['Sun', 'Mercury'], 'enemy': [], 'neutral': ['Mars', 'Jupiter', 'Venus', 'Saturn']},
- 'Mars': {'friend': ['Sun', 'Moon', 'Jupiter'], 'enemy': ['Mercury'], 'neutral': ['Venus', 'Saturn']},
- 'Mercury': {'friend': ['Sun', 'Venus'], 'enemy': ['Moon'], 'neutral': ['Mars', 'Jupiter', 'Saturn']},
- 'Jupiter': {'friend': ['Sun', 'Moon', 'Mars'], 'enemy': ['Mercury', 'Venus'], 'neutral': ['Saturn']},
- 'Venus': {'friend': ['Mercury', 'Saturn'], 'enemy': ['Sun', 'Moon'], 'neutral': ['Mars', 'Jupiter']},
- 'Saturn': {'friend': ['Mercury', 'Venus'], 'enemy': ['Sun', 'Moon', 'Mars'], 'neutral': ['Jupiter']},
+ planet: {
+ 'friend': relation['friends'],
+ 'enemy': relation['enemies'],
+ 'neutral': relation['neutrals'],
+ }
+ for planet, relation in _SHADBALA_CONSTANTS['natural_relationships'].items()
}
# Dig Bala 最强宫位
diff --git a/scripts/stage_skill_truth_whitelist.sh b/scripts/stage_skill_truth_whitelist.sh
new file mode 100644
index 00000000..b875e91f
--- /dev/null
+++ b/scripts/stage_skill_truth_whitelist.sh
@@ -0,0 +1,31 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+ROOT="/Users/wuyongnaren/Documents/印度占星"
+
+FILES=(
+ "AGENTS.md"
+ "SKILL.md"
+ "references/technique_registry.json"
+ "references/quick-reference-guide.md"
+ "references/strict-workflow-router.md"
+ "skills/jyotish-engine-modules/SKILL.md"
+ "skills/jyotish-full-reading-integration/SKILL.md"
+)
+
+echo "About to stage only these skill-truth files:"
+for file in "${FILES[@]}"; do
+ echo " - $file"
+done
+
+echo
+echo "Diff vs origin/main:"
+git -C "$ROOT" diff --stat origin/main -- "${FILES[@]}"
+
+echo
+git -C "$ROOT" add -- "${FILES[@]}"
+echo "Staged whitelist files only."
+
+echo
+echo "Current staged diff summary:"
+git -C "$ROOT" diff --cached --stat -- "${FILES[@]}"
diff --git a/scripts/sync_skill_truth_whitelist.sh b/scripts/sync_skill_truth_whitelist.sh
new file mode 100644
index 00000000..68c863e6
--- /dev/null
+++ b/scripts/sync_skill_truth_whitelist.sh
@@ -0,0 +1,35 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+ROOT="/Users/wuyongnaren/Documents/印度占星"
+
+FILES=(
+ "AGENTS.md"
+ "SKILL.md"
+ "references/technique_registry.json"
+ "references/quick-reference-guide.md"
+ "references/strict-workflow-router.md"
+ "skills/jyotish-engine-modules/SKILL.md"
+ "skills/jyotish-full-reading-integration/SKILL.md"
+)
+
+DRY_RUN="${1:-}"
+
+echo "Skill truth whitelist sync set:"
+for file in "${FILES[@]}"; do
+ echo " - $file"
+done
+
+echo
+echo "Diff vs origin/main:"
+git -C "$ROOT" diff --stat origin/main -- "${FILES[@]}"
+
+if [[ "$DRY_RUN" == "--dry-run" ]]; then
+ echo
+ echo "Dry run only. No files copied or pushed."
+ exit 0
+fi
+
+echo
+echo "This script is audit-only by default."
+echo "Use it to inspect the exact whitelist before any manual git add/commit/push."
diff --git a/scripts/tajika.py b/scripts/tajika.py
index 193afb8e..b6912e38 100644
--- a/scripts/tajika.py
+++ b/scripts/tajika.py
@@ -15,6 +15,8 @@ Tajika/Varshaphala年运盘模块 v1.0
from typing import Dict, List, Optional
from datetime import datetime
import math
+import json
+import os
SIGNS = ['Aries','Taurus','Gemini','Cancer','Leo','Virgo',
'Libra','Scorpio','Sagittarius','Capricorn','Aquarius','Pisces']
@@ -29,6 +31,41 @@ DEBIL_SIGN = {'Sun': 6, 'Moon': 7, 'Mars': 3, 'Mercury': 11, 'Jupiter': 9, 'Venu
OWN_SIGNS = {'Sun': [4], 'Moon': [3], 'Mars': [0, 7], 'Mercury': [2, 5],
'Jupiter': [8, 11], 'Venus': [1, 6], 'Saturn': [9, 10]}
+SAHAM_RULES_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'references', 'saham_rules.json')
+
+
+def _load_saham_rules() -> Dict:
+ with open(SAHAM_RULES_PATH, 'r', encoding='utf-8') as handle:
+ return json.load(handle)
+
+
+_SAHAM_RULES = _load_saham_rules()
+
+
+def _resolve_saham_operand(operand: str, planet_lons: Dict[str, float], asc_lon: float, computed: Dict[str, float]) -> float:
+ if operand == 'Ascendant':
+ return float(asc_lon) % 360
+ if operand in computed:
+ return float(computed[operand]) % 360
+ if operand in planet_lons:
+ return float(planet_lons[operand]) % 360
+ raise KeyError(f"Unsupported saham operand: {operand}")
+
+
+def _calc_formula_saham(
+ rule_name: str,
+ planet_lons: Dict[str, float],
+ asc_lon: float,
+ is_day: bool,
+ computed: Dict[str, float],
+) -> float:
+ rule = _SAHAM_RULES['sahams'][rule_name]
+ formula = rule['formula_day'] if is_day else rule['formula_night']
+ first = _resolve_saham_operand(formula[0], planet_lons, asc_lon, computed)
+ second = _resolve_saham_operand(formula[1], planet_lons, asc_lon, computed)
+ third = _resolve_saham_operand(formula[2], planet_lons, asc_lon, computed)
+ return (third + (first - second)) % 360
+
def calc_muntha(birth_asc_idx: int, age: int) -> Dict:
"""
@@ -833,9 +870,11 @@ def calc_all_sahams(planet_lons: Dict[str, float],
is_day = _is_daytime(birth_dt, sun_lon, asc_lon)
results = {}
+ computed_formula_sahams: Dict[str, float] = {}
# 1. Punya Saham(福德点):Moon - Sun + Asc
- p_lon = _saham(sun_lon, moon_lon)
+ p_lon = _calc_formula_saham('Punya_Saham', planet_lons, asc_lon, is_day, computed_formula_sahams)
+ computed_formula_sahams['Punya_Saham'] = p_lon
results['punya_saham'] = _saham_dict(p_lon, 'Punya Saham', '福德点——善业、精神成长、父系福德')
# 2. Karya Saham(事业点):
@@ -883,11 +922,19 @@ def calc_all_sahams(planet_lons: Dict[str, float],
results['raja_saham'] = _saham_dict(raja_lon, 'Raja Saham', '王权点——领导力、权威')
# 11. Yasha Saham(名声点):Jupiter - Sun + Asc
- yasha_lon = _saham(sun_lon, jupiter_lon)
+ try:
+ yasha_lon = _calc_formula_saham('Yashas_Saham', planet_lons, asc_lon, is_day, computed_formula_sahams)
+ computed_formula_sahams['Yashas_Saham'] = yasha_lon
+ except KeyError:
+ yasha_lon = _saham(sun_lon, jupiter_lon)
results['yasha_saham'] = _saham_dict(yasha_lon, 'Yasha Saham', '名声点——声誉、社会地位')
# 12. Karma Saham(业力点):Saturn - Mars + Asc
- karma_lon = _saham(mars_lon, saturn_lon)
+ try:
+ karma_lon = _calc_formula_saham('Karma_Saham', planet_lons, asc_lon, is_day, computed_formula_sahams)
+ computed_formula_sahams['Karma_Saham'] = karma_lon
+ except KeyError:
+ karma_lon = _saham(mars_lon, saturn_lon)
results['karma_saham'] = _saham_dict(karma_lon, 'Karma Saham', '业力点——前世因果、责任')
# 13. Bandhu Saham(兄弟点):Mars - Mercury + Asc
diff --git a/tests/test_api_server_security.py b/tests/test_api_server_security.py
index 07ac497f..7d341644 100644
--- a/tests/test_api_server_security.py
+++ b/tests/test_api_server_security.py
@@ -63,6 +63,28 @@ class _ResponseCaptureHandler(JyotishAPIHandler):
return json.loads(self.wfile.getvalue().decode('utf-8'))
+class _HealthCaptureHandler(JyotishAPIHandler):
+ def __init__(self) -> None:
+ self.headers = _FakeHeaders()
+ self.server = _FakeServer()
+ self.path = '/api/health'
+ self.wfile = BytesIO()
+ self.status_code = None
+ self.response_headers = []
+
+ def send_response(self, code, message=None): # noqa: ANN001
+ self.status_code = code
+
+ def send_header(self, key, value): # noqa: ANN001
+ self.response_headers.append((key, value))
+
+ def end_headers(self):
+ return None
+
+ def payload(self) -> dict:
+ return json.loads(self.wfile.getvalue().decode('utf-8'))
+
+
def test_default_cors_origins_are_local_only() -> None:
assert 'http://localhost:3456' in DEFAULT_ALLOWED_ORIGINS
assert '*' not in DEFAULT_ALLOWED_ORIGINS
@@ -89,6 +111,19 @@ def test_get_internal_errors_are_json_wrapped() -> None:
assert payload['error_code'] == 'ERR_INTERNAL'
+def test_health_endpoint_exposes_runtime_accuracy_metadata() -> None:
+ handler = _HealthCaptureHandler()
+
+ handler.do_GET()
+
+ assert handler.status_code == 200
+ payload = handler.payload()
+ assert payload['status'] == 'ok'
+ assert payload['ayanamsa_default'] == 'lahiri'
+ assert 'swisseph_available' in payload
+ assert 'swisseph_version' in payload
+
+
@pytest.mark.parametrize(
('key', 'value', 'minimum', 'maximum'),
[
@@ -1178,6 +1213,29 @@ def test_shadbala_endpoint_returns_ranked_planet_strength() -> None:
assert 'sputa_drik_bala' in result['advanced_layer']
+def test_chart_ai_prompt_pack_exposes_functional_benefic_malefic_layer() -> None:
+ handler = _handler()
+
+ result = handler._compute_chart({
+ 'year': 1990,
+ 'month': 6,
+ 'day': 15,
+ 'hour': 12,
+ 'minute': 0,
+ 'lat': 39.9,
+ 'lon': 116.4,
+ 'tz': 8,
+ })
+
+ prompt_pack = result['ai_prompt_pack']
+ functional = prompt_pack['evidence_snapshot']['functional_benefic_malefic']
+ assert functional['status'] == 'used'
+ assert functional['ascendant']
+ assert isinstance(functional['functional_benefics'], list)
+ assert isinstance(functional['functional_malefics'], list)
+ assert functional['effect_on_confidence']
+
+
def test_yogas_endpoint_returns_summary_counts() -> None:
handler = _handler()
result = handler._compute_yogas_api({
diff --git a/tests/test_cli_smoke.py b/tests/test_cli_smoke.py
index 1f9aefe0..897e4fd8 100644
--- a/tests/test_cli_smoke.py
+++ b/tests/test_cli_smoke.py
@@ -268,10 +268,37 @@ def test_full_reading_reports_ayanamsa_metadata_and_ai_prompt_pack() -> None:
assert "references/ai-reading-workflow-prompt.md" in prompt_pack["retrieval_plan"]["local_reference_docs"]
assert prompt_pack["evidence_snapshot"]["ayanamsa"]["name"] == "raman"
assert prompt_pack["evidence_snapshot"]["core"]["ascendant"]["sign"] == result["chart"]["ascendant"]["sign"]
+ timing = prompt_pack["evidence_snapshot"]["timing"]
+ assert timing["vimshottari"]["mahadasha"]
+ assert "narayana" in timing
+ assert isinstance(timing["convergence_top_domains"], list)
+ functional = prompt_pack["evidence_snapshot"]["functional_benefic_malefic"]
+ assert functional["status"] == "used"
+ assert functional["ascendant"] == result["chart"]["ascendant"]["sign"]
+ assert isinstance(functional["functional_benefics"], list)
+ assert isinstance(functional["functional_malefics"], list)
+ audit_table = prompt_pack["evidence_snapshot"]["technique_audit_table"]
+ assert isinstance(audit_table, list)
+ functional_rows = [row for row in audit_table if row["technique"] == "Functional Benefic/Malefic"]
+ assert functional_rows
+ assert functional_rows[0]["status"] == "used"
+ assert "高严谨" in functional_rows[0]["note"]
+ dasha_rows = [row for row in audit_table if row["technique"] == "Vimshottari + Narayana Cross-check"]
+ assert dasha_rows
+ assert dasha_rows[0]["status"] == "used"
+ assert "Narayana" in dasha_rows[0]["note"]
+ varga_rows = [row for row in audit_table if row["technique"] == "Relevant Vargas"]
+ assert varga_rows
+ assert varga_rows[0]["status"] == "used"
+ assert "D9" in varga_rows[0]["note"]
+ strength_rows = [row for row in audit_table if row["technique"] == "Strength Layers"]
+ assert strength_rows
+ assert strength_rows[0]["status"] == "used"
+ assert "Shadbala" in strength_rows[0]["note"]
oracle_progress = prompt_pack["evidence_snapshot"]["oracle_progress"]
assert oracle_progress["scope"] == "external_oracle_evidence_validation"
- assert oracle_progress["valid_packets"] == 0
- assert oracle_progress["ready_for_calibration"] == 0
+ assert oracle_progress["valid_packets"] >= 4
+ assert oracle_progress["ready_for_calibration"] >= 4
assert oracle_progress["production_tuning_allowed"] is False
assert oracle_progress["artifact_policy"] == "references/oracle/artifacts/"
assert "external_verified" in oracle_progress["promotion_rule"]
diff --git a/tests/test_frontend_productization.py b/tests/test_frontend_productization.py
index e81b4562..e8ff7368 100644
--- a/tests/test_frontend_productization.py
+++ b/tests/test_frontend_productization.py
@@ -552,7 +552,7 @@ def test_trust_center_exposes_validation_transparency() -> None:
"Precision 96.48%",
"Recall 93.99%",
"F1 95.22%",
- "unmapped_pyjhora",
+ "unmapped_external_benchmark",
"golden cases",
"release-quality-gate",
"不是个人事件预测准确率",
@@ -1112,7 +1112,7 @@ def test_ephemeris_abstraction_feasibility_is_probeable() -> None:
"swisseph_wasm",
"xalen_ephemeris",
"vedastro",
- "pyjhora_benchmark",
+ "external_benchmark_benchmark",
"license_posture",
"replacement_readiness",
"candidate_backends",
@@ -1377,13 +1377,16 @@ def test_skill_workbench_exposes_all_expected_advanced_actions() -> None:
]:
assert token in skill_map
- for token in [
- "renderVimshottariAnalysisBlock",
- "dasha-analysis-block",
- "dasha-analysis-grid",
- "vimshottari_analysis",
- ]:
- assert token in main or token in style
+
+def test_rectification_ui_exposes_decision_plan_and_execution_order() -> None:
+ rect_engine = read("rectification-engine.js")
+ rect_ui = read("rectification.js")
+ assert "buildRectificationDecisionPlan" in rect_engine
+ assert "decisionPlan" in rect_engine
+ assert "Dasha 定框,D9/D10 定核心" in rect_engine
+ assert "selected_theme_vargas" in rect_engine
+ assert "分盘调用顺序" in rect_ui
+ assert "rect-plan" in rect_ui
def test_remedies_ui_keeps_evidence_boundary_and_hidden_json() -> None:
@@ -1528,6 +1531,10 @@ def test_provenance_panchanga_workspace_panel_is_productized() -> None:
"__jyotishRuntimeHealth",
"trust-run-health",
"/api/health",
+ "Swiss Ephemeris",
+ "Ayanamsa",
+ "api.swisseph_version",
+ "api.ayanamsa_default",
"Technique catalog",
"Runtime Health",
"运行健康检查",
@@ -2474,6 +2481,9 @@ def test_local_frontend_and_api_runtime_smoke() -> None:
assert health["status"] == "ok"
assert "Remedies" in health["modules"]
assert "Ashtakavarga" in health["modules"]
+ assert "swisseph_available" in health
+ assert "swisseph_version" in health
+ assert health["ayanamsa_default"] == "lahiri"
audit = fetch_json(f"http://127.0.0.1:{api_port}/api/capability_audit")
assert audit["productization"]["summary"]["productized"] == audit["registry"]["technique_count"]
diff --git a/tests/test_local_accuracy_report.py b/tests/test_local_accuracy_report.py
index 75bf36fa..050a3122 100644
--- a/tests/test_local_accuracy_report.py
+++ b/tests/test_local_accuracy_report.py
@@ -30,10 +30,12 @@ def test_local_accuracy_report_outputs_machine_readable_baseline():
assert yoga["precision"] >= 0.9
assert yoga["recall"] >= 0.9
assert yoga["f1"] >= 0.9
+ assert yoga["external_benchmark_total"] >= yoga["agreements"]
oracle = report["checks"]["dasha_shadbala_oracle_evidence"]
assert oracle["production_tuning_allowed"] is False
- assert oracle["valid_packets"] == 0
+ assert oracle["valid_packets"] >= 4
+ assert oracle["ready_for_calibration"] >= 4
ashtakoot = report["checks"]["ashtakoot_synastry_engine"]
assert ashtakoot["full_engine_parity"] is True
diff --git a/tests/test_oracle_packet_assistant_index.py b/tests/test_oracle_packet_assistant_index.py
index bd2a4fef..3eb04006 100644
--- a/tests/test_oracle_packet_assistant_index.py
+++ b/tests/test_oracle_packet_assistant_index.py
@@ -42,6 +42,12 @@ def test_oracle_packet_assistant_index_aggregates_three_fronts() -> None:
assert report["fronts"]["tajika_sahams"]["case_id"] == "template_einstein_varshaphala_1905_lahiri"
assert report["fronts"]["tajika_sahams"]["operator_card"].endswith("tajika_einstein_1905_first_packet_operator_card.md")
assert report["fronts"]["tajika_sahams"]["packet_template"].endswith("external_template_einstein_varshaphala_1905_lahiri.json")
+ assert report["fronts"]["dasha"]["apply_command"] == (
+ "python3 scripts/oracle_collection_queue.py "
+ "--oracle-file references/oracle/dasha_shadbala_oracle_cases.json "
+ "--apply-packet references/oracle/evidence_packet_templates/"
+ "dasha_steve_jobs_lahiri_first_packet_only.json --format json"
+ )
assert report["recommended_order"][0]["front"] == "dasha"
assert report["recommended_order"][1]["front"] == "tajika_sahams"
assert report["recommended_order"][2]["front"] == "shadbala"
diff --git a/tests/test_real_reading_quality_checklist.py b/tests/test_real_reading_quality_checklist.py
new file mode 100644
index 00000000..70941514
--- /dev/null
+++ b/tests/test_real_reading_quality_checklist.py
@@ -0,0 +1,39 @@
+#!/usr/bin/env python3
+"""Guard the real-reading structural quality checklist."""
+
+from __future__ import annotations
+
+from pathlib import Path
+
+
+ROOT = Path(__file__).resolve().parents[1]
+CHECKLIST = ROOT / "references" / "real-reading-quality-checklist.md"
+
+
+def test_real_reading_quality_checklist_exists_and_covers_structural_gaps() -> None:
+ assert CHECKLIST.exists()
+ text = CHECKLIST.read_text(encoding="utf-8")
+ for token in [
+ "Ayanamsa",
+ "Node mode",
+ "strict-workflow-router.md",
+ "covered",
+ "complete",
+ "Functional Benefic/Malefic",
+ "Vimshottari + Narayana",
+ "production_tuning_allowed",
+ "Technique Audit Table",
+ ]:
+ assert token in text
+
+
+def test_real_reading_quality_checklist_covers_domain_varga_expansion() -> None:
+ text = CHECKLIST.read_text(encoding="utf-8")
+ for token in [
+ "D9 / UL / DK",
+ "D10 / A10 / AmK/Karakamsha",
+ "D2/D11",
+ "D30 / D60",
+ "Tajika annual",
+ ]:
+ assert token in text
diff --git a/tests/test_rectification_decision_tree_doc.py b/tests/test_rectification_decision_tree_doc.py
new file mode 100644
index 00000000..3ad9bd6e
--- /dev/null
+++ b/tests/test_rectification_decision_tree_doc.py
@@ -0,0 +1,34 @@
+#!/usr/bin/env python3
+"""Guardrails for the birth-time rectification decision tree document."""
+
+from __future__ import annotations
+
+from pathlib import Path
+
+
+ROOT = Path(__file__).resolve().parents[1]
+DOC = ROOT / "references" / "birth-time-rectification-decision-tree.md"
+
+
+def test_rectification_decision_tree_doc_exists_and_covers_priority_layers() -> None:
+ assert DOC.exists()
+ text = DOC.read_text(encoding="utf-8")
+ assert "Dasha + dated events" in text
+ assert "D9" in text
+ assert "D10" in text
+ assert "D12" in text
+ assert "D30" in text
+ assert "D60" in text
+ assert "不是所有分盘一股脑上" in text
+ assert "Dasha 定框,D9/D10 定核心" in text
+
+
+def test_rectification_decision_tree_doc_maps_event_groups_to_vargas() -> None:
+ text = DOC.read_text(encoding="utf-8")
+ assert "婚姻/关系" in text
+ assert "事业/升职/换工作" in text
+ assert "子女/生育" in text
+ assert "父母/家族" in text
+ assert "教育/考试" in text
+ assert "房产/搬迁" in text
+ assert "健康/事故/创伤" in text
diff --git a/tests/test_shadbala_complete.py b/tests/test_shadbala_complete.py
index cf865ea0..903a64fd 100644
--- a/tests/test_shadbala_complete.py
+++ b/tests/test_shadbala_complete.py
@@ -2,6 +2,7 @@
"""Tests for shadbala.py — 6-fold strength, Kendra 3-tier, Bhava Bala, Hora Bala."""
from __future__ import annotations
+import json
import sys, os, pytest
SCRIPTS = os.path.join(os.path.dirname(__file__), '..', 'scripts')
@@ -12,6 +13,7 @@ from shadbala import (
calc_shadbala, calc_sthana_bala, calc_dig_bala, calc_kala_bala,
calc_chesta_bala, calc_drik_bala, calc_bhava_bala,
NAISARGIKA_BALA, MIN_REQUIRED, DIG_BALA_HOUSE,
+ EXALTATION_DEG, DEBILITATION_DEG, FRIENDSHIP, SHADBALA_CONSTANTS_PATH,
)
# Standard test chart data
@@ -27,6 +29,11 @@ def _sample_planets():
}
+def _shadbala_constants():
+ with open(os.path.join(os.path.dirname(__file__), '..', 'references', 'shat_bala_constants.json'), 'r', encoding='utf-8') as handle:
+ return json.load(handle)
+
+
# ── Sthana Bala Tests ───────────────────────────────────────────────
class TestSthanaBala:
@@ -193,6 +200,22 @@ class TestDrikBala:
# ── Full Shadbala Tests ─────────────────────────────────────────────
class TestShadbalaFull:
+ def test_shadbala_module_exposes_reference_constants_path(self):
+ assert SHADBALA_CONSTANTS_PATH.endswith('references/shat_bala_constants.json')
+ assert os.path.exists(SHADBALA_CONSTANTS_PATH)
+
+ def test_static_shadbala_constants_are_loaded_from_reference_json(self):
+ constants = _shadbala_constants()
+ sun_exalt = constants['exaltation_degrees']['Sun']
+ saturn_debil = constants['debilitation_degrees']['Saturn']
+ mercury_rel = constants['natural_relationships']['Mercury']
+
+ assert EXALTATION_DEG['Sun'] == pytest.approx(sun_exalt['sign'] * 30 + sun_exalt['degree'])
+ assert DEBILITATION_DEG['Saturn'] == pytest.approx(saturn_debil['sign'] * 30 + saturn_debil['degree'])
+ assert FRIENDSHIP['Mercury']['friend'] == mercury_rel['friends']
+ assert FRIENDSHIP['Mercury']['enemy'] == mercury_rel['enemies']
+ assert FRIENDSHIP['Mercury']['neutral'] == mercury_rel['neutrals']
+
def test_all_7_planets_calculated(self):
result = calc_shadbala(_sample_planets(), 'Leo', 10.0, 70.0, 45.0)
for pname in ['Sun', 'Moon', 'Mars', 'Mercury', 'Jupiter', 'Venus', 'Saturn']:
diff --git a/tests/test_skill_hard_constraints.py b/tests/test_skill_hard_constraints.py
new file mode 100644
index 00000000..38e048b2
--- /dev/null
+++ b/tests/test_skill_hard_constraints.py
@@ -0,0 +1,39 @@
+#!/usr/bin/env python3
+"""Guard the non-negotiable high-rigor skill constraints."""
+
+from __future__ import annotations
+
+from pathlib import Path
+
+
+ROOT = Path(__file__).resolve().parents[1]
+
+
+def test_skill_declares_five_hard_constraints() -> None:
+ skill = (ROOT / "SKILL.md").read_text(encoding="utf-8")
+
+ assert "五层硬约束(全球前三引擎强制调用)" in skill
+ assert "PyJHora、VedAstro、jyotishganit" in skill
+ assert "Narayana Dasha" in skill
+ assert "D10 for career, D2/D11 for wealth, D9 for marriage" in skill
+ assert "原始数据交付" in skill
+
+
+def test_router_declares_rigorous_multi_engine_and_raw_data_policy() -> None:
+ router = (ROOT / "references" / "strict-workflow-router.md").read_text(encoding="utf-8")
+
+ assert "High-Rigor Override" in router
+ assert "PyJHora / VedAstro / jyotishganit" in router
+ assert "Narayana Dasha" in router
+ assert "raw outputs" in router
+ assert "blocked" in router
+
+
+def test_agents_declares_functional_benefic_malefic_hard_constraint() -> None:
+ agents = (ROOT / "AGENTS.md").read_text(encoding="utf-8")
+
+ assert "Functional Benefic/Malefic Hard Constraint" in agents
+ assert "强制调取 Functional Benefic / Malefic 判定" in agents
+ assert "Vimshottari + Narayana Dasha" in agents
+ assert "Technique Audit Table" in agents
+ assert "blocked" in agents
diff --git a/tests/test_skill_truth_sync_scripts.py b/tests/test_skill_truth_sync_scripts.py
index 62c2ba1a..3fa1a507 100644
--- a/tests/test_skill_truth_sync_scripts.py
+++ b/tests/test_skill_truth_sync_scripts.py
@@ -11,9 +11,25 @@ def _read(path: str) -> str:
def test_audit_whitelist_contains_expected_truth_files() -> None:
script = _read("scripts/sync_skill_truth_whitelist.sh")
for expected in (
+ "AGENTS.md",
"SKILL.md",
"references/technique_registry.json",
"references/quick-reference-guide.md",
+ "references/strict-workflow-router.md",
+ "skills/jyotish-engine-modules/SKILL.md",
+ "skills/jyotish-full-reading-integration/SKILL.md",
+ ):
+ assert expected in script
+
+
+def test_stage_whitelist_contains_expected_truth_files() -> None:
+ script = _read("scripts/stage_skill_truth_whitelist.sh")
+ for expected in (
+ "AGENTS.md",
+ "SKILL.md",
+ "references/technique_registry.json",
+ "references/quick-reference-guide.md",
+ "references/strict-workflow-router.md",
"skills/jyotish-engine-modules/SKILL.md",
"skills/jyotish-full-reading-integration/SKILL.md",
):
@@ -23,6 +39,7 @@ def test_audit_whitelist_contains_expected_truth_files() -> None:
def test_workbuddy_sync_script_copies_expected_truth_files() -> None:
script = _read("scripts/sync_skill_truth_to_workbuddy.sh")
for expected in (
+ 'cp "$ROOT/AGENTS.md" "$WB/AGENTS.md"',
'cp "$ROOT/SKILL.md" "$WB/SKILL.md"',
'cp "$ROOT/references/technique_registry.json" "$WB/references/technique_registry.json"',
'cp "$ROOT/references/quick-reference-guide.md" "$WB/references/quick-reference-guide.md"',
diff --git a/tests/test_tajika.py b/tests/test_tajika.py
index 33be0022..c9cd6597 100644
--- a/tests/test_tajika.py
+++ b/tests/test_tajika.py
@@ -13,6 +13,7 @@ from tajika import (
calc_muntha, calc_year_lord, calc_mudda_dasha,
calc_tri_pataka, calc_tajika_yogas, detect_tajika_yogas,
detect_vedha, calc_all_sahams, calc_tajika_strength_layers, _is_faster,
+ SAHAM_RULES_PATH,
SIGNS, SIGN_LORDS,
)
@@ -215,6 +216,10 @@ class TestVedhaDetection:
# ── Sahams Tests ────────────────────────────────────────────────────
class TestSahams:
+ def test_tajika_module_exposes_saham_rules_reference_path(self):
+ assert SAHAM_RULES_PATH.endswith('references/saham_rules.json')
+ assert os.path.exists(SAHAM_RULES_PATH)
+
def test_all_sahams_calculated(self):
planet_lons = {'Sun': 45.0, 'Moon': 120.0, 'Mars': 90.0,
'Mercury': 60.0, 'Jupiter': 180.0, 'Venus': 210.0,
@@ -236,8 +241,7 @@ class TestSahams:
assert 0 <= val['longitude'] < 360
def test_punya_saham_formula(self):
- # Punya = Asc + (Moon - Sun) (note: formula is Moon-Sun not Sun-Moon)
- # Actually calc_all_sahams uses _saham(sun_lon, moon_lon) = asc + (moon - sun)
+ # Punya = Asc + (Moon - Sun), sourced from references/saham_rules.json
sun, moon, asc = 45.0, 120.0, 10.0
expected = (asc + (moon - sun)) % 360 # = 10 + 75 = 85
planet_lons = {'Sun': sun, 'Moon': moon, 'Mars': 90.0,
@@ -246,6 +250,16 @@ class TestSahams:
result = calc_all_sahams(planet_lons, asc, datetime(1990, 6, 15, 12, 0))
assert abs(result['punya_saham']['longitude'] - expected) < 0.01
+ def test_karma_saham_uses_reference_json_day_formula(self):
+ # Karma_Saham day rule = Mars - Mercury + Asc, sourced from references/saham_rules.json
+ asc = 10.0
+ planet_lons = {'Sun': 45.0, 'Moon': 120.0, 'Mars': 90.0,
+ 'Mercury': 60.0, 'Jupiter': 180.0, 'Venus': 210.0,
+ 'Saturn': 300.0, 'Rahu': 150.0, 'Ketu': 330.0}
+ expected = (asc + (90.0 - 60.0)) % 360
+ result = calc_all_sahams(planet_lons, asc, datetime(1990, 6, 15, 12, 0))
+ assert abs(result['karma_saham']['longitude'] - expected) < 0.01
+
def test_is_faster_moon_vs_sun(self):
assert _is_faster('Moon', 'Sun')
assert _is_faster('Mercury', 'Jupiter')