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 => ` +
  1. +
    + ${escapeHtml(layer.label)} + ${escapeHtml(layer.role)} +
    +

    ${escapeHtml(layer.reason)}

    +
  2. + `).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')