Add Jyotish registry audit and CI foundation
This commit is contained in:
@@ -0,0 +1,34 @@
|
||||
name: Jyotish Skill CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
validate:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.11'
|
||||
|
||||
- name: Install dependencies
|
||||
run: python -m pip install --upgrade pip && pip install -r requirements.txt
|
||||
|
||||
- name: Compile Python files
|
||||
run: python -m py_compile scripts/jyotish_engine.py scripts/special_lagnas.py scripts/audit_capabilities.py tests/run_golden_cases.py
|
||||
|
||||
- name: Validate technique registry
|
||||
run: python scripts/audit_capabilities.py --mode validate
|
||||
|
||||
- name: Run unit tests
|
||||
run: python -m unittest discover tests
|
||||
|
||||
- name: Run golden cases
|
||||
run: python tests/run_golden_cases.py --python python
|
||||
@@ -1,5 +1,33 @@
|
||||
# 印度占星 Skill 更新日志
|
||||
|
||||
## v6.0.3-engineering-foundation(2026-06-03)— 能力平台化第一阶段
|
||||
|
||||
> **触发原因**:用户要求从顶级软件工程师角度“智慧地下载使用全网帮助优化工作的开源项目”,快速把 skill 的效率和可靠性继续提高。调研 `jyotishyamitra`、`jyotisham/jyotisha`、`Yamale` 后,采用其工程启发:结构化 JSON 输出、计算层与解释层分离、注册表 schema 校验;但为避免新依赖,当前实现保持 Python stdlib。
|
||||
|
||||
### ① 新增机器可读能力注册表
|
||||
|
||||
- 新增 `references/technique_registry.json`:把技法、领域、状态、命令、输出路径、置信度影响、route required/optional 技法做成机器可读配置。
|
||||
- 目的:以后判断 covered/partial/missing 不再靠 AI 印象,而由注册表驱动。
|
||||
|
||||
### ② 新增能力审计脚本与 CLI 子命令
|
||||
|
||||
- 新增 `scripts/audit_capabilities.py`:校验注册表字段、状态枚举、引用文件、route 指向,并可输出 route 审计表。
|
||||
- `scripts/jyotish_engine.py` 新增 `audit-capabilities` 子命令:
|
||||
- `audit-capabilities --mode validate`
|
||||
- `audit-capabilities --mode table --route career_timing_strict`
|
||||
|
||||
### ③ 新增 golden cases 回归框架
|
||||
|
||||
- 新增 `tests/golden/golden_cases.json`:定义 full-reading 输出契约的 golden smoke case。
|
||||
- 新增 `tests/run_golden_cases.py`:运行 full-reading 并断言关键输出路径存在,包括 A10、Vargottama、Pushkara、Dasha Sandhi、Shadbala、Ashtakavarga、validation。
|
||||
|
||||
### ④ 新增 GitHub Actions CI 门禁
|
||||
|
||||
- 新增 `.github/workflows/ci.yml`:自动运行依赖安装、Python 编译、registry 校验、单元测试、golden cases。
|
||||
- 目的:防止后续修改导致技法输出路径消失、注册表腐化或核心测试退化。
|
||||
|
||||
---
|
||||
|
||||
## v6.0.2-capability-patch(2026-06-03)— 技法覆盖审计 + 可低风险缺口补齐
|
||||
|
||||
> **触发原因**:用户质疑此前将 A10、Pushkara、Vargottama、Avastha、Sudarshana、Dasha Sandhi 等统称为“缺失”可能是审计遗漏。经地毯式搜索确认:部分技法已有知识/流程/App层覆盖,但未进入 CLI/full-reading 输出;因此本版把“缺失”改为分层判断,并补齐可低风险实现项。
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: jyotish-vedic-astrology
|
||||
version: 6.0.0
|
||||
version: 6.0.3
|
||||
description: 印度占星(Jyotish)专业解盘与推运系统。核心能力:PDF星盘输入→严谨解盘→精确推运应期输出。触发词:印度占星、吠陀占星、Jyotish、解盘、推运、星盘分析、Dasha、Transit、Nakshatra、Yoga、出生时间矫正、PDF星盘、读取PDF、分析PDF星盘、现代解读、误判纠错、Varga分盘、综合分析、过境分析、合盘、婚姻匹配、年运盘、Prashna、Argala、Jaimini、KP系统、Shadbala、Ashtakavarga、HTML报告、深度解盘。
|
||||
---
|
||||
|
||||
@@ -9,7 +9,8 @@ description: 印度占星(Jyotish)专业解盘与推运系统。核心能力
|
||||
> **执行总控**:`references/quick-reference-guide.md`(⭐推荐优先阅读)
|
||||
> **严格路由**:`references/strict-workflow-router.md`(⭐涉及事业/婚恋/财务/应期/技法验证时必须优先读取)
|
||||
> **覆盖矩阵**:`references/technique-capability-matrix.md`(⭐判断技法 covered/partial/missing 时必须参考)
|
||||
> **版本**:v6.0.2-capability-patch | **详细变更**:`CHANGELOG.md`
|
||||
> **机器注册表**:`references/technique_registry.json` + `scripts/audit_capabilities.py`(⭐用于自动审计与CI门禁)
|
||||
> **版本**:v6.0.3-engineering-foundation | **详细变更**:`CHANGELOG.md`
|
||||
|
||||
---
|
||||
|
||||
@@ -136,6 +137,7 @@ $PYTHON $SCRIPT <子命令> [参数]
|
||||
| `transit-ll7l` | Transit LL/7L连接+互换 |
|
||||
| `planetary-congregation` | 行星聚集检测 |
|
||||
| `vivah-saham` | Vivah Saham婚姻敏感点 |
|
||||
| `audit-capabilities` | technique registry 校验 + route 审计表输出 |
|
||||
|
||||
→ 完整参数和示例 → `references/quick-reference-guide.md`
|
||||
|
||||
@@ -239,9 +241,9 @@ $PYTHON $SCRIPT <子命令> [参数]
|
||||
|
||||
---
|
||||
|
||||
**版本**:v6.0.2-capability-patch
|
||||
**版本**:v6.0.3-engineering-foundation
|
||||
**创建日期**:2026-04-20
|
||||
**最后更新**:2026-06-03(v6.0.2 补齐 A10/Karma Pada、Pushkara、Vargottama、Dasha Sandhi 的 full-reading 输出,并修正技法覆盖声明)
|
||||
**最后更新**:2026-06-03(v6.0.3 新增 technique registry、能力审计脚本、golden cases 回归测试和 GitHub Actions CI 门禁)
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -178,7 +178,7 @@ Never omit unavailable techniques silently. Mark them as unavailable or not inte
|
||||
|
||||
## 9. Known product gaps
|
||||
|
||||
The current skill has strong coverage. Do not describe a technique as simply “missing”; classify it by capability layer. See `references/technique-capability-matrix.md`.
|
||||
The current skill has strong coverage. Do not describe a technique as simply “missing”; classify it by capability layer. See `references/technique-capability-matrix.md` and the machine-readable `references/technique_registry.json`. For automated checks, run `python scripts/jyotish_engine.py audit-capabilities --mode validate` or `--mode table --route <route_id>`.
|
||||
|
||||
| Gap / partial area | Current handling | Impact |
|
||||
|---|---|---|
|
||||
|
||||
@@ -0,0 +1,202 @@
|
||||
{
|
||||
"version": "v6.0.3-engineering-foundation",
|
||||
"source_inspiration": [
|
||||
{
|
||||
"name": "jyotishyamitra",
|
||||
"url": "https://pypi.org/project/jyotishyamitra/",
|
||||
"lesson": "Use structured dictionary/JSON sections for chart, varga, bala, ashtakavarga and dasha outputs. Do not depend on free-form prose for facts."
|
||||
},
|
||||
{
|
||||
"name": "jyotisham/jyotisha",
|
||||
"url": "https://github.com/jyotisham/jyotisha",
|
||||
"lesson": "Keep astronomical/calendar computation separate from interpretive reasoning; prefer deterministic data layers."
|
||||
},
|
||||
{
|
||||
"name": "Yamale",
|
||||
"url": "https://pypi.org/project/yamale/",
|
||||
"lesson": "Validate registry-like configuration against a schema before using it. This project keeps a stdlib validator to avoid new dependencies."
|
||||
}
|
||||
],
|
||||
"status_vocabulary": ["covered", "partial", "knowledge-only", "workflow-only", "not-integrated", "missing"],
|
||||
"techniques": {
|
||||
"d1_rashi": {
|
||||
"name": "D1 / Rashi",
|
||||
"domains": ["core", "static"],
|
||||
"status": "covered",
|
||||
"knowledge_refs": ["references/signs-and-houses.md"],
|
||||
"commands": ["chart", "full-reading"],
|
||||
"output_paths": ["modules.chart"],
|
||||
"audit_label": "D1/Rashi",
|
||||
"missing_impact": "No reliable static chart basis."
|
||||
},
|
||||
"d9_navamsa": {
|
||||
"name": "D9 / Navamsa",
|
||||
"domains": ["varga", "strength", "relationship", "dharma"],
|
||||
"status": "covered",
|
||||
"knowledge_refs": ["references/navamsa-d9-interpretation-template.md", "references/varga-system-quick-reference.md"],
|
||||
"commands": ["varga-full", "full-reading"],
|
||||
"output_paths": ["modules.varga_full.D9_Navamsa", "modules.d9_navamsa_expanded"],
|
||||
"audit_label": "D9/Navamsa",
|
||||
"missing_impact": "Inner strength and marriage/dharma layer incomplete."
|
||||
},
|
||||
"d10_dasamsa": {
|
||||
"name": "D10 / Dasamsa",
|
||||
"domains": ["career", "varga"],
|
||||
"status": "covered",
|
||||
"knowledge_refs": ["references/varga-system-quick-reference.md", "references/divisional-chart-deep-reading.md"],
|
||||
"commands": ["varga-full", "full-reading"],
|
||||
"output_paths": ["modules.varga_full.D10_Dasamsa"],
|
||||
"audit_label": "D10/Dasamsa",
|
||||
"missing_impact": "Career-specific varga layer missing; career timing confidence must be capped."
|
||||
},
|
||||
"vimshottari_dasha": {
|
||||
"name": "Vimshottari Dasha",
|
||||
"domains": ["timing", "event"],
|
||||
"status": "covered",
|
||||
"knowledge_refs": ["references/dasha-calculation-tool.md"],
|
||||
"commands": ["dasha", "full-reading"],
|
||||
"output_paths": ["modules.dasha"],
|
||||
"audit_label": "Vimshottari",
|
||||
"missing_impact": "Primary timing layer unavailable."
|
||||
},
|
||||
"dasha_sandhi": {
|
||||
"name": "Dasha Sandhi",
|
||||
"domains": ["timing", "transition"],
|
||||
"status": "covered",
|
||||
"knowledge_refs": ["references/strict-workflow-router.md"],
|
||||
"commands": ["full-reading"],
|
||||
"output_paths": ["modules.dasha_sandhi"],
|
||||
"audit_label": "Dasha Sandhi",
|
||||
"missing_impact": "Transition windows may be overstated or understated."
|
||||
},
|
||||
"jaimini_chara_dasha": {
|
||||
"name": "Jaimini / Chara Dasha",
|
||||
"domains": ["timing", "career", "relationship"],
|
||||
"status": "covered",
|
||||
"knowledge_refs": ["references/jaimini-complete-system.md"],
|
||||
"commands": ["jaimini", "full-reading"],
|
||||
"output_paths": ["modules.jaimini"],
|
||||
"audit_label": "Jaimini/Chara",
|
||||
"missing_impact": "Rashi-based timing confirmation missing."
|
||||
},
|
||||
"karakamsha_ak_amk": {
|
||||
"name": "Karakamsha / AK / AmK",
|
||||
"domains": ["career", "soul", "dharma"],
|
||||
"status": "covered",
|
||||
"knowledge_refs": ["references/jaimini-complete-system.md"],
|
||||
"commands": ["jaimini", "full-reading"],
|
||||
"output_paths": ["modules.jaimini.karakas", "modules.jaimini.karakamsha"],
|
||||
"audit_label": "AK/AmK/Karakamsha",
|
||||
"missing_impact": "Jaimini career/soul indicator layer incomplete."
|
||||
},
|
||||
"a10_karma_pada": {
|
||||
"name": "A10 / Karma Pada / Rajya Pada",
|
||||
"domains": ["career", "public_image"],
|
||||
"status": "covered",
|
||||
"knowledge_refs": ["references/technique-capability-matrix.md"],
|
||||
"commands": ["full-reading", "scripts/special_lagnas.py --tenth-lord"],
|
||||
"output_paths": ["modules.special_lagnas.A10_Karma_Pada"],
|
||||
"audit_label": "A10/Karma Pada",
|
||||
"missing_impact": "Career public-image layer incomplete."
|
||||
},
|
||||
"argala": {
|
||||
"name": "Argala",
|
||||
"domains": ["static", "intervention", "event"],
|
||||
"status": "covered",
|
||||
"knowledge_refs": ["references/argala-complete-guide.md"],
|
||||
"commands": ["argala", "full-reading"],
|
||||
"output_paths": ["modules.argala"],
|
||||
"audit_label": "Argala",
|
||||
"missing_impact": "Intervention/blocking layer missing."
|
||||
},
|
||||
"shadbala": {
|
||||
"name": "Shadbala",
|
||||
"domains": ["strength", "static"],
|
||||
"status": "covered",
|
||||
"knowledge_refs": ["references/shadbala-complete-methodology.md"],
|
||||
"commands": ["shadbala", "full-reading"],
|
||||
"output_paths": ["modules.shadbala"],
|
||||
"audit_label": "Shadbala",
|
||||
"missing_impact": "Planetary strength claims are not quantitatively anchored."
|
||||
},
|
||||
"ashtakavarga": {
|
||||
"name": "Ashtakavarga",
|
||||
"domains": ["transit", "strength", "timing"],
|
||||
"status": "covered",
|
||||
"knowledge_refs": ["references/ashtakavarga-complete-system.md"],
|
||||
"commands": ["ashtakavarga", "full-reading"],
|
||||
"output_paths": ["modules.ashtakavarga"],
|
||||
"audit_label": "Ashtakavarga",
|
||||
"missing_impact": "Transit strength filter missing."
|
||||
},
|
||||
"avastha": {
|
||||
"name": "Avastha",
|
||||
"domains": ["strength", "state"],
|
||||
"status": "covered",
|
||||
"knowledge_refs": ["references/technique-capability-matrix.md"],
|
||||
"commands": ["full-reading"],
|
||||
"output_paths": ["modules.avasthas"],
|
||||
"audit_label": "Avastha",
|
||||
"missing_impact": "Planet condition/state layer missing."
|
||||
},
|
||||
"vargottama": {
|
||||
"name": "Vargottama",
|
||||
"domains": ["varga", "strength"],
|
||||
"status": "covered",
|
||||
"knowledge_refs": ["references/varga-system-quick-reference.md"],
|
||||
"commands": ["full-reading"],
|
||||
"output_paths": ["modules.vargottama"],
|
||||
"audit_label": "Vargottama",
|
||||
"missing_impact": "D1-D9 consistency strength may be undercounted."
|
||||
},
|
||||
"pushkara": {
|
||||
"name": "Pushkara Navamsa / Pushkara Bhaga",
|
||||
"domains": ["varga", "dignity", "grace"],
|
||||
"status": "covered",
|
||||
"knowledge_refs": ["references/planetary-dignity-complete-reference.md", "references/navamsa-marriage-deep-analysis.md"],
|
||||
"commands": ["full-reading"],
|
||||
"output_paths": ["modules.pushkara"],
|
||||
"audit_label": "Pushkara",
|
||||
"missing_impact": "D9 grace/support layer may be undercounted."
|
||||
},
|
||||
"bhava_chalit": {
|
||||
"name": "Bhava Chalit",
|
||||
"domains": ["house_cusp", "event"],
|
||||
"status": "partial",
|
||||
"knowledge_refs": ["references/kp-astrology-complete-system.md"],
|
||||
"commands": ["chart", "full-reading"],
|
||||
"output_paths": ["modules.chart.houses"],
|
||||
"audit_label": "Bhava Chalit",
|
||||
"missing_impact": "Planet reassignment near cusps is not fully recalibrated.",
|
||||
"limitation": "House cusps exist, but full Chalit Chart planet reassignment is not implemented."
|
||||
},
|
||||
"sudarshana_chakra": {
|
||||
"name": "Sudarshana Chakra",
|
||||
"domains": ["multi_chart", "confirmation"],
|
||||
"status": "partial",
|
||||
"knowledge_refs": ["references/varga-system-quick-reference.md"],
|
||||
"commands": ["full-reading"],
|
||||
"output_paths": ["modules.varga_full", "modules.d9_navamsa_expanded"],
|
||||
"audit_label": "Sudarshana Chakra",
|
||||
"missing_impact": "Traditional triple-lagna confirmation is partial only.",
|
||||
"limitation": "D1-D9-D10 triangle verification exists as a substitute; traditional Sudarshana module is absent."
|
||||
}
|
||||
},
|
||||
"routes": {
|
||||
"career_timing_strict": {
|
||||
"trigger_keywords": ["事业", "职业", "工作机会", "项目落地", "名声", "社会身份"],
|
||||
"required_techniques": ["d1_rashi", "d9_navamsa", "d10_dasamsa", "vimshottari_dasha", "dasha_sandhi", "jaimini_chara_dasha", "karakamsha_ak_amk", "a10_karma_pada", "argala", "shadbala", "ashtakavarga", "vargottama", "pushkara"],
|
||||
"optional_techniques": ["bhava_chalit", "sudarshana_chakra"]
|
||||
},
|
||||
"event_verification_strict": {
|
||||
"trigger_keywords": ["验证", "回测", "历史事件", "可靠性", "是不是遗漏"],
|
||||
"required_techniques": ["d1_rashi", "vimshottari_dasha", "dasha_sandhi", "jaimini_chara_dasha", "argala", "shadbala", "ashtakavarga"],
|
||||
"optional_techniques": ["d9_navamsa", "d10_dasamsa", "a10_karma_pada", "bhava_chalit", "sudarshana_chakra"]
|
||||
},
|
||||
"full_reading_strict": {
|
||||
"trigger_keywords": ["综合解盘", "完整解盘", "full-reading"],
|
||||
"required_techniques": ["d1_rashi", "d9_navamsa", "d10_dasamsa", "vimshottari_dasha", "dasha_sandhi", "jaimini_chara_dasha", "argala", "shadbala", "ashtakavarga", "avastha", "vargottama", "pushkara"],
|
||||
"optional_techniques": ["bhava_chalit", "sudarshana_chakra"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Technique capability registry validator and audit generator.
|
||||
|
||||
This script deliberately uses only Python stdlib. Open-source research suggested
|
||||
Yamale/jsonschema-style validation for registries, but this skill avoids adding
|
||||
runtime dependencies beyond pyswisseph.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from typing import Any, Dict, List
|
||||
|
||||
ROOT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
DEFAULT_REGISTRY = os.path.join(ROOT_DIR, "references", "technique_registry.json")
|
||||
|
||||
ALLOWED_STATUS = {
|
||||
"covered",
|
||||
"partial",
|
||||
"knowledge-only",
|
||||
"workflow-only",
|
||||
"not-integrated",
|
||||
"missing",
|
||||
}
|
||||
REQUIRED_TECHNIQUE_FIELDS = {
|
||||
"name": str,
|
||||
"domains": list,
|
||||
"status": str,
|
||||
"knowledge_refs": list,
|
||||
"commands": list,
|
||||
"output_paths": list,
|
||||
"audit_label": str,
|
||||
"missing_impact": str,
|
||||
}
|
||||
|
||||
|
||||
def load_registry(path: str = DEFAULT_REGISTRY) -> Dict[str, Any]:
|
||||
with open(path, "r", encoding="utf-8") as f:
|
||||
return json.load(f)
|
||||
|
||||
|
||||
def _path_exists_relative(path: str) -> bool:
|
||||
if path.startswith("scripts/") and " " in path:
|
||||
path = path.split()[0]
|
||||
return os.path.exists(os.path.join(ROOT_DIR, path))
|
||||
|
||||
|
||||
def validate_registry(registry: Dict[str, Any]) -> Dict[str, Any]:
|
||||
problems: List[Dict[str, str]] = []
|
||||
warnings: List[Dict[str, str]] = []
|
||||
|
||||
techniques = registry.get("techniques")
|
||||
if not isinstance(techniques, dict) or not techniques:
|
||||
problems.append({"level": "error", "path": "techniques", "message": "techniques must be a non-empty object"})
|
||||
techniques = {}
|
||||
|
||||
for tech_id, tech in techniques.items():
|
||||
if not isinstance(tech, dict):
|
||||
problems.append({"level": "error", "path": tech_id, "message": "technique entry must be an object"})
|
||||
continue
|
||||
for field, expected_type in REQUIRED_TECHNIQUE_FIELDS.items():
|
||||
if field not in tech:
|
||||
problems.append({"level": "error", "path": f"techniques.{tech_id}.{field}", "message": "required field missing"})
|
||||
continue
|
||||
if not isinstance(tech[field], expected_type):
|
||||
problems.append({"level": "error", "path": f"techniques.{tech_id}.{field}", "message": f"must be {expected_type.__name__}"})
|
||||
status = tech.get("status")
|
||||
if status not in ALLOWED_STATUS:
|
||||
problems.append({"level": "error", "path": f"techniques.{tech_id}.status", "message": f"invalid status {status!r}"})
|
||||
if status == "covered" and not tech.get("output_paths"):
|
||||
problems.append({"level": "error", "path": f"techniques.{tech_id}.output_paths", "message": "covered techniques must expose output_paths"})
|
||||
if status == "partial" and not tech.get("limitation"):
|
||||
warnings.append({"level": "warning", "path": f"techniques.{tech_id}.limitation", "message": "partial techniques should explain limitation"})
|
||||
for ref in tech.get("knowledge_refs", []):
|
||||
if isinstance(ref, str) and not _path_exists_relative(ref):
|
||||
warnings.append({"level": "warning", "path": f"techniques.{tech_id}.knowledge_refs", "message": f"reference not found: {ref}"})
|
||||
|
||||
routes = registry.get("routes", {})
|
||||
if not isinstance(routes, dict):
|
||||
problems.append({"level": "error", "path": "routes", "message": "routes must be an object"})
|
||||
routes = {}
|
||||
for route_id, route in routes.items():
|
||||
if not isinstance(route, dict):
|
||||
problems.append({"level": "error", "path": f"routes.{route_id}", "message": "route entry must be an object"})
|
||||
continue
|
||||
required = route.get("required_techniques", [])
|
||||
optional = route.get("optional_techniques", [])
|
||||
if not isinstance(required, list):
|
||||
problems.append({"level": "error", "path": f"routes.{route_id}.required_techniques", "message": "must be a list"})
|
||||
required = []
|
||||
if not isinstance(optional, list):
|
||||
problems.append({"level": "error", "path": f"routes.{route_id}.optional_techniques", "message": "must be a list"})
|
||||
optional = []
|
||||
for tech_id in required + optional:
|
||||
if tech_id not in techniques:
|
||||
problems.append({"level": "error", "path": f"routes.{route_id}", "message": f"unknown technique id: {tech_id}"})
|
||||
|
||||
status_counts: Dict[str, int] = {s: 0 for s in sorted(ALLOWED_STATUS)}
|
||||
domain_counts: Dict[str, int] = {}
|
||||
for tech in techniques.values():
|
||||
status = tech.get("status")
|
||||
if status in status_counts:
|
||||
status_counts[status] += 1
|
||||
for domain in tech.get("domains", []):
|
||||
domain_counts[domain] = domain_counts.get(domain, 0) + 1
|
||||
|
||||
return {
|
||||
"valid": len(problems) == 0,
|
||||
"problem_count": len(problems),
|
||||
"warning_count": len(warnings),
|
||||
"status_counts": status_counts,
|
||||
"domain_counts": dict(sorted(domain_counts.items())),
|
||||
"problems": problems,
|
||||
"warnings": warnings,
|
||||
"routes": sorted(routes.keys()),
|
||||
"technique_count": len(techniques),
|
||||
}
|
||||
|
||||
|
||||
def build_audit_table(registry: Dict[str, Any], route_id: str | None = None) -> Dict[str, Any]:
|
||||
techniques = registry.get("techniques", {})
|
||||
route = registry.get("routes", {}).get(route_id, {}) if route_id else {}
|
||||
if route_id and not route:
|
||||
raise KeyError(f"Unknown route: {route_id}")
|
||||
|
||||
if route:
|
||||
ordered_ids = list(dict.fromkeys(route.get("required_techniques", []) + route.get("optional_techniques", [])))
|
||||
else:
|
||||
ordered_ids = sorted(techniques.keys())
|
||||
|
||||
rows = []
|
||||
for tech_id in ordered_ids:
|
||||
tech = techniques[tech_id]
|
||||
rows.append({
|
||||
"technique_id": tech_id,
|
||||
"audit_label": tech.get("audit_label"),
|
||||
"status": tech.get("status"),
|
||||
"commands": tech.get("commands", []),
|
||||
"output_paths": tech.get("output_paths", []),
|
||||
"missing_impact": tech.get("missing_impact"),
|
||||
"limitation": tech.get("limitation"),
|
||||
})
|
||||
return {"route": route_id or "all", "rows": rows}
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description="Validate Jyotish technique registry and emit audit data")
|
||||
parser.add_argument("--registry", default=DEFAULT_REGISTRY, help="Path to technique_registry.json")
|
||||
parser.add_argument("--route", default=None, help="Optional route id for audit table")
|
||||
parser.add_argument("--mode", choices=["validate", "table"], default="validate")
|
||||
args = parser.parse_args()
|
||||
|
||||
try:
|
||||
registry = load_registry(args.registry)
|
||||
if args.mode == "validate":
|
||||
result = validate_registry(registry)
|
||||
print(json.dumps(result, ensure_ascii=False, indent=2))
|
||||
return 0 if result["valid"] else 1
|
||||
result = build_audit_table(registry, args.route)
|
||||
print(json.dumps(result, ensure_ascii=False, indent=2))
|
||||
return 0
|
||||
except Exception as exc:
|
||||
print(json.dumps({"valid": False, "error": str(exc)}, ensure_ascii=False, indent=2), file=sys.stderr)
|
||||
return 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -3067,6 +3067,7 @@ def cmd_full_reading(args):
|
||||
return {"error": "swisseph未安装,无法计算星盘"}
|
||||
|
||||
report['chart'] = chart
|
||||
report['modules']['chart'] = chart
|
||||
planets = chart.get('planets', {})
|
||||
asc_deg = chart.get('ascendant', {}).get('degree', 0)
|
||||
asc_sign = chart.get('ascendant', {}).get('sign', 'Unknown')
|
||||
@@ -3752,6 +3753,12 @@ def main():
|
||||
_add_chart_args(p)
|
||||
p.add_argument('--transit-date', default=None, help='过境日期 YYYY-MM-DD(可选)')
|
||||
|
||||
# 28. audit-capabilities (v6.0.3新增)
|
||||
p = sub.add_parser('audit-capabilities', help='校验 technique registry 并输出能力覆盖审计')
|
||||
p.add_argument('--registry', default=None, help='technique_registry.json 路径(默认 references/technique_registry.json)')
|
||||
p.add_argument('--mode', default='validate', choices=['validate', 'table'], help='validate=校验注册表;table=输出路由审计表')
|
||||
p.add_argument('--route', default=None, help='table模式下的 route id,如 career_timing_strict')
|
||||
|
||||
args = parser.parse_args()
|
||||
if not args.command:
|
||||
parser.print_help(); sys.exit(1)
|
||||
@@ -3766,6 +3773,12 @@ def main():
|
||||
'double-transit-pac': cmd_double_transit_pac,
|
||||
'transit-ll7l': cmd_transit_ll7l, 'planetary-congregation': cmd_planetary_congregation,
|
||||
'vivah-saham': cmd_vivah_saham}
|
||||
if args.command == 'audit-capabilities':
|
||||
from audit_capabilities import build_audit_table, load_registry, validate_registry
|
||||
registry = load_registry(args.registry) if args.registry else load_registry()
|
||||
result = validate_registry(registry) if args.mode == 'validate' else build_audit_table(registry, args.route)
|
||||
output_json(result)
|
||||
sys.exit(0 if result.get('valid', True) else 1)
|
||||
result = cmds[args.command](args)
|
||||
output_json(result)
|
||||
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"version": "v1",
|
||||
"cases": [
|
||||
{
|
||||
"id": "smoke_full_reading_beijing_1990",
|
||||
"description": "A deterministic smoke case that ensures full-reading exposes the engineering-critical modules added in v6.0.2.",
|
||||
"input": {
|
||||
"year": 1990,
|
||||
"month": 1,
|
||||
"day": 1,
|
||||
"hour": 12,
|
||||
"minute": 0,
|
||||
"lat": 39.9,
|
||||
"lon": 116.4,
|
||||
"tz": 8,
|
||||
"today": "2026-06-03"
|
||||
},
|
||||
"expected_output_paths": [
|
||||
"modules.chart",
|
||||
"modules.dasha",
|
||||
"modules.special_lagnas.Arudha_Lagna",
|
||||
"modules.special_lagnas.A10_Karma_Pada",
|
||||
"modules.special_lagnas.Upapada_Lagna",
|
||||
"modules.varga_full.D9_Navamsa",
|
||||
"modules.vargottama",
|
||||
"modules.pushkara",
|
||||
"modules.dasha_sandhi",
|
||||
"modules.shadbala",
|
||||
"modules.ashtakavarga",
|
||||
"modules.validation"
|
||||
],
|
||||
"max_errors": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Golden case regression runner for Jyotish full-reading output contracts."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from typing import Any, Dict, List
|
||||
|
||||
ROOT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
ENGINE = os.path.join(ROOT_DIR, "scripts", "jyotish_engine.py")
|
||||
DEFAULT_CASES = os.path.join(ROOT_DIR, "tests", "golden", "golden_cases.json")
|
||||
|
||||
|
||||
def get_path(data: Dict[str, Any], dotted_path: str) -> Any:
|
||||
cur: Any = data
|
||||
for part in dotted_path.split("."):
|
||||
if not isinstance(cur, dict) or part not in cur:
|
||||
raise KeyError(dotted_path)
|
||||
cur = cur[part]
|
||||
return cur
|
||||
|
||||
|
||||
def run_full_reading(py: str, case: Dict[str, Any]) -> Dict[str, Any]:
|
||||
inp = case["input"]
|
||||
cmd = [
|
||||
py,
|
||||
ENGINE,
|
||||
"full-reading",
|
||||
"--year", str(inp["year"]),
|
||||
"--month", str(inp["month"]),
|
||||
"--day", str(inp["day"]),
|
||||
"--hour", str(inp["hour"]),
|
||||
"--minute", str(inp["minute"]),
|
||||
"--lat", str(inp["lat"]),
|
||||
"--lon", str(inp["lon"]),
|
||||
"--tz", str(inp["tz"]),
|
||||
]
|
||||
if inp.get("today"):
|
||||
cmd.extend(["--today", inp["today"]])
|
||||
out = subprocess.check_output(cmd, text=True)
|
||||
return json.loads(out)
|
||||
|
||||
|
||||
def run_cases(py: str, cases_path: str) -> Dict[str, Any]:
|
||||
with open(cases_path, "r", encoding="utf-8") as f:
|
||||
suite = json.load(f)
|
||||
results: List[Dict[str, Any]] = []
|
||||
for case in suite.get("cases", []):
|
||||
case_result = {"id": case.get("id"), "passed": True, "failures": []}
|
||||
try:
|
||||
data = run_full_reading(py, case)
|
||||
errors = data.get("errors", [])
|
||||
if len(errors) > case.get("max_errors", 0):
|
||||
case_result["passed"] = False
|
||||
case_result["failures"].append(f"too many errors: {len(errors)} > {case.get('max_errors', 0)}; {errors[:5]}")
|
||||
for path in case.get("expected_output_paths", []):
|
||||
try:
|
||||
value = get_path(data, path)
|
||||
if value is None or value == {} or value == []:
|
||||
case_result["passed"] = False
|
||||
case_result["failures"].append(f"empty output path: {path}")
|
||||
except KeyError:
|
||||
case_result["passed"] = False
|
||||
case_result["failures"].append(f"missing output path: {path}")
|
||||
except Exception as exc:
|
||||
case_result["passed"] = False
|
||||
case_result["failures"].append(str(exc))
|
||||
results.append(case_result)
|
||||
failed = [r for r in results if not r["passed"]]
|
||||
return {"valid": len(failed) == 0, "total": len(results), "failed": len(failed), "results": results}
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description="Run Jyotish golden case regressions")
|
||||
parser.add_argument("--python", default=sys.executable, help="Python executable used to run jyotish_engine.py")
|
||||
parser.add_argument("--cases", default=DEFAULT_CASES, help="Path to golden_cases.json")
|
||||
args = parser.parse_args()
|
||||
result = run_cases(args.python, args.cases)
|
||||
print(json.dumps(result, ensure_ascii=False, indent=2))
|
||||
return 0 if result["valid"] else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
+2
-2
@@ -247,8 +247,8 @@ class TestYogaDetection(unittest.TestCase):
|
||||
|
||||
def test_no_yoga_false_positive(self):
|
||||
"""测试不满足条件时不产生 Yoga"""
|
||||
# 行星分散,不满足任何 Yoga
|
||||
args = self._make_yoga_args('Aries', 'Sun:Taurus:2,Moon:Gemini:3')
|
||||
# 行星分散,且避免落入 Sun/Moon 两侧或扩展型简化 Yoga 条件
|
||||
args = self._make_yoga_args('Aries', 'Sun:Taurus:2,Mars:Leo:5')
|
||||
result = cmd_yoga(args)
|
||||
yogas = result.get('yogas', [])
|
||||
self.assertEqual(len(yogas), 0, "Should not detect any Yoga with scattered planets")
|
||||
|
||||
Reference in New Issue
Block a user