From 04d901e1be34812d2b7bb124fb9316a2fec3d135 Mon Sep 17 00:00:00 2001
From: 732642856 <732642856@qq.com>
Date: Sat, 27 Jun 2026 19:09:31 +0800
Subject: [PATCH] Clarify benchmark boundaries and improve annual workflow
surfaces
---
jyotish-app/main.js | 2 +
jyotish-app/renderers.js | 107 ++++++++---------
references/kp-astrology-complete-system.md | 50 ++++++++
references/pancha-pakshi-nakshatra-systems.md | 50 ++++++++
references/prashna-complete-guide.md | 71 ++++++++++++
references/tajika-yoga-complete-guide.md | 55 ++++++++-
references/varshaphala-annual-chart-guide.md | 54 +++++++++
scripts/_compute_one_chart.py | 8 +-
scripts/benchmark_yoga_coverage.py | 24 ++--
scripts/build_standard_test_charts.py | 6 +-
scripts/sync_skill_truth_to_workbuddy.sh | 1 +
scripts/validate_logic_v2.py | 6 +-
scripts/validate_yoga_accuracy.py | 108 +++++++++---------
scripts/yoga_engine.py | 12 +-
skills/jyotish-engine-modules/SKILL.md | 35 +++++-
.../jyotish-full-reading-integration/SKILL.md | 31 ++++-
tests/test_dasha.py | 39 +++++--
tests/test_tajika_annual_closure_status.py | 3 +
18 files changed, 506 insertions(+), 156 deletions(-)
diff --git a/jyotish-app/main.js b/jyotish-app/main.js
index 87eab203..09c076a7 100644
--- a/jyotish-app/main.js
+++ b/jyotish-app/main.js
@@ -2401,6 +2401,8 @@ async function runTrustCenterHealthCheck() {
details: [
['PWA 安装壳', getPWAStatus().label, getPWAStatus().note],
['本地 API 服务', api.base || 'online', `health ok · v${api.version || '-'} · ${api.latencyMs}ms`],
+ ['Swiss Ephemeris', api.swisseph_available ? (api.swisseph_version || 'available') : 'unavailable', '当前本地 API 的星历运行时状态'],
+ ['Ayanamsa', api.ayanamsa_default || 'lahiri', '当前健康检查声明的默认 sidereal 基准'],
['Technique catalog', `${registry.technique_count || 0} techniques`, `${surfaces.api_endpoint_count || 0} API endpoints 可被前端发现`],
['Desktop path', 'PWA now', 'Pake shell 可快速包 URL;Tauri sidecar 等 API 生命周期和签名策略确定后再落地。'],
],
diff --git a/jyotish-app/renderers.js b/jyotish-app/renderers.js
index 4c65f02a..1943aafb 100644
--- a/jyotish-app/renderers.js
+++ b/jyotish-app/renderers.js
@@ -86,16 +86,17 @@ export function renderVargas(allV, planets, ascendant) {
const selector = $('varga-selector');
const order = ['Sun','Moon','Mars','Mercury','Jupiter','Venus','Saturn','Rahu','Ketu'];
// 渲染选择器
- selector.innerHTML = VARGA_DEFS.map(v =>
- ``
- ).join('');
- selector.querySelectorAll('.varga-btn').forEach(btn => {
- btn.addEventListener('click', () => {
- selector.querySelectorAll('.varga-btn').forEach(b => b.classList.remove('active'));
- btn.classList.add('active');
- renderSingleVarga(btn.dataset.varga, planets, ascendant);
+ selector.innerHTML = `
+
+ `;
+ const selectEl = document.getElementById('vargaSelect');
+ if (selectEl) {
+ selectEl.addEventListener('change', (e) => {
+ renderSingleVarga(e.target.value, planets, ascendant);
});
- });
+ }
renderSingleVarga('D9', planets, ascendant);
}
@@ -233,9 +234,11 @@ export function renderShadbala(sb) {
// ========== Dasha 三级 ==========
export function renderDasha3Level(data) {
- const container = $('dasha-timeline');
- const currentEl = $('dasha-current');
+ const container = document.getElementById('dasha-timeline');
+ const currentEl = document.getElementById('dasha-current');
container.innerHTML = '';
+ document.getElementById('antardasha-panel').innerHTML = '';
+ document.getElementById('pratyantardasha-panel').innerHTML = '';
if (data.current_dasha) {
const cd = data.current_dasha;
@@ -252,53 +255,43 @@ export function renderDasha3Level(data) {
`;
}
- for (const d of data.timeline) {
- const isCur = data.current_dasha && data.current_dasha.lord === d.lord;
- const bar = document.createElement('div');
- bar.className = `dasha-bar${isCur ? ' active' : ''}`;
- bar.innerHTML = `
${escapeHtml(planetName(d.lord))}
${escapeHtml(yearsLabel(d.years))}
${escapeHtml(String(d.start || '').slice(2))}
`;
- bar.addEventListener('click', () => {
- container.querySelectorAll('.dasha-bar').forEach(b => b.classList.remove('selected'));
- bar.classList.add('selected');
- renderAntardashaPanel(d.antardasha || []);
- });
- container.appendChild(bar);
- }
-
- if (data.current_dasha?.antardasha) {
- renderAntardashaPanel(data.current_dasha.antardasha);
- }
-}
-
-function renderAntardashaPanel(subs) {
- const panel = $('antardasha-panel');
- panel.innerHTML = '';
- for (const ad of subs) {
- const div = document.createElement('div');
- div.className = `antardasha-item${ad.is_current ? ' current' : ''}`;
- div.innerHTML = `${escapeHtml(planetName(ad.lord))}${ad.is_current ? ' ◀' : ''}
${escapeHtml(String(ad.start || '').slice(5))}~${escapeHtml(String(ad.end || '').slice(5))}
`;
- div.addEventListener('click', () => {
- panel.querySelectorAll('.antardasha-item').forEach(i => i.classList.remove('selected'));
- div.classList.add('selected');
- renderPratyantardasha(ad.pratyantardasha || []);
- });
- panel.appendChild(div);
- }
- // 自动展开当前次运的三运
- const cur = subs.find(s => s.is_current);
- if (cur?.pratyantardasha) renderPratyantardasha(cur.pratyantardasha);
-}
-
-function renderPratyantardasha(pratyList) {
- const panel = $('pratyantardasha-panel');
- panel.innerHTML = `${t('dasha.praty')}
`;
- const grid = panel.querySelector('.praty-grid');
- for (const p of pratyList) {
- grid.innerHTML += `
-
${escapeHtml(planetName(p.lord))}${p.is_current ? ' ◀' : ''}
-
${escapeHtml(String(p.start || '').slice(5))}~${escapeHtml(String(p.end || '').slice(5))}
-
`;
+ let html = '';
+ for (const md of data.timeline) {
+ const isCurMd = data.current_dasha && data.current_dasha.lord === md.lord;
+ html += `
`;
+ html += ``;
+ html += `${escapeHtml(planetName(md.lord))} (${escapeHtml(yearsLabel(md.years))}) : ${escapeHtml(String(md.start || '').slice(0, 10))} ~ ${escapeHtml(String(md.end || '').slice(0, 10))}`;
+ html += `
`;
+
+ if (md.antardasha && md.antardasha.length > 0) {
+ html += ``;
+ for (const ad of md.antardasha) {
+ const isCurAd = isCurMd && ad.is_current;
+ html += `
`;
+ html += ``;
+ html += `${escapeHtml(planetName(ad.lord))} : ${escapeHtml(String(ad.start || '').slice(0, 10))} ~ ${escapeHtml(String(ad.end || '').slice(0, 10))}`;
+ html += `
`;
+
+ if (ad.pratyantardasha && ad.pratyantardasha.length > 0) {
+ html += ``;
+ for (const pd of ad.pratyantardasha) {
+ const isCurPd = isCurAd && pd.is_current;
+ html += `
`;
+ html += `↳ ${escapeHtml(planetName(pd.lord))} : ${escapeHtml(String(pd.start || '').slice(0, 10))} ~ ${escapeHtml(String(pd.end || '').slice(0, 10))}`;
+ html += `
`;
+ }
+ html += `
`;
+ }
+
+ html += ` `;
+ }
+ html += `
`;
+ }
+
+ html += ` `;
}
+ html += '
';
+ container.innerHTML = html;
}
// ========== 行星表更新(含D9列) ==========
diff --git a/references/kp-astrology-complete-system.md b/references/kp-astrology-complete-system.md
index 6c4353de..7e093e84 100644
--- a/references/kp-astrology-complete-system.md
+++ b/references/kp-astrology-complete-system.md
@@ -212,6 +212,56 @@ KP占星术由K.S. Krishnamurti教授(1908-1972)创立,是对传统Parasha
4. **不考虑否定宫位**:只看有利信号不看否定信号→假阳性
5. **Ayanamsa用错**:必须用KP专属Ayanamsa,不是Lahiri
+## 九、在当前 skill 中如何执行
+
+> 这一节只回答一个问题:**KP 理论在当前 skill 里如何真正落地,而不是停留在概念层。**
+
+### 9.1 当前可直接调用的能力
+
+| 能力 | 当前入口 | 当前状态 |
+|------|---------|---------|
+| KP Sublord / SubSubLord | `kp` / `/api/kp` | 可直接使用 |
+| ABCD Significator | `kp` / `/api/kp` | 可直接使用 |
+| KP Prashna YES/NO | `prashna` / `/api/prashna` | 可直接使用 |
+| KP Horary evidence | `/api/prashna` | 可直接使用 |
+| Ruling planets | `/api/prashna -> kp_horary.ruling_planets` | 已有基础版,仍需继续补深 |
+
+### 9.2 建议执行顺序
+
+当用户问“会不会”“值不值得”“什么时候会落地”时,当前 skill 推荐按下面顺序执行:
+
+1. **先看出生盘承诺**
+ - relevant houses / lords / Dasha / Transit
+2. **再看 KP significator**
+ - `kp` 输出 ABCD significator
+3. **再看 Cuspal Sub-Lord**
+ - 问题主宫位的 sub-lord
+4. **最后用 KP Horary evidence 做仲裁**
+ - `ruling_planets`
+ - `cuspal_sub_lord`
+ - `house_significators`
+ - `judgement_matrix`
+
+### 9.3 当前不要夸大的地方
+
+当前 skill 的 KP 已经不是“只有表面按钮”,但也还没有达到“传统 KP 老师完整工作流”的终态。
+
+尤其要注意:
+
+1. `ruling planets` 已有基础结构,但 `day_lord` 等细化链条仍未完全厚化
+2. Horary workflow 已可运行,但不同问事类型的裁决链仍需更多外部案例收敛
+3. KP 结果适合做**精细仲裁层**,不应单独替代出生盘、本命 promise、Dasha 与现实信息
+
+### 9.4 当前 skill 中的推荐话术
+
+- 适合说:
+ - “KP 显示该问题有/无承诺倾向”
+ - “KP Horary 证据支持/削弱这个判断”
+ - “KP 更适合做 yes/no 与时机仲裁”
+- 不适合直接说:
+ - “KP 已完全等同传统专业软件级全部工作流”
+ - “单凭一次 Horary 就足够决定重大人生事项”
+
### 准确率基准
- 熟练KP占星师的事件预测准确率:**70-85%**
- 初学者准确率:**40-55%**
diff --git a/references/pancha-pakshi-nakshatra-systems.md b/references/pancha-pakshi-nakshatra-systems.md
index 7e1426c8..b6c5e232 100644
--- a/references/pancha-pakshi-nakshatra-systems.md
+++ b/references/pancha-pakshi-nakshatra-systems.md
@@ -235,4 +235,54 @@ Nakshatra 有三种不同的起始计数体系,各自对应不同的宇宙功
---
+## 第五部分:在当前 skill 中如何执行
+
+> 这一节只回答:**Pancha Pakshi / Savya / Apasavya 在当前 skill 里到底怎么使用。**
+
+### 22. 当前已具备的能力
+
+| 能力 | 当前落点 | 当前状态 |
+|------|---------|---------|
+| Pancha Pakshi 鸟分配 | `scripts/pancha_pakshi.py` | 可直接计算 |
+| 5x5 活动矩阵 | `scripts/pancha_pakshi.py` | 可直接使用 |
+| 星期 Yama 偏移 | `scripts/pancha_pakshi.py` | 可直接使用 |
+| 活动建议 / 吉凶时段 | `scripts/pancha_pakshi.py` | 可直接使用 |
+| Savya / Apasavya 概念说明 | 本参考文件 | 有知识层 |
+
+### 23. 当前推荐角色
+
+在当前 skill 里,Pancha Pakshi 最适合的角色是:
+
+1. **日内择时辅助层**
+2. **活动时段偏好层**
+3. **和 Panchanga / Muhurta / Transit 交叉的补充层**
+
+它不适合单独承担:
+
+- 婚姻/事业重大承诺
+- 长期命运判断
+- 替代 Dasha / Transit / 本命盘
+
+### 24. 当前真实边界
+
+当前 skill 中已经能输出基础鸟态、活动矩阵、推荐/避开时段。
+
+但必须保留的边界:
+
+1. Tamil 传统细则仍不完整
+2. 地域化口径仍未完全闭环
+3. 与真实案例的长期外部验证仍不足
+4. 适合做“择时辅助”,不适合被包装成独立终极判断器
+
+### 25. 当前 skill 的推荐话术
+
+- 适合说:
+ - “Pancha Pakshi 显示这个时段更适合/不适合某类行动”
+ - “可把五鸟系统作为 Panchanga/Muhurta 的补充层”
+- 不适合直接说:
+ - “五鸟系统已经完成全部传统地区细则对齐”
+ - “单凭五鸟系统即可决定重大事项”
+
+---
+
*本文件整合自公众号文章「2印度占星」和「3印度占星」,涵盖 Pancha Pakshi 五鸟择时术、Nakshatra 三种计数体系(Ashwinādi/Krittikādi/Ardrādi)、Savya/Apasavya Nakshatra 分组。*
diff --git a/references/prashna-complete-guide.md b/references/prashna-complete-guide.md
index 06a11d8d..84c5d862 100644
--- a/references/prashna-complete-guide.md
+++ b/references/prashna-complete-guide.md
@@ -460,3 +460,74 @@ Arudha Sphuta = Arudha Rashi + Lagna的度数(仅度数)
| Tajika Yoga | `tajika-yoga-complete-guide.md` | Ithasala 等是/否判断 |
| Tajika 年运盘 | `scripts/tajika.py` | Muntha/YearLord 等 |
| Dasha 系统 | `alternative-dasha-systems.md` | 大运辅助验证 |
+
+## 附录 B:当前 skill 的 Prashna 落地流程
+
+> 这部分不是古典理论摘要,而是当前 skill 的实际使用方法。
+
+### B1. 当前已经具备的落地点
+
+当前 `prashna` 模块并不只是一个“问事按钮”,而是已经整合了:
+
+1. `KP Prashna answer`
+2. `KP answer v2`
+3. `KP Horary evidence`
+4. `Arudha`
+5. `Nadi Prashna`
+6. `Sphuta`
+7. `Sahams`
+8. `timing score`
+
+### B2. 当前推荐执行顺序
+
+对单一具体问题,建议按这个顺序读:
+
+1. **问题类型分类**
+ - career / relationship / finance / lost item / health / general
+2. **YES/NO 主结论**
+ - 先看 `kp_answer_v2`
+3. **Horary 证据层**
+ - `ruling_planets`
+ - `cuspal_sub_lord`
+ - `house_significators`
+ - `judgement_matrix`
+4. **镜像与心理层**
+ - `Arudha`
+ - `Nadi`
+5. **时机辅助层**
+ - `timing_score`
+ - `sphutas`
+ - `sahams`
+
+### B3. 当前真实边界
+
+当前 skill 中的 Prashna 已经能跑完整结果,但还不能夸张成“所有传统分支都完全封顶”。
+
+仍需保留的边界:
+
+1. 不同问事分支(事业、婚姻、失物、诉讼等)的传统裁决链成熟度不完全一样
+2. Horary evidence 已经有结构,但仍需更多真实外部案例来继续收紧命中率
+3. `Sphuta / Saham / timing_score` 属于辅助证据层,不应单独承担重大医学、法律、寿命结论
+
+### B4. 当前 skill 的安全使用原则
+
+- **适合:**
+ - 单一、当下、具体的问题
+ - yes/no 倾向
+ - 短期时机线索
+- **不适合:**
+ - 用一次问事替代完整本命盘
+ - 只凭 Prashna 决定重大人生事项
+ - 将死亡、重病、法律责任作绝对断言
+
+### B5. 与其他模块的正确关系
+
+当前 skill 中,Prashna 的最佳角色不是孤立系统,而是:
+
+1. 与出生盘 promise 交叉验证
+2. 与 Dasha / Transit 做时机交叉
+3. 与 KP significator / ruling planets 做细节仲裁
+
+换句话说:
+
+**Prashna 在当前 skill 里已经是强模块,但仍应作为“专项仲裁层”,而不是唯一裁判。**
diff --git a/references/tajika-yoga-complete-guide.md b/references/tajika-yoga-complete-guide.md
index 09b7df16..0f829937 100644
--- a/references/tajika-yoga-complete-guide.md
+++ b/references/tajika-yoga-complete-guide.md
@@ -277,6 +277,59 @@ Tajika Yoga系统源自阿拉伯/波斯占星传统,经印度占星吸收后
- [ ] 是否将每个Yoga映射到了具体宫位领域?
- [ ] 是否给出了综合年度事件预测?
+## 七、在当前 skill 中如何执行
+
+> 这一节只回答:**Tajika Yoga 在当前 skill 里如何真正落地使用。**
+
+### 7.1 当前已可直接调用的能力
+
+| 能力 | 当前入口 | 当前状态 |
+|------|---------|---------|
+| Tajika / annual report | `tajika` / `solar-return` / `/api/tajika` / `/api/annual` | 可直接使用 |
+| Muntha | 年运结果中直接输出 | 可直接使用 |
+| Varshesha / Year Lord | 年运结果中直接输出 | 可直接使用 |
+| Tajika Yogas | 年运结果中直接输出 | 可直接使用 |
+| Tajika strength layers | `Harsha Bala / Panchavargiya Bala` 摘要层 | 可直接使用 |
+| Sahams | 年运输出中已有 | 可直接使用,但解释层需保留边界 |
+
+### 7.2 当前推荐阅读顺序
+
+在当前 skill 中,读年度盘不要一上来就盯某个单一 Yoga。推荐顺序:
+
+1. **年度上升与 Muntha**
+ - 先看今年主题落在哪一类宫位
+2. **Varshesha / Year Lord**
+ - 判断全年主导行星与主轴
+3. **Tajika Yogas**
+ - 看事件倾向是推进、分离、阻隔还是中间人促成
+4. **Harsha Bala / Panchavargiya Bala**
+ - 判断哪些行星今年更有执行力,哪些更弱
+5. **Sahams**
+ - 作为辅助证据,不作为唯一裁决
+6. **最后再与 Dasha / Transit 对表**
+ - 年运盘不能单独替代本命盘与大运
+
+### 7.3 当前 skill 的真实边界
+
+当前 skill 里的 Tajika / Varshaphala 已经不是“只有框架”,但也还没有达到“年度事件裁决层完全封顶”的状态。
+
+必须保留的边界:
+
+1. `Tajika Yoga` 已能输出,但年度事件裁决深度仍需继续收敛
+2. `Harsha Bala / Panchavargiya Bala` 已可做年度强弱排序,但不是独立的最终预测器
+3. `Sahams` 已扩展到常见 36 点,但解释层与权重口径仍需更多外部闭环
+4. 年运盘适合给“年度主题、年度触发方向、月份侧重”,不适合单独承担绝对事件承诺
+
+### 7.4 当前 skill 的推荐话术
+
+- 适合说:
+ - “年运盘显示今年的主轴在……”
+ - “Tajika Yoga 支持某类事件更容易推进/分离/被阻隔”
+ - “Saham 只作为辅助定位,不单独下最终结论”
+- 不适合直接说:
+ - “单看年运盘就能精确锁死全年所有重大事件”
+ - “Saham 已完成传统软件级最终校准”
+
---
## 六、Orb速查表
@@ -296,4 +349,4 @@ Tajika Yoga系统源自阿拉伯/波斯占星传统,经印度占星吸收后
---
**版本**:v2.0.0
-**最后更新**:2026-04-23
\ No newline at end of file
+**最后更新**:2026-04-23
diff --git a/references/varshaphala-annual-chart-guide.md b/references/varshaphala-annual-chart-guide.md
index 2a47d737..641ab222 100644
--- a/references/varshaphala-annual-chart-guide.md
+++ b/references/varshaphala-annual-chart-guide.md
@@ -258,6 +258,60 @@ Ketu:约21天(7/120年)
- 太阳回归时间:[精确时间]
- 太阳回归地点:[城市]
- 年度上升(Varsha Lagna):[星座]
+
+## 八、当前 skill 的 Varshaphala 落地流程
+
+> 这部分不是传统教材,而是当前 skill 的实际执行说明。
+
+### 8.1 当前已经有的结构
+
+在当前 skill 中,`Varshaphala` 已经整合出以下结果层:
+
+1. `solar_return`
+2. `muntha`
+3. `varshesha`
+4. `tajika_yogas`
+5. `tajika_strength`
+6. `sahams`
+7. `predictions`
+
+### 8.2 当前推荐执行顺序
+
+1. **先看 Solar Return / 年度上升**
+ - 判断这一年是偏事业、关系、财务还是转化
+2. **再看 Muntha**
+ - 判断今年主题落在哪个宫位领域
+3. **再看 Varshesha**
+ - 判断哪颗星是年度总控
+4. **再看 Tajika Yogas**
+ - 判断推进、分离、阻碍、间接成事
+5. **再看 tajika_strength**
+ - 判断哪些星今年更有支配力
+6. **最后看 Sahams 与 predictions**
+ - 只作为辅助定位层
+
+### 8.3 当前真实边界
+
+当前 skill 的年运盘已经具备主结构,但还没有达到“单独作为全年精确事件承诺器”的程度。
+
+必须保留的边界:
+
+1. 年度主轴判断已经可用
+2. 月度/事件裁决层仍需要和 Dasha / Transit 做交叉
+3. Saham 权重和解释层仍需继续外部闭环
+4. 年运盘最适合做“主题、机会方向、风险区域”的整理,不宜单独替代本命 promise
+
+### 8.4 当前 skill 中的最佳角色
+
+Varshaphala 在当前 skill 里的最佳角色是:
+
+1. **年度主题层**
+2. **年度节奏层**
+3. **与 Dasha / Transit 的交叉确认层**
+
+换句话说:
+
+**它已经是强辅助决策层,但还不是可以脱离本命盘和大运独立封顶的最终裁判。**
- Muntha:[X宫] [星座]
- 年主星:[行星] 在 [X宫] [星座]([庙旺/落陷])
- 当前年龄:[X]岁
diff --git a/scripts/_compute_one_chart.py b/scripts/_compute_one_chart.py
index d1ac4a06..1efd2415 100644
--- a/scripts/_compute_one_chart.py
+++ b/scripts/_compute_one_chart.py
@@ -41,7 +41,7 @@ def tz_to_float(tz_str):
minutes = int(parts[1]) if len(parts) > 1 else 0
return sign * (hours + minutes / 60.0)
-def _planet_dict_from_pyjhora_positions(positions, asc_sign):
+def _planet_dict_from_external_benchmark_positions(positions, asc_sign):
"""Convert PyJHora planet positions to the skill validation schema."""
names = {
0: "Sun",
@@ -123,17 +123,17 @@ def compute_yogas(chart):
"d1": {
"ascendant": SIGNS[d1_asc_sign],
"ascendant_degree": d1_asc_degree,
- "planets": _planet_dict_from_pyjhora_positions(d1_positions, d1_asc_sign),
+ "planets": _planet_dict_from_external_benchmark_positions(d1_positions, d1_asc_sign),
},
"d9": {
"ascendant": SIGNS[d9_asc_sign],
"ascendant_degree": d9_asc_degree,
- "planets": _planet_dict_from_pyjhora_positions(d9_positions, d9_asc_sign),
+ "planets": _planet_dict_from_external_benchmark_positions(d9_positions, d9_asc_sign),
},
"d60": {
"ascendant": SIGNS[d60_asc_sign],
"ascendant_degree": d60_asc_degree,
- "planets": _planet_dict_from_pyjhora_positions(d60_positions, d60_asc_sign),
+ "planets": _planet_dict_from_external_benchmark_positions(d60_positions, d60_asc_sign),
},
"panchanga": {
"tithi": tithi_no,
diff --git a/scripts/benchmark_yoga_coverage.py b/scripts/benchmark_yoga_coverage.py
index c24e5149..d5728330 100644
--- a/scripts/benchmark_yoga_coverage.py
+++ b/scripts/benchmark_yoga_coverage.py
@@ -101,8 +101,8 @@ def extract_skill_names(rules: list[dict]) -> tuple[set[str], dict[str, list[str
return keys, reverse
-def extract_pyjhora_names(pyjhora_yoga_file: Path) -> set[str]:
- content = pyjhora_yoga_file.read_text(encoding="utf-8", errors="ignore")
+def extract_external_benchmark_names(external_benchmark_yoga_file: Path) -> set[str]:
+ content = external_benchmark_yoga_file.read_text(encoding="utf-8", errors="ignore")
funcs = re.findall(r"^def ([a-zA-Z_][a-zA-Z0-9_]*)\(", content, re.MULTILINE)
names: set[str] = set()
for fn in funcs:
@@ -126,7 +126,7 @@ def extract_pyjhora_names(pyjhora_yoga_file: Path) -> set[str]:
return names
-def find_pyjhora_yoga_file(explicit: str | None = None) -> Path | None:
+def find_external_benchmark_yoga_file(explicit: str | None = None) -> Path | None:
if explicit:
p = Path(explicit).expanduser().resolve()
return p if p.exists() else None
@@ -176,12 +176,12 @@ def main() -> int:
skill_keys, skill_reverse = extract_skill_names(rules)
- pyjhora_file = find_pyjhora_yoga_file(args.pyjhora_yoga_file)
+ external_benchmark_file = find_external_benchmark_yoga_file(args.external_benchmark_yoga_file)
py_names: set[str] = set()
missing: list[str] = []
coverage_pct = None
- if pyjhora_file:
- py_names = extract_pyjhora_names(pyjhora_file)
+ if external_benchmark_file:
+ py_names = extract_external_benchmark_names(external_benchmark_file)
missing = sorted([name for name in py_names if not covered(name, skill_keys)])
coverage_pct = round((len(py_names) - len(missing)) / len(py_names) * 100, 2) if py_names else None
@@ -197,11 +197,11 @@ def main() -> int:
"categories": dict(categories.most_common()),
"strength_values": dict(strength_values),
"skill_normalized_name_keys": len(skill_keys),
- "pyjhora_yoga_file": str(pyjhora_file) if pyjhora_file else None,
- "pyjhora_unique_yoga_names": len(py_names) if pyjhora_file else None,
- "matched_unique_yoga_names": (len(py_names) - len(missing)) if pyjhora_file else None,
+ "external_benchmark_yoga_file": str(external_benchmark_file) if external_benchmark_file else None,
+ "external_benchmark_unique_yoga_names": len(py_names) if external_benchmark_file else None,
+ "matched_unique_yoga_names": (len(py_names) - len(missing)) if external_benchmark_file else None,
"coverage_pct": coverage_pct,
- "missing_count": len(missing) if pyjhora_file else None,
+ "missing_count": len(missing) if external_benchmark_file else None,
"missing": missing,
}
@@ -221,11 +221,11 @@ def main() -> int:
print(f" {cat:16s} {count:3d}")
print("\nPyJHora 对比:")
- if not pyjhora_file:
+ if not external_benchmark_file:
print(" 未找到 PyJHora yoga.py;仅完成本地 JSON 统计。")
print(" 可用 --pyjhora-yoga-file 指定路径。")
else:
- print(f" yoga.py: {pyjhora_file}")
+ print(f" yoga.py: {external_benchmark_file}")
print(f" PyJHora 唯一 Yoga 名称: {len(py_names)}")
print(f" 已匹配: {len(py_names) - len(missing)}")
print(f" 疑似缺失: {len(missing)}")
diff --git a/scripts/build_standard_test_charts.py b/scripts/build_standard_test_charts.py
index be12f3d9..4cbaa6c2 100644
--- a/scripts/build_standard_test_charts.py
+++ b/scripts/build_standard_test_charts.py
@@ -74,7 +74,7 @@ CELEBRITY_CHARTS = [
{"name": "Paramahansa Yogananda", "date": "1893-01-05", "time": "20:38", "tz": "+05:30", "lat": 27.0360, "lon": 88.2627, "city": "Gorakhpur, India"},
]
-def compute_pyjhora_yogas(chart):
+def compute_external_benchmark_yogas(chart):
try:
result = subprocess.run(
[PYJHORA, HELPER],
@@ -98,12 +98,12 @@ if __name__ == "__main__":
}
for chart in CELEBRITY_CHARTS:
print(f" Computing {chart['name']}...", flush=True)
- yogas = compute_pyjhora_yogas(chart)
+ yogas = compute_external_benchmark_yogas(chart)
entry = dict(chart)
entry["expected_yogas"] = yogas.get("yogas", [])
if "context" in yogas:
entry["context"] = yogas["context"]
- entry["pyjhora_raw"] = yogas
+ entry["external_benchmark_raw"] = yogas
output["charts"].append(entry)
with open(outpath, "w") as f:
json.dump(output, f, indent=2, ensure_ascii=False)
diff --git a/scripts/sync_skill_truth_to_workbuddy.sh b/scripts/sync_skill_truth_to_workbuddy.sh
index 30e981fb..8b4c5669 100644
--- a/scripts/sync_skill_truth_to_workbuddy.sh
+++ b/scripts/sync_skill_truth_to_workbuddy.sh
@@ -8,6 +8,7 @@ mkdir -p "$WB/references"
mkdir -p "$WB/skills/jyotish-engine-modules"
mkdir -p "$WB/skills/jyotish-full-reading-integration"
+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/scripts/validate_logic_v2.py b/scripts/validate_logic_v2.py
index bf18e6d8..c2fe0a39 100644
--- a/scripts/validate_logic_v2.py
+++ b/scripts/validate_logic_v2.py
@@ -353,7 +353,7 @@ def main():
'chart': name,
'rule_id': rid,
'rule_name': rule_id_to_name.get(rid, '?'),
- 'pyjhora_names': orig_names,
+ 'external_benchmark_names': orig_names,
})
# ==== 输出报告 ====
@@ -404,8 +404,8 @@ def main():
"charts_tested": 60,
"comparable_rules": len(comparable_rule_ids),
"skill_total": total_skill_comp,
- "pyjhora_total": total_pyj_comp,
- "unmapped_pyjhora": total_unmapped_pyj,
+ "external_benchmark_total": total_pyj_comp,
+ "unmapped_external_benchmark": total_unmapped_pyj,
"missing_mappings": {k: v for k, v in sorted(missing_mappings.items())},
"agreements": total_agreements,
"false_positives": total_false_positives,
diff --git a/scripts/validate_yoga_accuracy.py b/scripts/validate_yoga_accuracy.py
index 902cba6e..9e938886 100644
--- a/scripts/validate_yoga_accuracy.py
+++ b/scripts/validate_yoga_accuracy.py
@@ -175,7 +175,7 @@ def skill_detect_yogas(planets, asc):
# ============================================================
# PyJhora 接口封装
# ============================================================
-def init_pyjhora():
+def init_external_benchmark():
"""初始化 PyJhora,返回是否成功"""
try:
import jhora.horoscope.chart.yoga as py_yoga
@@ -188,13 +188,13 @@ def init_pyjhora():
return False
-def pyjhora_jd(year, month, day, hour_frac):
+def external_benchmark_jd(year, month, day, hour_frac):
"""计算 Julian Day(与 PyJhora 一致)"""
import swisseph as swe
return swe.julday(year, month, day, hour_frac)
-def pyjhora_get_yogas(jd, lat, lon, tz, divisional_chart_factor=1):
+def external_benchmark_get_yogas(jd, lat, lon, tz, divisional_chart_factor=1):
"""
调用 PyJhora 获取 D1 宫盘的 Yoga 检测结果。
返回:{yoga_function_name: {"name": ..., "desc": ..., "benefits": ...}}
@@ -329,10 +329,10 @@ def _canonical_name(key: str) -> str:
return CROSS_NAME_MAP.get(key, key)
-def get_pyjhora_yoga_keys(pyjhora_results: Dict[str, dict]) -> Set[str]:
+def get_external_benchmark_yoga_keys(external_benchmark_results: Dict[str, dict]) -> Set[str]:
"""从 PyJhora 检测结果中提取归一化名称集合"""
keys = set()
- for fname in pyjhora_results.keys():
+ for fname in external_benchmark_results.keys():
# 去掉 _from_jd_place 等后缀
base = re.sub(r"_(from_jd_place|from_planet_positions|calculation|calc)$",
"", fname)
@@ -347,7 +347,7 @@ def get_pyjhora_yoga_keys(pyjhora_results: Dict[str, dict]) -> Set[str]:
# ============================================================
# 核心验证逻辑
# ============================================================
-def validate_one_case(case: dict, run_pyjhora: bool = True) -> dict:
+def validate_one_case(case: dict, run_external_benchmark: bool = True) -> dict:
"""
验证单个测试用例。
@@ -363,10 +363,10 @@ def validate_one_case(case: dict, run_pyjhora: bool = True) -> dict:
"birth": f"{case['year']}-{case['month']:02d}-{case['day']:02d} "
f"{case.get('hour', 12):02d}:{case.get('minute', 0):02d}",
"skill_yogas": [],
- "pyjhora_yogas": [],
+ "external_benchmark_yogas": [],
"matched": [],
"skill_only": [], # false positive
- "pyjhora_only": [], # false negative
+ "external_benchmark_only": [], # false negative
"error": None,
}
@@ -399,16 +399,16 @@ def validate_one_case(case: dict, run_pyjhora: bool = True) -> dict:
return result
# --- PyJhora 检测 ---
- if run_pyjhora:
+ if run_external_benchmark:
try:
- jd_py = pyjhora_jd(year, month, day, hour + minute / 60.0)
- pyjhora_results = pyjhora_get_yogas(jd_py, lat, lon, tz,
+ jd_py = external_benchmark_jd(year, month, day, hour + minute / 60.0)
+ external_benchmark_results = external_benchmark_get_yogas(jd_py, lat, lon, tz,
divisional_chart_factor=1)
- result['pyjhora_yogas'] = list(pyjhora_results.keys())
- result['pyjhora_count'] = len(pyjhora_results)
- result['pyjhora_details'] = []
- for fname, details in pyjhora_results.items():
- result['pyjhora_details'].append({
+ result['external_benchmark_yogas'] = list(external_benchmark_results.keys())
+ result['external_benchmark_count'] = len(external_benchmark_results)
+ result['external_benchmark_details'] = []
+ for fname, details in external_benchmark_results.items():
+ result['external_benchmark_details'].append({
'function': fname,
'name': details.get('name', ''),
'desc': details.get('desc', ''),
@@ -419,11 +419,11 @@ def validate_one_case(case: dict, run_pyjhora: bool = True) -> dict:
# --- 对比 ---
skill_keys = get_skill_yoga_keys(skill_yogas)
- pyjhora_keys = get_pyjhora_yoga_keys(pyjhora_results)
+ external_benchmark_keys = get_external_benchmark_yoga_keys(external_benchmark_results)
- result['matched'] = sorted(skill_keys & pyjhora_keys)
- result['skill_only'] = sorted(skill_keys - pyjhora_keys)
- result['pyjhora_only'] = sorted(pyjhora_keys - skill_keys)
+ result['matched'] = sorted(skill_keys & external_benchmark_keys)
+ result['skill_only'] = sorted(skill_keys - external_benchmark_keys)
+ result['external_benchmark_only'] = sorted(external_benchmark_keys - skill_keys)
# --- 分类:功能性 Yoga vs 经典 Yoga ---
cat_map = get_skill_rule_categories()
@@ -439,17 +439,17 @@ def validate_one_case(case: dict, run_pyjhora: bool = True) -> dict:
return result
-def run_validation(cases: List[dict], run_pyjhora: bool = True) -> dict:
+def run_validation(cases: List[dict], run_external_benchmark: bool = True) -> dict:
"""运行批量验证"""
report = {
"total_cases": len(cases),
"cases": [],
"summary": {
"total_skill_yogas": 0,
- "total_pyjhora_yogas": 0,
+ "total_external_benchmark_yogas": 0,
"total_matched": 0,
"total_skill_only": 0,
- "total_pyjhora_only": 0,
+ "total_external_benchmark_only": 0,
"total_skill_only_functional": 0,
"total_skill_only_classic": 0,
}
@@ -458,7 +458,7 @@ def run_validation(cases: List[dict], run_pyjhora: bool = True) -> dict:
for case in cases:
name = case.get('name', 'unknown')
print(f"🔍 验证: {name} ({case['year']}-{case['month']:02d}-{case['day']:02d})")
- r = validate_one_case(case, run_pyjhora=run_pyjhora)
+ r = validate_one_case(case, run_external_benchmark=run_external_benchmark)
report['cases'].append(r)
if r['error']:
@@ -466,21 +466,21 @@ def run_validation(cases: List[dict], run_pyjhora: bool = True) -> dict:
continue
sc = r.get('skill_count', 0)
- pc = r.get('pyjhora_count', '?')
+ pc = r.get('external_benchmark_count', '?')
func_n = len(r.get('skill_only_functional', []))
classic_n = len(r.get('skill_only_classic', []))
print(f" Skill: {sc} | PyJhora: {pc}")
print(f" 匹配: {len(r['matched'])} | "
f"Skill独有: {len(r['skill_only'])} (功能性{func_n}, 经典{classic_n}) | "
- f"PyJhora独有: {len(r['pyjhora_only'])}")
+ f"PyJhora独有: {len(r['external_benchmark_only'])}")
report['summary']['total_skill_yogas'] += sc
if isinstance(pc, int):
- report['summary']['total_pyjhora_yogas'] += pc
+ report['summary']['total_external_benchmark_yogas'] += pc
report['summary']['total_matched'] += len(r['matched'])
report['summary']['total_skill_only'] += len(r['skill_only'])
if isinstance(pc, int):
- report['summary']['total_pyjhora_only'] += len(r['pyjhora_only'])
+ report['summary']['total_external_benchmark_only'] += len(r['external_benchmark_only'])
report['summary']['total_skill_only_functional'] += func_n
report['summary']['total_skill_only_classic'] += classic_n
@@ -535,27 +535,27 @@ def print_report(report: dict):
summary = report['summary']
total_skill = summary['total_skill_yogas']
- total_pyjhora = summary['total_pyjhora_yogas']
+ total_external_benchmark = summary['total_external_benchmark_yogas']
matched = summary['total_matched']
skill_only = summary['total_skill_only']
- pyjhora_only = summary['total_pyjhora_only']
+ external_benchmark_only = summary['total_external_benchmark_only']
func_only = summary.get('total_skill_only_functional', 0)
classic_only = summary.get('total_skill_only_classic', 0)
print(f"\n📊 汇总:")
print(f" 测试用例数: {report['total_cases']}")
print(f" Skill 检测总数: {total_skill}")
- print(f" PyJhora 检测总数: {total_pyjhora}")
+ print(f" PyJhora 检测总数: {total_external_benchmark}")
print(f" 匹配总数: {matched}")
print(f" Skill 独有: {skill_only} (功能性{func_only}, 经典{classic_only})")
- print(f" PyJhora 独有 (skill 缺失): {pyjhora_only}")
+ print(f" PyJhora 独有 (skill 缺失): {external_benchmark_only}")
if total_skill > 0:
precision = matched / total_skill * 100
print(f"\n 总体精确率 (Precision): {precision:.1f}%")
- if total_pyjhora > 0:
- recall = matched / total_pyjhora * 100
+ if total_external_benchmark > 0:
+ recall = matched / total_external_benchmark * 100
print(f" 总体召回率 (Recall): {recall:.1f}%")
# --- 核心经典 Yoga 准确率(排除 skill 特色功能性 Yoga)---
@@ -564,21 +564,21 @@ def print_report(report: dict):
classic_precision = matched / classic_skill_total * 100
print(f"\n 🎯 核心经典 Yoga 精确率: {classic_precision:.1f}%")
print(f" (排除 {func_only} 条功能性 Yoga 后: {matched}/{classic_skill_total})")
- if total_pyjhora > 0:
- classic_recall = matched / total_pyjhora * 100
+ if total_external_benchmark > 0:
+ classic_recall = matched / total_external_benchmark * 100
print(f" 🎯 核心经典 Yoga 召回率: {classic_recall:.1f}%")
# --- 全局不匹配统计 ---
print(f"\n🔍 全局不匹配分析:")
all_skill_classic = set()
all_skill_func = set()
- all_pyjhora_missing = set()
+ all_external_benchmark_missing = set()
for r in report['cases']:
if r.get('error'):
continue
all_skill_classic.update(r.get('skill_only_classic', []))
all_skill_func.update(r.get('skill_only_functional', []))
- all_pyjhora_missing.update(r.get('pyjhora_only', []))
+ all_external_benchmark_missing.update(r.get('external_benchmark_only', []))
print(f" Skill 经典 Yoga 不匹配(可能误判): {len(all_skill_classic)} 种")
if all_skill_classic:
@@ -586,9 +586,9 @@ def print_report(report: dict):
print(f" Skill 功能性 Yoga(PyJhora 无对应,属 skill 特色): {len(all_skill_func)} 种")
if all_skill_func:
print(f" {sorted(all_skill_func)[:15]}")
- print(f" PyJhora 有但 Skill 缺失的 Yoga: {len(all_pyjhora_missing)} 种")
- if all_pyjhora_missing:
- print(f" {sorted(all_pyjhora_missing)[:15]}")
+ print(f" PyJhora 有但 Skill 缺失的 Yoga: {len(all_external_benchmark_missing)} 种")
+ if all_external_benchmark_missing:
+ print(f" {sorted(all_external_benchmark_missing)[:15]}")
# --- 改进建议 ---
print(f"\n💡 改进建议:")
@@ -599,13 +599,13 @@ def print_report(report: dict):
if len(all_skill_classic) > 10:
print(f" ... 等共 {len(all_skill_classic)} 种")
- missing_top = sorted(all_pyjhora_missing)[:20]
- print(f"\n 2. 【规则补齐】以下 {len(all_pyjhora_missing)} 种 Yoga PyJhora 已实现但 skill 缺失,")
+ missing_top = sorted(all_external_benchmark_missing)[:20]
+ print(f"\n 2. 【规则补齐】以下 {len(all_external_benchmark_missing)} 种 Yoga PyJhora 已实现但 skill 缺失,")
print(f" 建议按优先级补充(推荐先补充高频出现的):")
for name in missing_top[:15]:
print(f" - {name}")
- if len(all_pyjhora_missing) > 15:
- print(f" ... 等共 {len(all_pyjhora_missing)} 种")
+ if len(all_external_benchmark_missing) > 15:
+ print(f" ... 等共 {len(all_external_benchmark_missing)} 种")
print(f"\n 3. 【名称映射】当前 CROSS_NAME_MAP 已覆盖常见别名,")
print(f" 如仍有新别名发现,请添加到映射表中。")
@@ -617,20 +617,20 @@ def print_report(report: dict):
print(f" ❌ {r['error'][:300]}")
continue
sc = r.get('skill_count', 0)
- pc = r.get('pyjhora_count', '?')
+ pc = r.get('external_benchmark_count', '?')
print(f" Skill ({sc}): {r['skill_yogas'][:5]}{'...' if len(r['skill_yogas']) > 5 else ''}")
- if 'pyjhora_yogas' in r:
- print(f" PyJhora ({pc}): {r['pyjhora_yogas'][:5]}{'...' if len(r['pyjhora_yogas']) > 5 else ''}")
+ if 'external_benchmark_yogas' in r:
+ print(f" PyJhora ({pc}): {r['external_benchmark_yogas'][:5]}{'...' if len(r['external_benchmark_yogas']) > 5 else ''}")
func_n = len(r.get('skill_only_functional', []))
cls_n = len(r.get('skill_only_classic', []))
- print(f" 匹配: {len(r['matched'])} | Skill独有: {len(r['skill_only'])}(功能{func_n},经典{cls_n}) | PyJhora独有: {len(r['pyjhora_only'])}")
+ print(f" 匹配: {len(r['matched'])} | Skill独有: {len(r['skill_only'])}(功能{func_n},经典{cls_n}) | PyJhora独有: {len(r['external_benchmark_only'])}")
if r.get('skill_only_classic'):
print(f" Skill 经典不匹配: {r['skill_only_classic'][:10]}")
if r.get('skill_only_functional'):
print(f" Skill 功能性: {r['skill_only_functional'][:10]}")
- if r.get('pyjhora_only'):
- print(f" PyJhora 独有: {r['pyjhora_only'][:10]}")
+ if r.get('external_benchmark_only'):
+ print(f" PyJhora 独有: {r['external_benchmark_only'][:10]}")
def save_report(report: dict, output_file: str):
@@ -653,8 +653,8 @@ def main():
args = parser.parse_args()
# 检查 PyJhora
- if not args.skip_pyjhora:
- if not init_pyjhora():
+ if not args.skip_external_benchmark:
+ if not init_external_benchmark():
print("❌ PyJhora 不可用,请先安装: pip install pyjhora swisseph")
print(" 提示:也可用 --skip-pyjhora 只测试 skill 侧")
return 1
@@ -672,7 +672,7 @@ def main():
return 1
# 运行验证
- report = run_validation(cases, run_pyjhora=not args.skip_pyjhora)
+ report = run_validation(cases, run_external_benchmark=not args.skip_external_benchmark)
# 输出报告
print_report(report)
diff --git a/scripts/yoga_engine.py b/scripts/yoga_engine.py
index 2f48a64c..ed828247 100644
--- a/scripts/yoga_engine.py
+++ b/scripts/yoga_engine.py
@@ -1298,7 +1298,7 @@ class YogaEngine:
return False
return offset(ctx.house_of(p), h) in (6, 3 if p == 'Mars' else -1, 7 if p == 'Mars' else -1, 4 if p == 'Jupiter' else -1, 8 if p == 'Jupiter' else -1, 2 if p == 'Saturn' else -1, 9 if p == 'Saturn' else -1)
- def pyjhora_planets_aspecting_raasi(p, h):
+ def external_benchmark_planets_aspecting_raasi(p, h):
"""Replicate PyJHora house.planets_aspecting_the_raasi() behavior for source parity."""
if p not in ctx.planets or h is None:
return False
@@ -1314,7 +1314,7 @@ class YogaEngine:
]
return target_rasi_idx in planet_ids_in_aspected_signs
- def pyjhora_aspected_planets_of_raasi(h):
+ def external_benchmark_aspected_planets_of_raasi(h):
"""Replicate PyJHora house.aspected_planets_of_the_raasi(): planets whose rasi drishti hits a target house."""
if h is None:
return []
@@ -1431,7 +1431,7 @@ class YogaEngine:
occupants = ctx.planets_in_house(target)
return bool(occupants) and all(p in BENEFICS for p in occupants)
- def pyjhora_natural_benefics():
+ def external_benchmark_natural_benefics():
"""Replicate PyJHora yoga._get_natural_benefics(): Jupiter, Venus, plus benefic Mercury."""
benefics = [p for p in ["Jupiter", "Venus"] if p in ctx.planets]
mercury_house = ctx.house_of("Mercury")
@@ -1492,8 +1492,8 @@ class YogaEngine:
# v6.0.32: 同宫与相位检查(custom规则常用)
"same_house": same_house, "aspect": aspect, "aspects_house": aspects_house,
"graha_aspects_house": graha_aspects_house,
- "pyjhora_planets_aspecting_raasi": pyjhora_planets_aspecting_raasi,
- "pyjhora_aspected_planets_of_raasi": pyjhora_aspected_planets_of_raasi,
+ "external_benchmark_planets_aspecting_raasi": external_benchmark_planets_aspecting_raasi,
+ "external_benchmark_aspected_planets_of_raasi": external_benchmark_aspected_planets_of_raasi,
"rasi_drishti_signs_from": rasi_drishti_signs_from,
"rasi_aspects_house": rasi_aspects_house, "rasi_aspects": rasi_aspects,
"rasi_aspected_by_planets": rasi_aspected_by_planets,
@@ -1503,7 +1503,7 @@ class YogaEngine:
"only_malefics_in_house": only_malefics_in_house,
"house_has_benefic": house_has_benefic, "house_has_malefic": house_has_malefic,
"house_sign": house_sign, "movable_house": movable_house,
- "pyjhora_natural_benefics": pyjhora_natural_benefics,
+ "external_benchmark_natural_benefics": external_benchmark_natural_benefics,
"d9_house_of": d9_house_of, "d9_sign_of": d9_sign_of,
"d9_lord_of_house": d9_lord_of_house, "navamsa_dispositor": navamsa_dispositor,
"tithi": tithi, "is_waning_moon": is_waning_moon,
diff --git a/skills/jyotish-engine-modules/SKILL.md b/skills/jyotish-engine-modules/SKILL.md
index c2aaded3..769bcce1 100644
--- a/skills/jyotish-engine-modules/SKILL.md
+++ b/skills/jyotish-engine-modules/SKILL.md
@@ -1,17 +1,42 @@
---
name: jyotish-engine-modules
-description: 印度占星排盘引擎缺失模块完整代码(5个核心模块)
+description: 印度占星排盘引擎历史模块集成说明(供审计与迁移参考,当前真源以主 SKILL.md 与 registry 为准)
version: 1.0.0
author: 助手
tags: [jyotish, vedic-astrology, calculation-engine, karaka, special-lagnas, vimsopaka, avastha, divisional-charts]
related_skills: [jyotish-full-reading-integration, jyotish-vedic-astrology]
---
-# 印度占星排盘引擎缺失模块
+# 印度占星排盘引擎历史模块说明
-本skill包含5个核心计算模块的完整Python代码,用于补充GitHub仓库 `yinduzhanxing` 的排盘引擎。
+本 skill 记录了 5 个核心计算模块的历史集成背景,主要用于审计、迁移和对照旧工作流。
-## 模块清单
+当前项目的唯一对外真源以以下文件为准:
+
+1. `/Users/wuyongnaren/Documents/印度占星/SKILL.md`
+2. `/Users/wuyongnaren/Documents/印度占星/references/technique_registry.json`
+3. `/Users/wuyongnaren/Documents/印度占星/references/strict-workflow-router.md`
+
+本文件不得单独作为“当前能力完整性”依据;若与主 skill 描述冲突,以主 skill 与 registry 为准。
+
+## 当前使用边界
+
+本文件的定位是:
+
+1. 说明历史模块曾如何并入主仓
+2. 便于审计、迁移、回收旧碎片
+3. 为“是否已有实现可复用”提供线索
+
+本文件**不是**当前 skill 完成度、成熟度或精度闭环的直接依据。
+
+## 判断原则(与主 skill 保持一致)
+
+1. 若主仓 `scripts/`、`references/`、`skills/` 已有主体实现,优先判断为“补成熟度/补入口”,不要在旧模块层面重写。
+2. 若外部来源是 MIT / Apache / BSD,可优先考虑复用结构、常数、映射与文档资产;GPL / AGPL / 闭源仅允许黑盒对照。
+3. 涉及 `Dasha` 精确边界、`Shadbala` 绝对值、传统软件口径冻结时,必须以外部 oracle 闭环为准,不能仅凭历史模块说明认定“已完成”。
+4. 若本文件的历史调用链与主仓当前真相源不一致,以主仓真相源为准,不沿用旧版本表述。
+
+## 历史模块清单
1. **karaka_calculator.py** - Karaka分配计算器(支持BPHS/JH兼容模式)
2. **special_lagnas.py** - 特殊上升点计算(Bhava/Hora/Ghati Lagna等)
@@ -145,7 +170,7 @@ from avastha_calculator import AvasthaCalculator
from divisional_charts_extended import DivisionalChartsCalculator
```
-> **注意**:这 5 个模块已整合到主仓库的 `scripts/` 目录中,与引擎一起维护。本 Skill 的 `scripts/` 副本仅作为独立分发包。
+> **注意**:这 5 个模块现已并入主仓库维护。本 Skill 的 `scripts/` 副本只作为历史分发包/迁移参考,不代表当前能力边界。
> **AI Native 注意**:主仓库 `full-reading` 与 `/api/chart` 已输出 `ai_prompt_pack` 和 Ayanamsa 元数据;独立分发时若调用主引擎,应优先消费这些字段作为解读上下文,不要在 Raman/KP 等非 Lahiri 设置下硬编码默认口径。
## 验证测试
diff --git a/skills/jyotish-full-reading-integration/SKILL.md b/skills/jyotish-full-reading-integration/SKILL.md
index 27ca5150..0fec2c07 100644
--- a/skills/jyotish-full-reading-integration/SKILL.md
+++ b/skills/jyotish-full-reading-integration/SKILL.md
@@ -1,15 +1,40 @@
---
name: jyotish-full-reading-integration
-description: 印度占星 full-reading 21步调用链集成方案(v4.4.0)
+description: 印度占星 full-reading 历史集成方案(供审计与迁移参考,当前真源以主 SKILL.md 与 registry 为准)
version: 4.4.0
author: 助手
tags: [jyotish, integration, full-reading, karaka-jh-mode, jyotish-engine]
related_skills: [jyotish-engine-modules, jyotish-vedic-astrology]
---
-# 印度占星 Full-Reading 21步调用链集成方案
+# 印度占星 Full-Reading 历史集成方案
-将 5 个新增模块(`special_lagnas` / `karaka_calculator` / `vimsopaka_calculator` / `avastha_calculator` / `divisional_charts_extended`)整合进 `jyotish_engine.py` 的 `full-reading` 调用链。
+本文档记录把 5 个模块整合进 `jyotish_engine.py` 的历史方案,主要用于审计、迁移和比对旧调用链。
+
+当前项目的唯一对外真源以以下文件为准:
+
+1. `/Users/wuyongnaren/Documents/印度占星/SKILL.md`
+2. `/Users/wuyongnaren/Documents/印度占星/references/technique_registry.json`
+3. `/Users/wuyongnaren/Documents/印度占星/references/strict-workflow-router.md`
+
+如果本文件与主 skill/registry 的成熟度、边界或步骤定义不一致,必须以后者为准。
+
+## 当前使用边界
+
+本文件的定位是:
+
+1. 记录 full-reading 历史调用链如何演进
+2. 方便审计旧版整合方案与迁移痕迹
+3. 帮助识别“哪些能力早已存在,只是后来入口或描述变化了”
+
+本文件**不是**当前 skill 精度闭环、成熟度分级或对标完成度的最终依据。
+
+## 判断原则(与主 skill 保持一致)
+
+1. 先判断当前主仓是否已经存在对应模块与执行链;若已存在,优先补入口、补边界、补解释层,不重复实现。
+2. 凡涉及 `covered` 与 `complete` 的区分,以主 skill 和 registry 为准,不得因为历史 full-reading 曾调用某模块,就推断该能力已经完全闭环。
+3. 凡涉及 MIT / Apache / BSD 资产,可继续作为合法复用来源;GPL / AGPL / 闭源方案只做黑盒 benchmark,不复制实现。
+4. 凡涉及 `Dasha`、`Shadbala`、`KP/Prashna`、`Tajika/Varshaphala/Sahams` 的传统软件级冻结,必须额外经过外部 oracle 或公开样本闭环。
## 核心变化
diff --git a/tests/test_dasha.py b/tests/test_dasha.py
index 1edb4ab9..8fbbde5b 100644
--- a/tests/test_dasha.py
+++ b/tests/test_dasha.py
@@ -26,6 +26,7 @@ from jyotish_engine import (
EXALTATION, DEBILITATION, SIGN_LORDS, MOOLATRIKONA,
DASHA_ORDER, DASHA_YEARS, NAKSHATRA_LIST, SIGNS,
)
+from kalachakra_dasha import calculate_kalachakra_dasha
class TestDignityLevel(unittest.TestCase):
@@ -52,10 +53,8 @@ class TestDignityLevel(unittest.TestCase):
self.assertEqual(_get_dignity_level('Mars', 'Aries', 15.0), 'OWN_SIGN')
def test_friend(self):
- """月亮在 Aries(火星的星座,月亮是火星的朋友)"""
- # Mars 的 PERMANENT_FRIENDS 包含 Moon,所以 Moon 在 Mars 的星座是 Friend
- # 但这里测的是 Moon 在 Aries: Aries lord = Mars, Moon in PERMANENT_FRIENDS['Mars'] = yes
- self.assertEqual(_get_dignity_level('Moon', 'Aries'), 'FRIEND')
+ """太阳在 Cancer(Sun 对 Moon 为友,且不触发更高优先级尊严)"""
+ self.assertEqual(_get_dignity_level('Sun', 'Cancer'), 'FRIEND')
def test_enemy(self):
"""金星在 Aries(火星的星座,金星是火星的敌人 → 但实际看 Aries lord(Mars) 的朋友表里没有 Venus → 不是 Friend)
@@ -69,10 +68,8 @@ class TestDignityLevel(unittest.TestCase):
self.assertEqual(_get_dignity_level('Saturn', 'Leo'), 'ENEMY')
def test_neutral(self):
- """木星在 Gemini(Mercury 守护),Jupiter 不在 Mercury 的 friends 也不在 enemies"""
- # PERMANENT_FRIENDS['Mercury'] = ['Sun', 'Venus']
- # PERMANENT_ENEMIES['Mercury'] = ['Moon']
- self.assertEqual(_get_dignity_level('Jupiter', 'Gemini'), 'NEUTRAL')
+ """月亮在 Aries(Moon 对 Mars 既非友也非敌)"""
+ self.assertEqual(_get_dignity_level('Moon', 'Aries'), 'NEUTRAL')
def test_all_exaltation_signs(self):
"""验证所有行星的入旺星座"""
@@ -225,6 +222,32 @@ class TestNakshatraMapping(unittest.TestCase):
self.assertEqual(result['moon_nakshatra'], 'Jyeshtha')
+class TestKalachakraDasha(unittest.TestCase):
+ """测试 Kalachakra Dasha 简化集成状态"""
+
+ def test_kalachakra_reports_savya_mode(self):
+ result = calculate_kalachakra_dasha({
+ 'moon_nakshatra_index': 0,
+ 'moon_pada': 1,
+ 'birth_datetime': datetime(1990, 6, 15, 10, 30),
+ })
+ self.assertEqual(result['mode'], 'savya')
+ self.assertEqual(result['starting_lord'], 'Ketu')
+ self.assertEqual(result['starting_rashi'], 'Capricorn')
+ self.assertTrue(result['current'])
+
+ def test_kalachakra_reports_apasavya_mode(self):
+ result = calculate_kalachakra_dasha({
+ 'moon_nakshatra_index': 3,
+ 'moon_pada': 2,
+ 'birth_datetime': datetime(1985, 3, 20, 8, 0),
+ })
+ self.assertEqual(result['mode'], 'apasavya')
+ self.assertEqual(result['starting_lord'], 'Ketu')
+ self.assertTrue(result['current'])
+ self.assertGreater(result['total_cycle'], 0)
+
+
class TestYogaDetection(unittest.TestCase):
"""测试 Yoga 格局识别"""
diff --git a/tests/test_tajika_annual_closure_status.py b/tests/test_tajika_annual_closure_status.py
index 22b5c781..a6b937fb 100644
--- a/tests/test_tajika_annual_closure_status.py
+++ b/tests/test_tajika_annual_closure_status.py
@@ -39,6 +39,9 @@ def test_tajika_annual_closure_status_identifies_first_annual_packet() -> None:
assert report["summary"]["annual_task_count"] == 5
assert report["summary"]["external_verified_annual_tasks"] == 1
assert report["summary"]["can_claim_tajika_sahams_closure"] is False
+ from scripts import tajika_annual_closure_status as module
+ assert module.FIRST_PRIORITY_CASE_ID == "template_einstein_varshaphala_1905_lahiri"
+ assert module.FIRST_PRIORITY_TEMPLATE_PATH.endswith("external_template_einstein_varshaphala_1905_lahiri.json")
assert report["first_priority"]["case_id"] == "template_einstein_varshaphala_1905_lahiri"
assert report["first_priority"]["capture_id"] == "external_template_einstein_varshaphala_1905_lahiri"
assert report["first_priority"]["required_target_fields"] == [