Files
Jyotisha/scripts/run_quality_gate.py
T
Jesse_Chen 54269fcfcc
Independent Staging Quality Gate / validate (push) Has been cancelled
Independent Staging Quality Gate / publish (push) Has been cancelled
refactor(chat): extract home helpers, chart library, and starter surfaces
page.tsx still owns the chat main chain, but the first product surfaces now
live in their own modules so later splits can land without editing the 4k-line
Home. Source-lock tests follow the moved tokens; the orphan user-data contract
is aligned and added to the quick gate.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-01 22:58:02 +08:00

572 lines
21 KiB
Python

#!/usr/bin/env python3
"""Run the Jyotish skill quality gate used by local development and CI."""
from __future__ import annotations
import argparse
import contextlib
import json
import os
import py_compile
import subprocess
import sys
import tempfile
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT / "scripts"))
from local_env import load_local_env # noqa: E402
load_local_env(ROOT)
APP = ROOT / "frontend"
PYTHON = sys.executable
os.environ.setdefault("PYTHON", PYTHON)
COMPILE_DIRS = [
ROOT / "scripts",
ROOT / "jyotish_vedic",
]
EXTRA_COMPILE_TARGETS = [
ROOT / "mcp_server.py",
ROOT / "scripts" / "audit_fragments.py",
ROOT / "scripts" / "character_level_inventory_manifest.py",
ROOT / "scripts" / "dasha_reference_audit.py",
ROOT / "scripts" / "external_oracle_sanity_closure.py",
ROOT / "scripts" / "interpretation_source_inventory_gate.py",
ROOT / "scripts" / "oracle_boundary_audit.py",
ROOT / "scripts" / "oracle_collection_queue.py",
ROOT / "scripts" / "oracle_evidence_validator.py",
ROOT / "scripts" / "sync_final_evidence_packet_status.py",
ROOT / "tests" / "run_golden_cases.py",
ROOT / "tests" / "run_real_case_revalidation.py",
]
CORE_PYTEST_TARGETS = [
"tests/test_cli_smoke.py",
"tests/test_api_server_security.py",
"tests/test_jaimini.py",
"tests/test_shadbala_complete.py",
"tests/test_transit_trigger.py",
"tests/test_oracle_collection_queue.py",
"tests/test_oracle_evidence_validator.py",
"tests/test_external_oracle_sanity_closure.py",
# The staging gate runs the quick profile, so a guard absent from this list never runs in CI.
# These pin the answer-truth contract every product consultation is built on, and their failure
# mode is silent widening — nothing errors when they regress (BUG-267, BUG-270).
"tests/test_consultation_consumer_context.py",
"tests/test_declared_window_chart.py",
# Staging quick profile never runs `tests/` wholesale. A distinguish probe
# with empty mapping or non-positive gain must fail this gate (BUG-393).
"tests/test_candidate_discriminator_contract.py",
# Auto staging gate is `--profile quick`. `test.yml` / `ci.yml` run the
# full pytest tree but are workflow_dispatch only, so a stale window_scan
# assertion in this glob stayed red on origin/staging until listed here.
"tests/test_rectification_*.py",
# This file regexes frontend source. Home-split and other page.tsx moves must keep it green.
"tests/test_supabase_user_data_contract.py",
]
RUNTIME_TRUTH_PYTEST_TARGETS = [
"tests/test_api_server_security.py::test_high_rigor_vedastro_official_summary_passes_through_contract_fields",
"tests/test_api_server_security.py::test_high_rigor_vedastro_official_summary_exposes_top_reader_contract_from_full_snapshot",
"tests/test_vedastro_external_technique_evidence.py::test_strict_workflow_uses_shared_consultation_executor",
"tests/test_vedastro_runtime_mode_diagnostics.py",
"tests/test_interpretation_source_inventory_gate.py::test_quality_gate_runs_interpretation_source_inventory_gate",
"tests/test_interpretation_source_runtime_coverage.py",
"tests/test_final_jhora_evidence_packet_acceptance.py",
]
def _expand_pytest_targets(targets: list[str]) -> list[str]:
"""Expand glob entries so pytest argv never depends on a shell.
`subprocess.run` is invoked without `shell=True`. A literal
`tests/test_rectification_*.py` is then a filename pytest may ignore,
so a pin that only checks the string is present can stay green while
the suite never runs.
"""
expanded: list[str] = []
for target in targets:
if any(mark in target for mark in "*?["):
matches = sorted(
path.relative_to(ROOT).as_posix()
for path in ROOT.glob(target)
if path.is_file()
)
if not matches:
raise SystemExit(f"pytest glob {target!r} matched no files under {ROOT}")
expanded.extend(matches)
continue
expanded.append(target)
return expanded
RELEASE_CRITICAL_UNTRACKED_PATHS = [
"docs/research/desktop_packaging_spike_2026_06_23.md",
"docs/research/ephemeris_abstraction_feasibility_2026_06_23.md",
"docs/research/ephemeris_adapter_contract_2026_06_23.md",
"docs/research/ephemeris_candidate_adapter_spike_2026_06_23.md",
"docs/research/open_source_scan_2026_06_22.md",
"docs/research/product_gap_matrix_2026_06_22.md",
"docs/research/whole_machine_git_audit_2026_06_23.md",
"findings.md",
"progress.md",
"references/oracle/dasha_shadbala_oracle_cases.json",
"scripts/audit_fragments.py",
"scripts/deep_varga_avastha.py",
"scripts/dasha_reference_audit.py",
"scripts/ephemeris_adapter_contract.py",
"scripts/ephemeris_backend_probe.py",
"scripts/ephemeris_candidate_adapter_spike.py",
"scripts/oracle_boundary_audit.py",
"scripts/oracle_collection_queue.py",
"scripts/oracle_evidence_validator.py",
"task_plan.md",
"tests/test_api_server_security.py",
"tests/test_dasha_reference_audit.py",
"tests/test_deep_varga_avastha.py",
"tests/test_oracle_boundary_audit.py",
"tests/test_oracle_collection_queue.py",
"tests/test_oracle_evidence_validator.py",
]
QUALITY_GATE_PROFILES = {
"quick": {
"skip_slow": True,
"skip_yoga_logic": True,
"skip_frontend_runtime": False,
"check_release_hygiene": False,
"skip_real_cases": True,
"skip_dasha_audit": True,
"skip_oracle_audit": True,
"skip_local_accuracy_report": True,
"skip_vedastro_live": True,
},
"browser": {
"skip_slow": True,
"skip_yoga_logic": True,
"skip_frontend_runtime": False,
"check_release_hygiene": False,
"skip_real_cases": True,
"skip_dasha_audit": True,
"skip_oracle_audit": True,
"skip_local_accuracy_report": True,
"skip_vedastro_live": True,
},
"release": {
"skip_slow": False,
"skip_yoga_logic": False,
"skip_frontend_runtime": False,
"check_release_hygiene": True,
"skip_real_cases": False,
"skip_dasha_audit": False,
"skip_oracle_audit": False,
"skip_local_accuracy_report": False,
"skip_vedastro_live": True,
},
"accuracy": {
"skip_slow": True,
"skip_yoga_logic": False,
"skip_frontend_runtime": True,
"check_release_hygiene": False,
"skip_real_cases": False,
"skip_dasha_audit": False,
"skip_oracle_audit": False,
"skip_local_accuracy_report": False,
"skip_vedastro_live": True,
},
"vedastro-live": {
"skip_slow": True,
"skip_yoga_logic": True,
"skip_frontend_runtime": True,
"check_release_hygiene": False,
"skip_real_cases": True,
"skip_dasha_audit": True,
"skip_oracle_audit": True,
"skip_local_accuracy_report": True,
"skip_vedastro_live": False,
},
"runtime-truth": {
"skip_slow": True,
"skip_yoga_logic": True,
"skip_frontend_runtime": True,
"check_release_hygiene": False,
"skip_real_cases": True,
"skip_dasha_audit": True,
"skip_oracle_audit": True,
"skip_local_accuracy_report": True,
"skip_vedastro_live": True,
},
}
DASHA_REFERENCE_AUDIT_CMD = [
PYTHON,
"scripts/dasha_reference_audit.py",
"--year",
"1990",
"--month",
"4",
"--day",
"17",
"--hour",
"14",
"--minute",
"45",
"--second",
"20",
"--lat",
"36.466667",
"--lon",
"114.2",
"--tz",
"8",
"--target-start-date",
"2021-05-18",
"--target-source",
"synthetic_fixture",
]
ORACLE_BOUNDARY_AUDIT_CMD = [
PYTHON,
"scripts/oracle_boundary_audit.py",
"--oracle-file",
"references/oracle/dasha_shadbala_oracle_cases.json",
]
EXTERNAL_ORACLE_SANITY_CLOSURE_CMD = [
PYTHON,
"scripts/external_oracle_sanity_closure.py",
"--format",
"json",
]
ORACLE_COLLECTION_QUEUE_CMD = [
PYTHON,
"scripts/oracle_collection_queue.py",
"--oracle-file",
"references/oracle/dasha_shadbala_oracle_cases.json",
"--format",
"json",
]
ORACLE_COLLECTION_QUEUE_EXPECTED_FIELDS = ["evidence_packet", "capture_id", "target_fields"]
ORACLE_EVIDENCE_VALIDATOR_CMD = [
PYTHON,
"scripts/oracle_evidence_validator.py",
"--queue-file",
"{queue_file}",
]
def tail_text(text: str, *, limit: int = 2400) -> str:
text = text.strip()
if len(text) <= limit:
return text
return f"...\n{text[-limit:]}"
def extract_json_payload(text: str) -> dict:
text = text.strip()
if not text:
return {}
for start in [text.find("{"), text.rfind("{")]:
if start < 0:
continue
try:
payload = json.loads(text[start:])
except json.JSONDecodeError:
continue
if isinstance(payload, dict):
return payload
return {}
def format_failure_summary(
step: str,
cmd: list[str],
returncode: int,
*,
stdout: str = "",
stderr: str = "",
cwd: Path = ROOT,
) -> str:
combined_payload = extract_json_payload(stderr) or extract_json_payload(stdout)
reason = combined_payload.get("reason") if isinstance(combined_payload, dict) else None
snapshot = combined_payload.get("process_snapshot") if isinstance(combined_payload, dict) else None
lines = [
"",
"== Quality gate failed ==",
f"step: {step}",
f"exit code: {returncode}",
f"cwd: {cwd.relative_to(ROOT) if cwd != ROOT else '.'}",
f"command: {' '.join(cmd)}",
]
if reason:
lines.append(f"reason: {reason}")
if snapshot:
lines.append(f"process_snapshot: {json.dumps(snapshot, ensure_ascii=False)}")
if stdout.strip():
lines.append(f"stdout tail:\n{tail_text(stdout)}")
if stderr.strip():
lines.append(f"stderr tail:\n{tail_text(stderr)}")
lines.extend([
"普通用户启动路径:",
"1. 本地 API 服务:.venv/bin/python scripts/jyotish_api_server.py --host 127.0.0.1 --port 5200",
"2. 网页服务:npm run dev --prefix frontend",
"3. Open http://127.0.0.1:3000 and verify /api/health.",
"Next action: Run the focused command above, then rerun the affected Python or Next.js check.",
"",
])
return "\n".join(lines)
def run(cmd: list[str], *, optional: bool = False, step: str | None = None, cwd: Path = ROOT) -> bool:
label = step or " ".join(cmd[:2])
print(f"\n$ {' '.join(cmd)}")
completed = subprocess.run(cmd, cwd=cwd, text=True)
if completed.returncode == 0:
return True
if optional:
print(f"Optional step failed with exit code {completed.returncode}; continuing.")
return False
print(
format_failure_summary(label, cmd, completed.returncode, stdout="", stderr="", cwd=cwd),
file=sys.stderr,
)
raise SystemExit(completed.returncode)
def run_oracle_collection_queue_and_validator() -> None:
with tempfile.NamedTemporaryFile("w", suffix=".json", delete=False, encoding="utf-8") as handle:
queue_path = Path(handle.name)
try:
print(f"\n$ {' '.join(ORACLE_COLLECTION_QUEUE_CMD)}")
completed = subprocess.run(ORACLE_COLLECTION_QUEUE_CMD, cwd=ROOT, text=True, capture_output=True)
if completed.stdout:
print(completed.stdout, end="" if completed.stdout.endswith("\n") else "\n")
if completed.stderr:
print(completed.stderr, end="" if completed.stderr.endswith("\n") else "\n", file=sys.stderr)
if completed.returncode != 0:
print(
format_failure_summary(
"oracle_collection_queue",
ORACLE_COLLECTION_QUEUE_CMD,
completed.returncode,
stdout=completed.stdout,
stderr=completed.stderr,
),
file=sys.stderr,
)
raise SystemExit(completed.returncode)
queue_path.write_text(completed.stdout, encoding="utf-8")
validator_cmd = [part if part != "{queue_file}" else str(queue_path) for part in ORACLE_EVIDENCE_VALIDATOR_CMD]
run(validator_cmd, step="oracle_evidence_validator")
finally:
with contextlib.suppress(FileNotFoundError):
queue_path.unlink()
def git_untracked_files() -> set[str]:
completed = subprocess.run(
["git", "ls-files", "--others", "--exclude-standard"],
cwd=ROOT,
text=True,
capture_output=True,
check=False,
)
if completed.returncode != 0:
raise SystemExit(
format_failure_summary(
"release_hygiene_check",
["git", "ls-files", "--others", "--exclude-standard"],
completed.returncode,
stdout=completed.stdout,
stderr=completed.stderr,
)
)
return {line.strip() for line in completed.stdout.splitlines() if line.strip()}
def release_hygiene_check(require_external_parity: bool = False) -> None:
print("\n== Release hygiene check ==")
untracked = git_untracked_files()
critical = [path for path in RELEASE_CRITICAL_UNTRACKED_PATHS if path in untracked]
if critical:
payload = {
"reason": "release_critical_untracked_files",
"untracked_count": len(critical),
"untracked_files": critical,
"next_action": "Stage or commit these product files before running the release profile.",
}
print(json.dumps(payload, ensure_ascii=False, indent=2), file=sys.stderr)
raise SystemExit(1)
run([PYTHON, "scripts/public_release_privacy_scan.py", "--json"])
run([PYTHON, "scripts/report_renderer_isolation_poc.py", "--strict"])
parity_command = [PYTHON, "scripts/three_engine_parity_replay_validator.py", "references/oracle/three_engine_parity_replay_manifest.json"]
if require_external_parity:
parity_command.append("--require-pass")
run(parity_command)
print("release_hygiene_check ok: no release-critical product files are untracked")
def run_vedastro_live_smoke() -> None:
print("\n== VedAstro live adapter smoke ==")
endpoint = os.environ.get("VEDASTRO_API_ENDPOINT", "").strip()
network_enabled = os.environ.get("VEDASTRO_ENABLE_NETWORK", "").strip().lower() in {"1", "true", "yes"}
if not endpoint or not network_enabled:
print(json.dumps({
"status": "blocked",
"reason": "vedastro_live_endpoint_or_network_flag_missing",
"required_env": {
"endpoint": "VEDASTRO_API_ENDPOINT",
"network": "VEDASTRO_ENABLE_NETWORK",
},
"boundary": "Default CI stays deterministic; configure both env vars to run a real VedAstro live smoke.",
}, ensure_ascii=False, indent=2))
return
run([
PYTHON,
"scripts/vedastro_service_adapter.py",
"--range-scan",
"--domain",
"career",
"--case",
"beijing_first_use_demo",
"--start-date",
"2026-01-01",
"--end-date",
"2026-12-31",
])
def compile_targets() -> None:
print("\n== Compile core Python files ==")
targets: list[Path] = []
for directory in COMPILE_DIRS:
targets.extend(sorted(directory.glob("*.py")))
targets.extend(EXTRA_COMPILE_TARGETS)
seen: set[Path] = set()
for target in targets:
if target in seen or not target.exists():
continue
seen.add(target)
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",
"references/oracle/dasha_shadbala_oracle_cases.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 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_real_cases",
"skip_dasha_audit",
"skip_oracle_audit",
"skip_local_accuracy_report",
"skip_vedastro_live",
]:
if getattr(args, key):
profile[key] = True
return profile
def main() -> int:
parser = argparse.ArgumentParser(description="Run Jyotish skill quality gate")
parser.add_argument("--profile", choices=["quick", "browser", "release", "accuracy", "vedastro-live", "runtime-truth"], default="browser", help="Quality gate profile: quick, browser, release, accuracy, vedastro-live, or runtime-truth")
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")
parser.add_argument("--skip-frontend-runtime", action="store_true", help="Skip Next.js tests, lint, and production build")
parser.add_argument("--skip-real-cases", action="store_true", help="Skip public real-person chart revalidation")
parser.add_argument("--skip-dasha-audit", action="store_true", help="Skip Dasha reference-drift audit")
parser.add_argument("--skip-oracle-audit", action="store_true", help="Skip combined Dasha/Shadbala external oracle boundary audit")
parser.add_argument("--skip-local-accuracy-report", action="store_true", help="Skip consolidated local accuracy report")
parser.add_argument("--skip-vedastro-live", action="store_true", help="Skip optional VedAstro live endpoint smoke")
parser.add_argument("--all-tests", action="store_true", help="Run every pytest file, including optional-dependency suites")
parser.add_argument("--require-external-parity", action="store_true", help="Fail the release gate unless the three-engine raw parity manifest passes.")
args = parser.parse_args()
profile = run_profile(args)
os.environ.setdefault("PYTHONPATH", str(ROOT / "scripts"))
print(f"\n== Quality gate profile: {args.profile} ==")
print(json.dumps(profile, ensure_ascii=False, indent=2))
if args.profile == "runtime-truth":
for target in [
ROOT / "scripts" / "jyotish_api_server.py",
ROOT / "scripts" / "diagnose_vedastro_mode.py",
ROOT / "scripts" / "diagnose_external_engine_adapters.py",
ROOT / "scripts" / "interpretation_source_runtime_coverage.py",
ROOT / "scripts" / "sync_final_evidence_packet_status.py",
ROOT / "scripts" / "external_validation_release_gate.py",
]:
py_compile.compile(str(target), doraise=True)
print(f"compiled {target.relative_to(ROOT)}")
run([PYTHON, "scripts/sync_final_evidence_packet_status.py"])
run([PYTHON, "scripts/interpretation_source_inventory_gate.py"])
run([PYTHON, "scripts/diagnose_vedastro_mode.py", "--json"])
run([PYTHON, "scripts/diagnose_external_engine_adapters.py", "--json"])
run([PYTHON, "scripts/external_validation_release_gate.py", "--require-match"])
else:
compile_targets()
validate_json_files()
run([PYTHON, "scripts/audit_capabilities.py", "--mode", "validate"])
run([PYTHON, "scripts/audit_fragments.py", "--strict"])
run([PYTHON, "scripts/interpretation_source_inventory_gate.py"])
run([PYTHON, "scripts/character_level_inventory_manifest.py", "--scope", "project", "--no-write", "--summary-only"])
if profile["check_release_hygiene"]:
release_hygiene_check(require_external_parity=args.require_external_parity)
run([PYTHON, "scripts/validate_bphs_invariants.py"])
if args.all_tests:
pytest_targets = ["tests"]
elif args.profile == "runtime-truth":
pytest_targets = RUNTIME_TRUTH_PYTEST_TARGETS
else:
pytest_targets = CORE_PYTEST_TARGETS
run([PYTHON, "-m", "pytest", *_expand_pytest_targets(pytest_targets)])
if not profile["skip_frontend_runtime"]:
run(["npm", "test"], optional=False, cwd=APP)
run(["npm", "run", "lint"], optional=False, cwd=APP)
run(["npm", "run", "build"], optional=False, cwd=APP)
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_dasha_audit"]:
run(DASHA_REFERENCE_AUDIT_CMD)
if not profile["skip_oracle_audit"]:
run(ORACLE_BOUNDARY_AUDIT_CMD)
run(EXTERNAL_ORACLE_SANITY_CLOSURE_CMD)
run_oracle_collection_queue_and_validator()
if not profile["skip_yoga_logic"]:
run([PYTHON, "scripts/validate_logic_v2.py"], optional=True)
if not profile["skip_local_accuracy_report"]:
run([PYTHON, "scripts/local_accuracy_report.py", "--format", "json"])
if not profile["skip_vedastro_live"]:
run_vedastro_live_smoke()
print("\nQuality gate passed.")
return 0
if __name__ == "__main__":
raise SystemExit(main())