chore: add engineering quality gate
This commit is contained in:
@@ -19,16 +19,10 @@ jobs:
|
||||
python-version: '3.11'
|
||||
|
||||
- name: Install dependencies
|
||||
run: python -m pip install --upgrade pip && pip install -r requirements.txt
|
||||
run: python -m pip install --upgrade pip && pip install -r requirements.txt && pip install -r requirements-dev.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: Run Ruff lint for v6.0.33 quality gate files
|
||||
run: ruff check scripts/run_quality_gate.py tests/test_varga_bphs.py tests/test_ashtakavarga_invariants.py tests/test_cli_smoke.py tests/test_yoga_rules_integrity.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
|
||||
- name: Run quality gate
|
||||
run: python scripts/run_quality_gate.py --skip-yoga-logic
|
||||
|
||||
@@ -11,3 +11,9 @@ build/
|
||||
brain/
|
||||
COVERAGE_AUDIT_REPORT.md
|
||||
*.tmp
|
||||
.coverage
|
||||
coverage.xml
|
||||
htmlcov/
|
||||
.ruff_cache/
|
||||
.mypy_cache/
|
||||
.hypothesis/
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
repos:
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.8.6
|
||||
hooks:
|
||||
- id: ruff
|
||||
args: ["--fix"]
|
||||
- id: ruff-format
|
||||
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v5.0.0
|
||||
hooks:
|
||||
- id: check-yaml
|
||||
- id: check-toml
|
||||
- id: check-json
|
||||
- id: end-of-file-fixer
|
||||
- id: trailing-whitespace
|
||||
exclude: "^(references/|CHANGELOG.md|SKILL.md)"
|
||||
@@ -1,5 +1,38 @@
|
||||
# 印度占星 Skill 更新日志
|
||||
|
||||
## v6.0.33(2026-06-05)—— 工程质量门禁、属性测试与 CI 强化
|
||||
|
||||
> **目标**:把 v6.0.32 的准确率修复沉淀为可持续工程体系,防止 Dasha、Varga、Ashtakavarga、Yoga 规则与 full-reading 输出契约回归。
|
||||
|
||||
### 工程配置
|
||||
|
||||
- 新增 `pyproject.toml`,统一项目元数据、pytest 配置、coverage 配置与 Ruff 静态检查规则。
|
||||
- 新增 `requirements-dev.txt`,将 `pytest`、`hypothesis`、`coverage`、`ruff` 作为开发/CI 依赖独立管理。
|
||||
- 新增 `.pre-commit-config.yaml`,接入 Ruff 与基础 YAML/TOML/JSON/换行/尾随空格检查。
|
||||
- 更新 `.gitignore`,忽略 coverage、Ruff、Hypothesis 等本地质量工具缓存。
|
||||
|
||||
### 自动化测试与质量门禁
|
||||
|
||||
- 新增 `scripts/run_quality_gate.py`,统一执行核心 Python 编译、关键 JSON 校验、technique registry 校验、BPHS 不变量验证、pytest 与 golden case 回归。
|
||||
- 新增 `tests/test_varga_bphs.py`:用 Hypothesis 属性测试 D9 Navamsa、D10 Dasamsa、D3 Drekkana 的 BPHS 映射不变量。
|
||||
- 新增 `tests/test_ashtakavarga_invariants.py`:用属性测试证明 BAV 固定总分、SAV=337、full SAV=386 与输出契约不随行星位置变化。
|
||||
- 新增 `tests/test_cli_smoke.py`:覆盖 `dasha`、`varga`、`ashtakavarga`、`audit-capabilities` CLI smoke tests。
|
||||
- 新增 `tests/test_yoga_rules_integrity.py`:校验 Yoga 规则 ID 唯一性、规则计数、低频 Yoga 高置信规则存在,以及逻辑验证报告结构。
|
||||
- 更新 GitHub Actions:CI 安装开发依赖,执行 Ruff 和统一质量门禁。
|
||||
|
||||
### 精度边界修复
|
||||
|
||||
- 修复 `scripts/varga.py` 中分盘度数在星座边界处可能由 `29.999999...` 四舍五入为 `30.0000` 的显示问题;现在输出强制保持在 `[0, 30)`,避免下游范围不变量误报。
|
||||
- 调整旧单测 `test_no_yoga_false_positive` 的断言范围:v6.0.32 后规则库已扩大到数百条,极简星盘可能触发其他专项 Yoga;该测试现在只约束原始目标——不误报 Raja Yoga。
|
||||
|
||||
### 当前验证结果
|
||||
|
||||
- `pytest tests`:**35 passed**。
|
||||
- `scripts/run_quality_gate.py --skip-yoga-logic`:通过;包含 18 项 BPHS/Ashtakavarga 不变量、technique registry 校验、pytest 与 golden case 回归。
|
||||
- 新增代码 Ruff 检查:通过。
|
||||
|
||||
---
|
||||
|
||||
## v6.0.32(2026-06-05)—— 逻辑正确性验证框架、Dasha/Varga/Ashtakavarga 修复
|
||||
|
||||
> **目标**:从“名称覆盖率”推进到“逻辑正确性验证”,并完成 Dasha、分盘与 Ashtakavarga 的核心准确率门禁。
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: jyotish-vedic-astrology
|
||||
version: 6.0.32
|
||||
version: 6.0.33
|
||||
description: 印度占星(Jyotish)专业解盘与推运系统。核心能力:PDF星盘输入→严谨解盘→精确推运应期输出。触发词:印度占星、吠陀占星、Jyotish、解盘、推运、星盘分析、Dasha、Transit、Nakshatra、Yoga、出生时间矫正、PDF星盘、读取PDF、分析PDF星盘、现代解读、误判纠错、Varga分盘、综合分析、过境分析、合盘、婚姻匹配、年运盘、Prashna、Argala、Jaimini、Shadbala、Ashtakavarga、HTML报告、深度解盘。
|
||||
---
|
||||
|
||||
@@ -10,7 +10,7 @@ description: 印度占星(Jyotish)专业解盘与推运系统。核心能力
|
||||
> **严格路由**:`references/strict-workflow-router.md`(⭐涉及事业/婚恋/财务/应期/技法验证时必须优先读取)
|
||||
> **覆盖矩阵**:`references/technique-capability-matrix.md`(⭐判断技法 covered/partial/missing 时必须参考)
|
||||
> **机器注册表**:`references/technique_registry.json` + `scripts/audit_capabilities.py`(⭐用于自动审计与CI门禁)
|
||||
> **版本**:v6.0.32-logic-validation-varga-fix | **详细变更**:`CHANGELOG.md`
|
||||
> **版本**:v6.0.33-engineering-quality-gate | **详细变更**:`CHANGELOG.md`
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
[project]
|
||||
name = "jyotish-vedic-astrology-skill"
|
||||
version = "6.0.33"
|
||||
description = "Jyotish WorkBuddy skill calculation engine and validation suite"
|
||||
requires-python = ">=3.11"
|
||||
dependencies = [
|
||||
"pyswisseph",
|
||||
]
|
||||
|
||||
[project.optional-dependencies]
|
||||
dev = [
|
||||
"pytest>=8.0",
|
||||
"hypothesis>=6.0",
|
||||
"coverage[toml]>=7.0",
|
||||
"ruff>=0.6.0",
|
||||
]
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
minversion = "8.0"
|
||||
testpaths = ["tests"]
|
||||
pythonpath = ["scripts"]
|
||||
addopts = "-q --strict-markers --strict-config"
|
||||
markers = [
|
||||
"slow: long-running validation tests",
|
||||
"external: tests that depend on optional external datasets or tools",
|
||||
]
|
||||
|
||||
[tool.coverage.run]
|
||||
branch = true
|
||||
source = ["scripts"]
|
||||
omit = [
|
||||
"scripts/add_high_confidence_yogas_batch1.py",
|
||||
"scripts/build_standard_test_charts.py",
|
||||
"scripts/_compute_one_chart.py",
|
||||
]
|
||||
|
||||
[tool.coverage.report]
|
||||
show_missing = true
|
||||
skip_covered = true
|
||||
|
||||
[tool.ruff]
|
||||
target-version = "py311"
|
||||
line-length = 140
|
||||
exclude = [
|
||||
".git",
|
||||
"__pycache__",
|
||||
".pytest_cache",
|
||||
"jyotish-app",
|
||||
"benchmark",
|
||||
]
|
||||
|
||||
[tool.ruff.lint]
|
||||
select = ["E", "F", "I", "UP", "B", "SIM"]
|
||||
ignore = [
|
||||
"E501",
|
||||
"E701",
|
||||
"E702",
|
||||
"E741",
|
||||
]
|
||||
|
||||
[tool.ruff.lint.per-file-ignores]
|
||||
"tests/**" = ["S101"]
|
||||
"scripts/validate_bphs_invariants.py" = ["T201"]
|
||||
@@ -0,0 +1,5 @@
|
||||
# 开发与 CI 依赖
|
||||
pytest>=8.0
|
||||
hypothesis>=6.0
|
||||
coverage[toml]>=7.0
|
||||
ruff>=0.6.0
|
||||
@@ -0,0 +1,84 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Run the Jyotish skill quality gate used by local development and CI."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import py_compile
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
PYTHON = sys.executable
|
||||
|
||||
COMPILE_TARGETS = [
|
||||
ROOT / "scripts" / "jyotish_engine.py",
|
||||
ROOT / "scripts" / "varga.py",
|
||||
ROOT / "scripts" / "ashtakavarga.py",
|
||||
ROOT / "scripts" / "yoga_engine.py",
|
||||
ROOT / "scripts" / "audit_capabilities.py",
|
||||
ROOT / "scripts" / "validate_bphs_invariants.py",
|
||||
ROOT / "tests" / "run_golden_cases.py",
|
||||
]
|
||||
|
||||
|
||||
def run(cmd: list[str], *, optional: bool = False) -> bool:
|
||||
print(f"\n$ {' '.join(cmd)}")
|
||||
completed = subprocess.run(cmd, cwd=ROOT, text=True)
|
||||
if completed.returncode == 0:
|
||||
return True
|
||||
if optional:
|
||||
print(f"Optional step failed with exit code {completed.returncode}; continuing.")
|
||||
return False
|
||||
raise SystemExit(completed.returncode)
|
||||
|
||||
|
||||
def compile_targets() -> None:
|
||||
print("\n== Compile core Python files ==")
|
||||
for target in COMPILE_TARGETS:
|
||||
print(f"compile {target.relative_to(ROOT)}")
|
||||
py_compile.compile(str(target), doraise=True)
|
||||
|
||||
|
||||
def validate_json_files() -> None:
|
||||
print("\n== Validate critical JSON files ==")
|
||||
for relative in [
|
||||
"references/technique_registry.json",
|
||||
"references/yoga_rules.json",
|
||||
"references/standard_test_charts.json",
|
||||
"references/validation_logic_report.json",
|
||||
"tests/golden/golden_cases.json",
|
||||
]:
|
||||
path = ROOT / relative
|
||||
if not path.exists():
|
||||
continue
|
||||
with path.open("r", encoding="utf-8") as handle:
|
||||
json.load(handle)
|
||||
print(f"json ok {relative}")
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description="Run Jyotish skill quality gate")
|
||||
parser.add_argument("--skip-slow", action="store_true", help="Skip slow golden-case regressions")
|
||||
parser.add_argument("--skip-yoga-logic", action="store_true", help="Skip Yoga logic comparison report refresh")
|
||||
args = parser.parse_args()
|
||||
|
||||
os.environ.setdefault("PYTHONPATH", str(ROOT / "scripts"))
|
||||
compile_targets()
|
||||
validate_json_files()
|
||||
run([PYTHON, "scripts/audit_capabilities.py", "--mode", "validate"])
|
||||
run([PYTHON, "scripts/validate_bphs_invariants.py"])
|
||||
run([PYTHON, "-m", "pytest", "tests"])
|
||||
if not args.skip_slow:
|
||||
run([PYTHON, "tests/run_golden_cases.py", "--python", PYTHON])
|
||||
if not args.skip_yoga_logic:
|
||||
run([PYTHON, "scripts/validate_logic_v2.py"], optional=True)
|
||||
print("\nQuality gate passed.")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
+7
-1
@@ -78,8 +78,14 @@ def calc_varga(lon, div):
|
||||
si=_si(lon); d=lon-si*30; ps=30.0/div; pi=int(d/ps)
|
||||
# For all vargas, degree within divisional sign is scaled to 0-30 degrees.
|
||||
dp=(d-pi*ps)*div
|
||||
# Keep the displayed divisional degree inside [0, 30). Values such as
|
||||
# 29.9999997 can round to 30.0000 at sign boundaries, which breaks
|
||||
# downstream range invariants while the underlying sign mapping is valid.
|
||||
dp_display = round(dp, 4)
|
||||
if dp_display >= 30:
|
||||
dp_display = 0.0
|
||||
vsi=varga_map(si,pi,div)
|
||||
r={'sign':_sn(vsi),'sign_idx':vsi,'degree_in_sign':round(dp,4),
|
||||
r={'sign':_sn(vsi),'sign_idx':vsi,'degree_in_sign':dp_display,
|
||||
'part_index':pi,'lord':SIGN_LORDS.get(_sn(vsi),'')}
|
||||
if div==9: r['pada']=pi+1
|
||||
return r
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Ashtakavarga mathematical invariant tests."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from ashtakavarga import ALL_SOURCES, BAV_TOTALS, EXPECTED_SAV_TOTAL, SEVEN_PLANETS, SIGNS, calc_ashtakavarga
|
||||
from hypothesis import given
|
||||
from hypothesis import strategies as st
|
||||
|
||||
|
||||
@st.composite
|
||||
def planet_signs(draw: st.DrawFn) -> dict[str, dict[str, str]]:
|
||||
return {planet: {"sign": draw(st.sampled_from(SIGNS))} for planet in SEVEN_PLANETS}
|
||||
|
||||
|
||||
@given(planet_signs(), st.integers(min_value=0, max_value=11))
|
||||
def test_ashtakavarga_totals_are_position_independent(planets: dict[str, dict[str, str]], asc_sign_idx: int) -> None:
|
||||
result = calc_ashtakavarga(planets, asc_sign_idx)
|
||||
|
||||
assert result["sav"]["total"] == EXPECTED_SAV_TOTAL
|
||||
assert result["sav"]["valid"] is True
|
||||
assert result["sav"]["full_total_with_lagna"] == EXPECTED_SAV_TOTAL + BAV_TOTALS["Lagna"]
|
||||
assert result["all_bav_valid"] is True
|
||||
|
||||
for planet in ALL_SOURCES:
|
||||
assert result["bav"][planet]["total"] == BAV_TOTALS[planet]
|
||||
assert result["bav"][planet]["valid"] is True
|
||||
|
||||
|
||||
def test_ashtakavarga_output_contract() -> None:
|
||||
planets = {planet: {"sign": SIGNS[index % 12]} for index, planet in enumerate(SEVEN_PLANETS)}
|
||||
result = calc_ashtakavarga(planets, asc_sign_idx=0)
|
||||
|
||||
assert result["method"] == "Ashtakavarga八分法(BPHS/PVR书例校准v2.1)"
|
||||
assert result["version"] == "2.1"
|
||||
assert set(result["sav"]["scores"].keys()) == set(SIGNS)
|
||||
assert len(result["house_scores"]) == 12
|
||||
assert len(result["house_scores_full"]) == 12
|
||||
@@ -0,0 +1,60 @@
|
||||
#!/usr/bin/env python3
|
||||
"""CLI smoke tests for critical Jyotish engine commands."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
ENGINE = ROOT / "scripts" / "jyotish_engine.py"
|
||||
BASE_BIRTH_ARGS = [
|
||||
"--year", "1990",
|
||||
"--month", "1",
|
||||
"--day", "1",
|
||||
"--hour", "12",
|
||||
"--minute", "0",
|
||||
"--lat", "39.9",
|
||||
"--lon", "116.4",
|
||||
"--tz", "8",
|
||||
]
|
||||
|
||||
|
||||
def run_engine(*args: str) -> dict:
|
||||
completed = subprocess.run(
|
||||
[sys.executable, str(ENGINE), *args],
|
||||
cwd=ROOT,
|
||||
check=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
return json.loads(completed.stdout)
|
||||
|
||||
|
||||
def test_dasha_accepts_birth_datetime_without_explicit_nakshatra() -> None:
|
||||
result = run_engine("dasha", *BASE_BIRTH_ARGS, "--today", "2026-01-01")
|
||||
assert "moon_nakshatra" in result
|
||||
assert len(result.get("timeline", [])) == 9
|
||||
assert result["timeline"][0]["is_balance"] is True
|
||||
|
||||
|
||||
def test_varga_cli_outputs_d9_and_d10() -> None:
|
||||
result = run_engine("varga", *BASE_BIRTH_ARGS, "--d9", "--d10")
|
||||
charts = result.get("divisional_charts", {})
|
||||
assert "D9_Navamsa" in charts
|
||||
assert "D10_Dasamsa" in charts
|
||||
assert "Moon" in charts["D9_Navamsa"]
|
||||
|
||||
|
||||
def test_ashtakavarga_cli_keeps_sav_invariant() -> None:
|
||||
result = run_engine("ashtakavarga", *BASE_BIRTH_ARGS)
|
||||
assert result["sav"]["total"] == 337
|
||||
assert result["sav"]["valid"] is True
|
||||
assert result["all_bav_valid"] is True
|
||||
|
||||
|
||||
def test_audit_capabilities_cli_validates_registry() -> None:
|
||||
result = run_engine("audit-capabilities", "--mode", "validate")
|
||||
assert result.get("valid") is True
|
||||
+6
-4
@@ -245,13 +245,15 @@ class TestYogaDetection(unittest.TestCase):
|
||||
raja = [y for y in yogas if y['name'] == 'Raja Yoga']
|
||||
self.assertTrue(len(raja) > 0, "Should detect Raja Yoga when Sun+Jupiter in 1st house")
|
||||
|
||||
def test_no_yoga_false_positive(self):
|
||||
"""测试不满足条件时不产生 Yoga"""
|
||||
# 行星分散,且避免落入 Sun/Moon 两侧或扩展型简化 Yoga 条件
|
||||
def test_scattered_planets_do_not_trigger_raja_yoga(self):
|
||||
"""分散行星不应触发 Raja Yoga。"""
|
||||
# v6.0.32 起规则库扩大到数百条,极简星盘可能触发其他低阶/专项 Yoga;
|
||||
# 此回归测试只约束原始目标:不应误报 Raja 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")
|
||||
raja = [y for y in yogas if y['name'] == 'Raja Yoga']
|
||||
self.assertEqual(len(raja), 0, "Should not detect Raja Yoga with scattered planets")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Regression tests for BPHS divisional chart mappings."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from hypothesis import given
|
||||
from hypothesis import strategies as st
|
||||
from varga import SIGNS, calc_varga, varga_map
|
||||
|
||||
|
||||
def navamsa_ref(lon: float) -> int:
|
||||
sign_index = int((lon % 360) / 30) % 12
|
||||
degree_in_sign = (lon % 360) - sign_index * 30
|
||||
part_index = int(degree_in_sign / (30 / 9))
|
||||
if sign_index % 3 == 0:
|
||||
start = sign_index
|
||||
elif sign_index % 3 == 1:
|
||||
start = (sign_index + 4) % 12
|
||||
else:
|
||||
start = (sign_index + 8) % 12
|
||||
return (start + part_index) % 12
|
||||
|
||||
|
||||
def dasamsa_ref(lon: float) -> int:
|
||||
sign_index = int((lon % 360) / 30) % 12
|
||||
degree_in_sign = (lon % 360) - sign_index * 30
|
||||
part_index = int(degree_in_sign / 3)
|
||||
start = sign_index if sign_index % 2 == 0 else (sign_index + 8) % 12
|
||||
return (start + part_index) % 12
|
||||
|
||||
|
||||
def drekkana_ref(lon: float) -> int:
|
||||
sign_index = int((lon % 360) / 30) % 12
|
||||
degree_in_sign = (lon % 360) - sign_index * 30
|
||||
part_index = int(degree_in_sign / 10)
|
||||
return (sign_index + part_index * 4) % 12
|
||||
|
||||
|
||||
@given(st.floats(min_value=0, max_value=359.999999, allow_nan=False, allow_infinity=False))
|
||||
def test_navamsa_matches_bphs_reference(lon: float) -> None:
|
||||
result = calc_varga(lon, 9)
|
||||
assert result["sign_idx"] == navamsa_ref(lon)
|
||||
assert result["sign"] == SIGNS[navamsa_ref(lon)]
|
||||
assert 0 <= result["degree_in_sign"] < 30
|
||||
|
||||
|
||||
@given(st.floats(min_value=0, max_value=359.999999, allow_nan=False, allow_infinity=False))
|
||||
def test_dasamsa_matches_bphs_reference(lon: float) -> None:
|
||||
result = calc_varga(lon, 10)
|
||||
assert result["sign_idx"] == dasamsa_ref(lon)
|
||||
assert result["sign"] == SIGNS[dasamsa_ref(lon)]
|
||||
assert 0 <= result["degree_in_sign"] < 30
|
||||
|
||||
|
||||
@given(st.floats(min_value=0, max_value=359.999999, allow_nan=False, allow_infinity=False))
|
||||
def test_drekkana_uses_same_plus_four_plus_eight(lon: float) -> None:
|
||||
result = calc_varga(lon, 3)
|
||||
assert result["sign_idx"] == drekkana_ref(lon)
|
||||
|
||||
|
||||
def test_varga_map_boundary_examples() -> None:
|
||||
assert varga_map(0, 0, 9) == 0 # Aries Navamsa starts Aries
|
||||
assert varga_map(1, 0, 9) == 5 # Taurus Navamsa starts Virgo
|
||||
assert varga_map(2, 0, 9) == 10 # Gemini Navamsa starts Aquarius
|
||||
assert varga_map(1, 0, 10) == 9 # Taurus Dasamsa starts Capricorn
|
||||
assert varga_map(0, 2, 3) == 8 # Aries third Drekkana = Sagittarius
|
||||
@@ -0,0 +1,45 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Integrity checks for the data-driven Yoga rules registry."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
YOGA_RULES = ROOT / "references" / "yoga_rules.json"
|
||||
LOGIC_REPORT = ROOT / "references" / "validation_logic_report.json"
|
||||
|
||||
|
||||
def test_yoga_rule_ids_are_unique_and_counted() -> None:
|
||||
data = json.loads(YOGA_RULES.read_text(encoding="utf-8"))
|
||||
rules = data.get("rules", [])
|
||||
rule_ids = [rule.get("id") for rule in rules]
|
||||
|
||||
assert len(rule_ids) == len(set(rule_ids))
|
||||
assert data.get("total_rules") == len(rules)
|
||||
assert data.get("total_enabled_rules") == sum(1 for rule in rules if rule.get("enabled", True))
|
||||
|
||||
|
||||
def test_new_low_frequency_yogas_exist_and_are_enabled() -> None:
|
||||
data = json.loads(YOGA_RULES.read_text(encoding="utf-8"))
|
||||
rules_by_id = {rule.get("id"): rule for rule in data.get("rules", [])}
|
||||
|
||||
for rule_id in [
|
||||
"bvr_106_indra_yoga",
|
||||
"bvr_107_makuta_yoga",
|
||||
"bvr_108_jaya_yoga",
|
||||
"bvr_109_putra_malika_yoga",
|
||||
]:
|
||||
assert rule_id in rules_by_id
|
||||
assert rules_by_id[rule_id].get("enabled", True) is True
|
||||
|
||||
|
||||
def test_logic_validation_report_contract() -> None:
|
||||
report = json.loads(LOGIC_REPORT.read_text(encoding="utf-8"))
|
||||
|
||||
assert report["summary"]["charts_tested"] >= 60
|
||||
assert report["summary"]["comparable_rules"] >= 70
|
||||
assert "precision" in report["summary"]
|
||||
assert "recall" in report["summary"]
|
||||
assert "f1" in report["summary"]
|
||||
Reference in New Issue
Block a user