Harden skill rigor constraints and sync reading quality tools

This commit is contained in:
732642856
2026-06-27 18:52:10 +08:00
parent 7600986e34
commit b884b8e7ea
30 changed files with 1262 additions and 56 deletions
+54
View File
@@ -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 或出生精度
- 功能性吉凶星层未完成
- 双重大运或多系统结果发生实质冲突
禁止把内部一致性伪装成“已经全球顶级精度”。
+27 -1
View File
@@ -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-*` |
@@ -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` 增加题目域安全话术:
- 事业
- 婚恋
- 财富
- 年运
## 结论
当前项目在规范层已经明确反对“见凶星就吓人”,但执行层仍有自然凶星被过度负面化的真实风险。
下一步应该把这一层冻结为模板,而不是继续靠人工记忆维持。
+60
View File
@@ -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,
};
}
+16
View File
@@ -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) {
<h4 class="sub-title">证据闭环</h4>
<div class="rect-audit-grid">${auditCards}</div>
</div>
<div class="rect-plan card">
<h4 class="sub-title">分盘调用顺序</h4>
<p class="rect-plan-principle">${escapeHtml(decisionPlan.principle || 'Dasha 定框,核心分盘先行,专项分盘后置。')}</p>
<ol class="rect-plan-list">${(decisionPlan.ordered_layers || []).map(layer => `
<li>
<div class="rect-plan-row">
<strong>${escapeHtml(layer.label)}</strong>
<span>${escapeHtml(layer.role)}</span>
</div>
<p>${escapeHtml(layer.reason)}</p>
</li>
`).join('')}</ol>
${(decisionPlan.selected_theme_vargas || []).length ? `<p class="rect-plan-focus">本轮优先专项分盘:${escapeHtml(decisionPlan.selected_theme_vargas.join(' / '))}</p>` : ''}
${(decisionPlan.warnings || []).length ? `<div class="rect-audit-warnings">${decisionPlan.warnings.map(w => `<p>${escapeHtml(w)}</p>`).join('')}</div>` : ''}
</div>
<div class="rect-scoring-detail card">
<h4 class="sub-title">${t('rect.scoring.title')}</h4>
${scoreBars}
+40
View File
@@ -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; }
@@ -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 最后谨慎使用”。**
@@ -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",
+2 -1
View File
@@ -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**:数据桥接映射
@@ -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 说成“已经无疏漏封顶”。
+17
View File
@@ -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
+26 -2
View File
@@ -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"])
+60 -5
View File
@@ -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
+166 -16
View File
@@ -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 = [
+7 -2
View File
@@ -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.",
},
{
+29 -12
View File
@@ -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 最强宫位
+31
View File
@@ -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[@]}"
+35
View File
@@ -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."
+50 -3
View File
@@ -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
+58
View File
@@ -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({
+29 -2
View File
@@ -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"]
+19 -9
View File
@@ -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"]
+3 -1
View File
@@ -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
@@ -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"
@@ -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
@@ -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
+23
View File
@@ -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']:
+39
View File
@@ -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
+17
View File
@@ -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"',
+16 -2
View File
@@ -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')