Add real case revalidation gate

This commit is contained in:
732642856
2026-06-24 18:42:09 +08:00
parent 3db4ad6a27
commit bff6ec4685
4 changed files with 266 additions and 2 deletions
+7 -1
View File
@@ -79,7 +79,13 @@ This is a **Vedic (Jyotish) astrology analysis system** designed for deep, audit
- quick:快速开发守门,适合普通代码/文案修改后先跑:`python3 scripts/run_quality_gate.py --profile quick`
- browser:完整浏览器守门,覆盖 runtime smoke 与真实浏览器用户路径:`python3 scripts/run_quality_gate.py --profile browser`
- release:发布前守门,包含关键产品文件未跟踪检查、慢速 golden cases 与 Yoga 逻辑报告:`python3 scripts/run_quality_gate.py --profile release`
- release:发布前守门,包含关键产品文件未跟踪检查、慢速 golden cases、真实案例复验与 Yoga 逻辑报告:`python3 scripts/run_quality_gate.py --profile release`
### 真实案例复验与准确率边界
公开人物样本复验命令:`python3 tests/run_real_case_revalidation.py`
当前复验口径是公开人物样本的出生盘星座级一致率,并对部分带有来源矛盾、时区争议或边界度数的参考行标记为 controversial_reference。这个指标用于验证排盘计算是否稳定,不等同于人生事件预测准确率,也不应被当作个人命运判断的命中率。
### Prerequisites
+8 -1
View File
@@ -25,6 +25,7 @@ EXTRA_COMPILE_TARGETS = [
ROOT / "scripts" / "audit_fragments.py",
ROOT / "scripts" / "deployment_preflight.py",
ROOT / "tests" / "run_golden_cases.py",
ROOT / "tests" / "run_real_case_revalidation.py",
ROOT / "tests" / "run_frontend_runtime_smoke.py",
]
@@ -77,6 +78,7 @@ QUALITY_GATE_PROFILES = {
"skip_frontend_click": True,
"frontend_click_mode": "core",
"check_release_hygiene": False,
"skip_real_cases": True,
},
"browser": {
"skip_slow": True,
@@ -85,6 +87,7 @@ QUALITY_GATE_PROFILES = {
"skip_frontend_click": False,
"frontend_click_mode": "all",
"check_release_hygiene": False,
"skip_real_cases": True,
},
"release": {
"skip_slow": False,
@@ -93,6 +96,7 @@ QUALITY_GATE_PROFILES = {
"skip_frontend_click": False,
"frontend_click_mode": "all",
"check_release_hygiene": True,
"skip_real_cases": False,
},
}
@@ -252,7 +256,7 @@ def validate_json_files() -> None:
def run_profile(args: argparse.Namespace) -> dict:
profile = dict(QUALITY_GATE_PROFILES[args.profile])
for key in ["skip_slow", "skip_yoga_logic", "skip_frontend_runtime", "skip_frontend_click"]:
for key in ["skip_slow", "skip_yoga_logic", "skip_frontend_runtime", "skip_frontend_click", "skip_real_cases"]:
if getattr(args, key):
profile[key] = True
if args.frontend_click_mode:
@@ -267,6 +271,7 @@ def main() -> int:
parser.add_argument("--skip-yoga-logic", action="store_true", help="Skip Yoga logic comparison report refresh")
parser.add_argument("--skip-frontend-runtime", action="store_true", help="Skip frontend build and runtime smoke")
parser.add_argument("--skip-frontend-click", action="store_true", help="Skip browser click smoke")
parser.add_argument("--skip-real-cases", action="store_true", help="Skip public real-person chart revalidation")
parser.add_argument("--frontend-click-mode", choices=["core", "mobile", "offline", "pdf", "workspace", "mobile-trust", "import-files", "all"], default=None, help="Browser click smoke mode for browser/release profiles")
parser.add_argument("--frontend-click-timeout", type=int, default=240, help="Timeout seconds for browser click smoke")
parser.add_argument("--all-tests", action="store_true", help="Run every pytest file, including optional-dependency suites")
@@ -300,6 +305,8 @@ def main() -> int:
])
if not profile["skip_slow"]:
run([PYTHON, "tests/run_golden_cases.py", "--python", PYTHON])
if not profile["skip_real_cases"]:
run([PYTHON, "tests/run_real_case_revalidation.py", "--python", PYTHON, "--summary"])
if not profile["skip_yoga_logic"]:
run([PYTHON, "scripts/validate_logic_v2.py"], optional=True)
print("\nQuality gate passed.")
+218
View File
@@ -0,0 +1,218 @@
#!/usr/bin/env python3
"""Revalidate public real-person chart fixtures against the local engine.
This is a chart-calculation regression gate, not an event_prediction_accuracy
claim. It checks sign-level and optional degree-level agreement for public
reference cases, while keeping known controversial_reference rows visible in
the JSON report.
"""
from __future__ import annotations
import argparse
import json
import subprocess
import sys
from pathlib import Path
from typing import Any
ROOT = Path(__file__).resolve().parents[1]
ENGINE = ROOT / "scripts" / "jyotish_engine.py"
PUBLIC_CASES_RELATIVE = "tests/celebrity_cases.json"
INDASTRO_CASES_RELATIVE = "tests/indastro_cases.json"
PUBLIC_CASES = ROOT / PUBLIC_CASES_RELATIVE
INDASTRO_CASES = ROOT / INDASTRO_CASES_RELATIVE
DEFAULT_MIN_PASS_RATE = 0.98
DEFAULT_DEGREE_TOLERANCE = 1.0
CONTROVERSIAL_HINTS = [
"内部矛盾",
"需进一步验证",
"存在重大偏差",
"存在约",
"边界案例",
]
def load_cases(path: Path) -> list[dict[str, Any]]:
return json.loads(path.read_text(encoding="utf-8"))
def run_engine(case: dict[str, Any], python: str) -> dict[str, Any]:
cmd = [
python,
str(ENGINE),
"chart",
"--year",
str(case["year"]),
"--month",
str(case["month"]),
"--day",
str(case["day"]),
"--hour",
str(case["hour"]),
"--minute",
str(case["minute"]),
"--lat",
str(case["lat"]),
"--lon",
str(case["lon"]),
"--tz",
str(case["tz"]),
]
completed = subprocess.run(
cmd,
cwd=ROOT,
text=True,
capture_output=True,
timeout=45,
check=False,
)
if completed.returncode != 0:
raise RuntimeError(completed.stderr.strip() or completed.stdout.strip())
return json.loads(completed.stdout)
def is_controversial(case: dict[str, Any]) -> bool:
note = case.get("tz_note", "")
return any(hint in note for hint in CONTROVERSIAL_HINTS)
def add_check(
checks: list[dict[str, Any]],
label: str,
expected: Any,
actual: Any,
*,
gated: bool,
tolerance: float | None = None,
) -> None:
if expected in (None, ""):
return
if tolerance is None:
passed = actual == expected
else:
passed = actual is not None and abs(float(actual) - float(expected)) <= tolerance
checks.append(
{
"label": label,
"expected": expected,
"actual": actual,
"passed": passed,
"gated": gated,
}
)
def validate_public_case(case: dict[str, Any], chart: dict[str, Any]) -> dict[str, Any]:
checks: list[dict[str, Any]] = []
planets = chart.get("planets", {})
add_check(checks, "lagna_sign", case.get("known_lagna"), chart.get("ascendant", {}).get("sign"), gated=True)
add_check(checks, "sun_sign", case.get("known_sun_sign"), planets.get("Sun", {}).get("sign"), gated=True)
add_check(checks, "moon_sign", case.get("known_moon_sign"), planets.get("Moon", {}).get("sign"), gated=True)
return summarize_case(case, "public_reference", checks)
def validate_indastro_case(case: dict[str, Any], chart: dict[str, Any], tolerance: float) -> dict[str, Any]:
checks: list[dict[str, Any]] = []
planets = chart.get("planets", {})
gated = not is_controversial(case)
category = "controversial_reference" if not gated else "public_reference"
asc = chart.get("ascendant", {})
sun = planets.get("Sun", {})
moon = planets.get("Moon", {})
add_check(checks, "lagna_sign", case.get("expected_lagna"), asc.get("sign"), gated=gated)
add_check(checks, "sun_sign", case.get("expected_sun"), sun.get("sign"), gated=gated)
add_check(checks, "moon_sign", case.get("expected_moon"), moon.get("sign"), gated=gated)
add_check(checks, "lagna_degree", case.get("expected_lagna_degree"), asc.get("degree_in_sign"), gated=False, tolerance=tolerance)
add_check(checks, "sun_degree", case.get("expected_sun_degree"), sun.get("degree_in_sign"), gated=False, tolerance=tolerance)
add_check(checks, "moon_degree", case.get("expected_moon_degree"), moon.get("degree_in_sign"), gated=False, tolerance=tolerance)
result = summarize_case(case, category, checks)
result["source"] = case.get("source", "Indastro.com")
result["note"] = case.get("tz_note", "")
return result
def summarize_case(case: dict[str, Any], category: str, checks: list[dict[str, Any]]) -> dict[str, Any]:
gated = [check for check in checks if check["gated"]]
return {
"id": case["id"],
"name": case["name"],
"category": category,
"passed_checks": sum(1 for check in checks if check["passed"]),
"total_checks": len(checks),
"gated_passed_checks": sum(1 for check in gated if check["passed"]),
"gated_total_checks": len(gated),
"checks": checks,
}
def build_report(args: argparse.Namespace) -> dict[str, Any]:
results: list[dict[str, Any]] = []
for case in load_cases(PUBLIC_CASES):
chart = run_engine(case, args.python)
results.append(validate_public_case(case, chart))
for case in load_cases(INDASTRO_CASES):
chart = run_engine(case, args.python)
results.append(validate_indastro_case(case, chart, args.degree_tolerance))
total_checks = sum(item["total_checks"] for item in results)
passed_checks = sum(item["passed_checks"] for item in results)
gated_total_checks = sum(item["gated_total_checks"] for item in results)
gated_passed_checks = sum(item["gated_passed_checks"] for item in results)
pass_rate = gated_passed_checks / gated_total_checks if gated_total_checks else 1.0
failures = [
{
"id": item["id"],
"name": item["name"],
"category": item["category"],
"failed_checks": [check for check in item["checks"] if check["gated"] and not check["passed"]],
}
for item in results
if any(check["gated"] and not check["passed"] for check in item["checks"])
]
controversial = [item for item in results if item["category"] == "controversial_reference"]
return {
"valid": pass_rate >= args.min_pass_rate and not failures,
"scope": "public real-person chart revalidation; not event_prediction_accuracy",
"min_pass_rate": args.min_pass_rate,
"pass_rate": round(pass_rate, 4),
"passed_checks": passed_checks,
"total_checks": total_checks,
"gated_passed_checks": gated_passed_checks,
"gated_total_checks": gated_total_checks,
"controversial_reference_cases": len(controversial),
"failures": failures,
"results": results,
}
def main() -> int:
parser = argparse.ArgumentParser(description="Run public real-case Jyotish chart revalidation")
parser.add_argument("--python", default=sys.executable)
parser.add_argument("--min-pass-rate", type=float, default=DEFAULT_MIN_PASS_RATE)
parser.add_argument("--degree-tolerance", type=float, default=DEFAULT_DEGREE_TOLERANCE)
parser.add_argument("--summary", action="store_true", help="Print a compact human-readable line before JSON")
args = parser.parse_args()
report = build_report(args)
if args.summary:
print(
"真实案例复验: "
f"gated={report['gated_passed_checks']}/{report['gated_total_checks']} "
f"all={report['passed_checks']}/{report['total_checks']} "
f"controversial={report['controversial_reference_cases']}"
)
print(json.dumps(report, ensure_ascii=False, indent=2))
return 0 if report["valid"] else 1
if __name__ == "__main__":
try:
raise SystemExit(main())
except BrokenPipeError:
raise SystemExit(0)
+33
View File
@@ -1725,6 +1725,39 @@ def test_user_delivery_matrix_is_documented_and_checkable() -> None:
assert '[PYTHON, "scripts/deployment_preflight.py"]' in quality_gate
def test_real_case_revalidation_is_release_gate_and_accuracy_boundary() -> None:
runner_path = ROOT / "tests" / "run_real_case_revalidation.py"
assert runner_path.exists()
runner = runner_path.read_text(encoding="utf-8")
quality_gate = (ROOT / "scripts" / "run_quality_gate.py").read_text(encoding="utf-8")
readme = (ROOT / "README.md").read_text(encoding="utf-8")
for token in [
"tests/celebrity_cases.json",
"tests/indastro_cases.json",
"public_reference",
"controversial_reference",
"min_pass_rate",
"event_prediction_accuracy",
"passed_checks",
"total_checks",
]:
assert token in runner
assert '"tests/run_real_case_revalidation.py"' in quality_gate
assert '[PYTHON, "tests/run_real_case_revalidation.py"' in quality_gate
assert "--skip-real-cases" in quality_gate
for token in [
"真实案例复验",
"公开人物样本",
"星座级一致率",
"不等同于人生事件预测准确率",
"python3 tests/run_real_case_revalidation.py",
]:
assert token in readme
def test_user_startup_labels_are_consistent_across_recovery_surfaces() -> None:
readme = (ROOT / "README.md").read_text(encoding="utf-8")
quality_gate = (ROOT / "scripts" / "run_quality_gate.py").read_text(encoding="utf-8")