Evidence Packet
+仅显示已完成任务的可审计计算状态、证据包和技法审计。不会展示内部提示词或原始出生输入。
+ +运行状态
Technique Audit
-
Machine Evidence Packet
-
Warnings
-
From a8f6a57ccc8675a815abf41ca08fdfeb13ab7abe Mon Sep 17 00:00:00 2001 From: 732642856 <732642856@qq.com> Date: Sat, 11 Jul 2026 23:24:54 +0800 Subject: [PATCH 01/61] harden calculation contracts and local API boundaries --- docs/research/pre_work_error_ledger.md | 14 + jyotish-app/ai-chat.js | 1 - jyotish-app/api-bridge.js | 18 ++ jyotish-app/auth.js | 3 +- pyproject.toml | 1 + requirements.txt | 1 + scripts/domain_calculation_service.py | 260 ++++++++++++++++ scripts/jyotish_api_server.py | 377 +++++++++++++++-------- scripts/jyotish_engine.py | 24 +- scripts/report_builder.py | 23 +- scripts/three_engine_parity_runner.py | 168 ++++++++++ scripts/timezone_utils.py | 18 +- tests/conftest.py | 16 + tests/test_api_server_security.py | 9 +- tests/test_calculation_p0_regressions.py | 117 +++++++ tests/test_runtime_security_p0.py | 156 ++++++++++ tests/test_three_engine_parity_runner.py | 26 ++ 17 files changed, 1075 insertions(+), 157 deletions(-) create mode 100644 scripts/domain_calculation_service.py create mode 100644 scripts/three_engine_parity_runner.py create mode 100644 tests/test_calculation_p0_regressions.py create mode 100644 tests/test_runtime_security_p0.py create mode 100644 tests/test_three_engine_parity_runner.py diff --git a/docs/research/pre_work_error_ledger.md b/docs/research/pre_work_error_ledger.md index ad2d5dde..b7dd94ae 100644 --- a/docs/research/pre_work_error_ledger.md +++ b/docs/research/pre_work_error_ledger.md @@ -64,6 +64,20 @@ For large architecture or release work, also read: | ERR-031 | Premium skill zip can ship without user install prompts or replay schemas, leaving users and future oracle imports without a contract. | mitigated 2026-07-09 | `skill_release_package.py` must inject `INSTALL.md` and `USER_PROMPTS.md`; replay contracts must live in `references/real_case_calibration/` and `references/oracle/`. | | ERR-032 | Full smoke files can time out while focused slices pass; `test_full_reading_reports_ayanamsa_metadata_and_ai_prompt_pack` currently exposes `external_oracle_gap_summary=null`. | observed 2026-07-10 | Do not claim full `tests/test_cli_smoke.py` or full `tests/test_vedastro_external_technique_evidence.py` passed unless run to completion; use focused slices for related changes and track the prompt-pack gap separately. | | ERR-033 | Premium skill zip validation can accidentally depend on a parent Git repository, so a cloud-drive user may fail in a clean unzip directory. | mitigated 2026-07-10 | Release acceptance must include `tests/test_skill_release_clean_trial.py`; scripts such as `public_release_privacy_scan.py` must support non-Git unpacked zip directories. | +| ERR-034 | A single `historical_event_backtest.build_report()` strict replay can exceed 120 seconds before returning a case result. | observed 2026-07-11 | Use `scripts/public_real_case_benchmark.py` for bounded batch evidence replay; keep strict workflow as a separately timed probe and report timeout as blocked. | +| ERR-035 | `benchmarks/jyotish/scripts/run_pyjhora_compare.py --help` executes the benchmark and crashes when canonical fixtures are absent. | observed 2026-07-11 | Do not claim PyJHora parity from readiness. Generate canonical fixtures or harden the runner before the next same-chart batch. | +| ERR-036 | `public_real_case_benchmark.py --rule-version compare` originally replayed both rule versions and exceeded the 120-second command budget. | mitigated 2026-07-11 | Compare mode must read precomputed `--comparison-v1` and `--comparison-v2` reports; never duplicate engine replay inside comparison. | +| ERR-037 | `scripts/muntha.py` failed at import because `List` was used in an annotation but not imported. | resolved 2026-07-11 | Keep `tests/test_muntha_module.py`; a technique file does not count as available unless it imports and runs independently. | +| ERR-038 | Real-case scoring counted the same planet twice when MD and AD had the same lord, inflating strong-hit scores and duplicating signals. | mitigated 2026-07-11 | V2.1 must deduplicate active lords before `_planet_score`; keep the same-MD/AD regression test and preserve legacy V2 reports for audit only. | +| ERR-039 | `exact_label_rate` looked like classification accuracy even though the benchmark already knew the event domain and assigned the expected label at the strong threshold. | mitigated 2026-07-11 | Use `known_event_activation_rate` and `strong_activation_rate`; keep old names deprecated and never present them as predictive accuracy. | +| ERR-040 | `.gitignore` excluded only parts of `scratch/`, leaving local helper files and `.serena/` visible to `git add .`. | resolved 2026-07-11 | Ignore `/scratch/` and `/.serena/` at repo root; keep a regression test for both private workspace directories. | +| ERR-041 | Positive-event replay scores were interpreted as timing evidence even though nearby non-target dates could receive equal or higher scores. | mitigated 2026-07-11 | Keep the negative-control date-ranking pilot and `timing_precision_gate`; block exact-day/month claims while Top-3 ranking remains below the gate. | +| ERR-042 | REST duplicated natal chart, Vimshottari and Sade Sati calculations, so True Node was ignored, the first Dasha balance drifted, and Saturn transit was fabricated. | resolved 2026-07-11 | Keep `tests/test_calculation_p0_regressions.py`; domain/CLI/REST must share `domain_calculation_service.py`, effective parameters and `result_hash`. | +| ERR-043 | Localhost POST requests trusted CORS response headers as an execution guard; report Chromium could load external/local resources; async job IDs were predictable and persisted without capability authentication or TTL. | mitigated 2026-07-11 | Keep `tests/test_runtime_security_p0.py`; enforce Origin/Host/JSON, sandbox report resources, use random capability tokens, `0600` atomic records, TTL deletion and a bounded worker queue. Run an isolated Chromium network PoC before declaring the renderer fully hardened. | +| ERR-044 | Focused selections that include legacy full chart API tests can still exceed the 120-second desktop command budget even after pure calculation tests pass. | observed 2026-07-11 | Keep P0 calculation/security tests pure and fast; profile the legacy chart fixture separately before using the full API file as a blocking CI gate. | +| ERR-045 | Three-engine readiness was mistaken for completed same-chart parity. Public replay on 2026-07-11 captured PyJHora and jyotishganit raw, but VedAstro returned `official_snapshot_budget_exhausted` with no raw response. | active external blocker | Keep `three_engine_parity_runner.py`; status remains `blocked`/`partial` until all required raw artifacts are normalized into comparison rows. | +| ERR-046 | Report-renderer SSRF/file PoC could not run because the Playwright Chromium binary was absent and installation exceeded the desktop outer timeout. | blocked environment | Keep route/JS-denial tests; rerun isolated HTTP/file PoC only after a verified Chromium installation, then update this ledger with the measured request count. | +| ERR-047 | Initial `slow` marker partition for `test_api_server_security.py` still exceeded the 120-second desktop budget; heavy paths extend beyond VedAstro/high-rigor prefix groups. | active profiling blocker | Profile test node IDs in bounded subprocess batches, mark only measured heavy tests, and keep fast-security acceptance separate from long CI integration coverage. | ## Fragment Sweep Command Set diff --git a/jyotish-app/ai-chat.js b/jyotish-app/ai-chat.js index a15533e4..11aaa57a 100644 --- a/jyotish-app/ai-chat.js +++ b/jyotish-app/ai-chat.js @@ -444,7 +444,6 @@ function buildAISetupGuidance() { function getApiBase() { if (window.JYOTISH_API_BASE) return window.JYOTISH_API_BASE; if (import.meta.env?.VITE_JYOTISH_API_BASE) return import.meta.env.VITE_JYOTISH_API_BASE; - if (window.Capacitor?.isNativePlatform?.()) return localStorage.getItem('jyotish_api_base') || ''; return ''; // 同域部署 } diff --git a/jyotish-app/api-bridge.js b/jyotish-app/api-bridge.js index 66286f2c..e48e5e39 100644 --- a/jyotish-app/api-bridge.js +++ b/jyotish-app/api-bridge.js @@ -47,6 +47,7 @@ async function postJson(path, payload, { requireModernChart = false } = {}) { continue; } activeApiBase = base; + if (data?.mode === 'async_submitted') return pollAsyncJob(data, { base }); return data; } catch (error) { lastAttempt = `${base}${path}`; @@ -60,6 +61,22 @@ async function postJson(path, payload, { requireModernChart = false } = {}) { throw lastError || new Error(buildAPIRecoveryMessage(path, '本地 API 未连接', lastAttempt)); } +async function pollAsyncJob(job, { base = activeApiBase, timeoutMs = 120000, intervalMs = 500 } = {}) { + if (!job?.poll_path || !job?.access_token) throw new Error('Async job response missing poll capability'); + const deadline = Date.now() + timeoutMs; + while (Date.now() < deadline) { + const resp = await fetch(`${base}${job.poll_path}`, { + headers: { Authorization: `Bearer ${job.access_token}` }, + }); + const data = await parseApiResponse(resp); + if (!resp.ok) throw new Error(buildAPIRecoveryMessage(job.poll_path, data?.error || `Job poll failed (${resp.status})`)); + if (data.status === 'completed') return data.result || data; + if (data.status === 'failed') throw new Error(data.error || 'Async job failed'); + await new Promise(resolve => setTimeout(resolve, intervalMs)); + } + throw new Error(buildAPIRecoveryMessage(job.poll_path, 'Async job timed out')); +} + async function fetchJson(path) { let lastError = null; let lastAttempt = null; @@ -501,6 +518,7 @@ window.JyotishAPI = { computeKakshya, computeBhavaBala, computeTransitTriggers, + pollAsyncJob, // AI 解读 aiReading, aiFullReading, diff --git a/jyotish-app/auth.js b/jyotish-app/auth.js index d061ddfd..349c19dd 100644 --- a/jyotish-app/auth.js +++ b/jyotish-app/auth.js @@ -12,7 +12,6 @@ import { escapeAttr, escapeHtml } from './security.js'; const API_BASE = ''; // 同域部署,留空;Capacitor 打包时改为服务器地址 const TOKEN_KEY = 'jyotish_auth_token'; const USER_KEY = 'jyotish_auth_user'; -const API_BASE_KEY = 'jyotish_api_base'; // ============================================================================ // 状态 @@ -61,7 +60,7 @@ export function getUser() { return _user; } export function isLoggedIn() { return !!_token && !!_user; } export function getApiBase() { - return window.JYOTISH_API_BASE || import.meta.env?.VITE_JYOTISH_API_BASE || localStorage.getItem(API_BASE_KEY) || API_BASE; + return window.JYOTISH_API_BASE || import.meta.env?.VITE_JYOTISH_API_BASE || API_BASE; } export function onAuthChange(cb) { _onAuthChange = cb; } diff --git a/pyproject.toml b/pyproject.toml index deecac3e..8404ede8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,6 +28,7 @@ classifiers = [ keywords = ["jyotish", "vedic", "astrology", "astronomy", "dasha", "varga", "nakshatra", "shadbala", "panchanga", "horoscope", "birth-chart"] dependencies = [ "pyswisseph>=2.8", + "timezonefinder>=6.5", ] [project.optional-dependencies] diff --git a/requirements.txt b/requirements.txt index c129a6b8..ab9e0673 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,6 +3,7 @@ # 核心:Swiss Ephemeris 天文计算库(必需) pyswisseph +timezonefinder>=6.5 # 以下为标准库,无需安装(仅供参考): # argparse, json, sys, os, csv, math, sqlite3 diff --git a/scripts/domain_calculation_service.py b/scripts/domain_calculation_service.py new file mode 100644 index 00000000..f778cf91 --- /dev/null +++ b/scripts/domain_calculation_service.py @@ -0,0 +1,260 @@ +#!/usr/bin/env python3 +"""Canonical calculation service shared by CLI, REST, and MCP adapters.""" + +from __future__ import annotations + +import hashlib +import json +import math +import threading +from datetime import datetime +from typing import Any +from zoneinfo import ZoneInfo + +import swisseph as swe +from ayanamsa_utils import apply_ayanamsa, normalize_ayanamsa_name +from dasha_analyzer import build_dasha_timeline, lon_to_nakshatra +from jyotish_engine import SIGNS, compute_chart_data +from sade_sati import calc_sade_sati_complete + +CONTRACT_VERSION = "1.0.0" +_SWISSEPH_LOCK = threading.RLock() +_PLANET_IDS = {"Saturn": swe.SATURN} + + +class CalculationError(ValueError): + pass + + +class TimezoneInferenceError(CalculationError): + pass + + +def _canonical_hash(payload: dict[str, Any]) -> str: + encoded = json.dumps( + payload, + ensure_ascii=True, + sort_keys=True, + separators=(",", ":"), + default=str, + ).encode("utf-8") + return hashlib.sha256(encoded).hexdigest() + + +def _lookup_timezone_name(lat: float, lon: float) -> str | None: + try: + from timezonefinder import TimezoneFinder + except ImportError as exc: + raise TimezoneInferenceError("timezone inference dependency unavailable") from exc + return TimezoneFinder().timezone_at(lng=lon, lat=lat) + + +def infer_timezone_offset(*, lat: float, lon: float, local_datetime: datetime) -> float: + if not (-90 <= lat <= 90 and -180 <= lon <= 180): + raise TimezoneInferenceError("timezone inference received invalid coordinates") + tz_name = _lookup_timezone_name(lat, lon) + if not tz_name: + raise TimezoneInferenceError("timezone inference returned no IANA zone") + try: + offset = local_datetime.replace(tzinfo=ZoneInfo(tz_name)).utcoffset() + except Exception as exc: + raise TimezoneInferenceError("timezone inference failed for IANA zone") from exc + if offset is None: + raise TimezoneInferenceError("timezone inference returned no UTC offset") + return offset.total_seconds() / 3600.0 + + +def _normalized_request(payload: dict[str, Any]) -> dict[str, Any]: + requested_node = str(payload.get("node_mode", payload.get("nodeMode", "mean"))).lower() + if requested_node not in {"mean", "true"}: + raise CalculationError("node_mode must be mean or true") + ayanamsa = normalize_ayanamsa_name(payload.get("ayanamsa", "lahiri")) + local_dt = datetime( + int(payload["year"]), + int(payload["month"]), + int(payload["day"]), + int(float(payload.get("hour", 0))), + int(float(payload.get("minute", 0))), + int(float(payload.get("second", 0))), + ) + lat = float(payload["lat"]) + lon = float(payload["lon"]) + tz_requested = payload.get("tz") + timezone_source = "explicit_offset" + if tz_requested in {None, ""}: + tz = infer_timezone_offset(lat=lat, lon=lon, local_datetime=local_dt) + timezone_source = "iana_inferred" + else: + tz = float(tz_requested) + if not math.isfinite(tz) or not -14 <= tz <= 14: + raise CalculationError("tz must be a finite offset between -14 and 14") + return { + "year": local_dt.year, + "month": local_dt.month, + "day": local_dt.day, + "hour": int(float(payload.get("hour", 0))), + "minute": int(float(payload.get("minute", 0))), + "second": int(float(payload.get("second", 0))), + "lat": lat, + "lon": lon, + "tz": tz, + "timezone_source": timezone_source, + "ayanamsa": ayanamsa, + "node_mode": requested_node, + } + + +def _contract(requested: dict[str, Any], effective: dict[str, Any], *, algorithm: str) -> dict[str, Any]: + return { + "contract_version": CONTRACT_VERSION, + "algorithm": algorithm, + "requested": requested, + "effective": effective, + } + + +def compute_chart(payload: dict[str, Any]) -> dict[str, Any]: + request = _normalized_request(payload) + with _SWISSEPH_LOCK: + chart, _asc_idx, _jd, _ayanamsa = compute_chart_data( + request["year"], + request["month"], + request["day"], + request["hour"], + request["minute"], + request["lat"], + request["lon"], + request["tz"], + node_mode=request["node_mode"], + second=request["second"], + ayanamsa_name=request["ayanamsa"], + ) + if not isinstance(chart, dict): + raise CalculationError("canonical chart calculation failed") + + for planet in chart.get("planets", {}).values(): + if not isinstance(planet, dict) or "error" in planet: + continue + planet.setdefault("lon", planet.get("degree_raw", planet.get("degree"))) + if planet.get("sign") in SIGNS: + planet.setdefault("sign_idx", SIGNS.index(planet["sign"])) + + birth = chart.get("birth_info", {}) + effective = { + "ayanamsa": birth.get("ayanamsa_name", request["ayanamsa"]), + "node_mode": birth.get("node_mode", request["node_mode"]), + "timezone_offset": request["tz"], + "timezone_source": request["timezone_source"], + "ephemeris_source": "swisseph_calc_ut", + "ephemeris_flags_verified": False, + } + requested = { + "ayanamsa": payload.get("ayanamsa", "lahiri"), + "node_mode": payload.get("node_mode", payload.get("nodeMode", "mean")), + "timezone_offset": payload.get("tz"), + } + contract = _contract(requested, effective, algorithm="sidereal_natal_chart") + hash_payload = { + "contract": contract, + "birth": birth, + "ascendant": chart.get("ascendant"), + "planets": chart.get("planets"), + } + chart["calculation_contract"] = contract + chart["result_hash"] = _canonical_hash(hash_payload) + return chart + + +def compute_vimshottari_timeline( + *, birth_dt: datetime, moon_lon: float, current_date: datetime | None = None +) -> dict[str, Any]: + nak_info, progress, pada = lon_to_nakshatra(float(moon_lon) % 360) + timeline, elapsed, remaining, start_lord = build_dasha_timeline( + birth_dt.strftime("%Y-%m-%d"), nak_info, progress + ) + periods = [ + { + "lord": period["lord"], + "years": period["years"], + "start": period["start"].strftime("%Y-%m-%d"), + "end": period["end"].strftime("%Y-%m-%d"), + } + for period in timeline + ] + contract = _contract( + {"moon_longitude": float(moon_lon) % 360}, + {"year_basis_days": 365.25, "nakshatra": nak_info[0], "pada": pada}, + algorithm="vimshottari_birth_balance", + ) + result = { + "periods": periods, + "birth_balance": { + "lord": start_lord, + "elapsed_years": elapsed, + "remaining_years": remaining, + }, + "calculation_contract": contract, + } + result["result_hash"] = _canonical_hash(result) + return result +def compute_transit_longitude( + *, planet: str, reference_date: str, tz: float, ayanamsa: str = "lahiri" +) -> dict[str, Any]: + if planet not in _PLANET_IDS: + raise CalculationError(f"unsupported transit planet: {planet}") + try: + local_dt = datetime.strptime(reference_date[:10], "%Y-%m-%d").replace(hour=12) + except (TypeError, ValueError) as exc: + raise CalculationError("reference_date must be YYYY-MM-DD") from exc + ayanamsa_name = normalize_ayanamsa_name(ayanamsa) + with _SWISSEPH_LOCK: + apply_ayanamsa(ayanamsa_name, swe) + jd = swe.julday( + local_dt.year, + local_dt.month, + local_dt.day, + 12.0 - float(tz), + ) + ayanamsa_value = swe.get_ayanamsa(jd) + position, flags = swe.calc_ut(jd, _PLANET_IDS[planet]) + longitude = (position[0] - ayanamsa_value) % 360 + return { + "planet": planet, + "longitude": longitude, + "reference_date": reference_date[:10], + "ayanamsa": ayanamsa_name, + "timezone_offset": float(tz), + "swisseph_return_flags": int(flags), + "data_layer": "true_transit_positions", + } + + +def compute_sade_sati( + *, + moon_degree: float, + asc_degree: float, + reference_date: str, + tz: float, + ayanamsa: str = "lahiri", +) -> dict[str, Any]: + transit = compute_transit_longitude( + planet="Saturn", + reference_date=reference_date, + tz=tz, + ayanamsa=ayanamsa, + ) + result = calc_sade_sati_complete( + float(moon_degree) % 360, + float(asc_degree) % 360, + transit["longitude"], + datetime.strptime(reference_date[:10], "%Y-%m-%d"), + ) + result["transit_saturn_lon"] = transit["longitude"] + result["provenance"] = transit + result["calculation_contract"] = _contract( + {"reference_date": reference_date[:10], "ayanamsa": ayanamsa, "tz": tz}, + transit, + algorithm="sade_sati_true_saturn_transit", + ) + result["result_hash"] = _canonical_hash(result) + return result diff --git a/scripts/jyotish_api_server.py b/scripts/jyotish_api_server.py index e0767f59..ed23e3e1 100644 --- a/scripts/jyotish_api_server.py +++ b/scripts/jyotish_api_server.py @@ -15,10 +15,12 @@ import json, sys, os, math import importlib.util import hashlib import re +import secrets import threading import time +from concurrent.futures import ThreadPoolExecutor from datetime import datetime, timedelta -from http.server import HTTPServer, BaseHTTPRequestHandler +from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer from pathlib import Path from urllib.parse import urlparse @@ -43,6 +45,25 @@ _LOCAL_MODULE_CACHE = {} _API_CHART_CACHE_SCOPE = 'api_chart_response' _HIGH_RIGOR_JOB_SCOPE = 'high_rigor_workflow' _UNIFIED_CONSULTATION_ORCHESTRATOR = UnifiedConsultationOrchestrator() +_ASYNC_JOB_WORKERS = max(int(os.environ.get('JYOTISH_ASYNC_JOB_WORKERS', '2')), 1) +_ASYNC_JOB_QUEUE_SIZE = max(int(os.environ.get('JYOTISH_ASYNC_JOB_QUEUE_SIZE', '8')), 0) +_ASYNC_JOB_EXECUTOR = ThreadPoolExecutor( + max_workers=_ASYNC_JOB_WORKERS, + thread_name_prefix='jyotish-job', +) +_ASYNC_JOB_CAPACITY = threading.BoundedSemaphore(_ASYNC_JOB_WORKERS + _ASYNC_JOB_QUEUE_SIZE) + + +def _submit_background_job(callback): + if not _ASYNC_JOB_CAPACITY.acquire(blocking=False): + raise JobQueueFull('Async job queue is full') + try: + future = _ASYNC_JOB_EXECUTOR.submit(callback) + except Exception: + _ASYNC_JOB_CAPACITY.release() + raise + future.add_done_callback(lambda _future: _ASYNC_JOB_CAPACITY.release()) + return future def _western_evidence_packet_from_body(body: dict, route_packet: dict) -> dict | None: @@ -467,29 +488,66 @@ def _async_job_path(scope: str, job_id: str) -> Path: return _async_job_dir(scope) / f'{job_id}.json' -def _load_high_rigor_job_record(job_id: str) -> dict | None: - return _load_async_job_record(_HIGH_RIGOR_JOB_SCOPE, job_id) +def _async_job_ttl_seconds() -> float: + raw = str(os.environ.get('JYOTISH_ASYNC_JOB_TTL_SECONDS', '3600')).strip() + try: + return max(float(raw), 1.0) + except ValueError: + return 3600.0 + + +def _new_async_job_identity(prefix: str) -> dict: + return { + 'job_id': f'{prefix}_{secrets.token_hex(16)}', + 'access_token': secrets.token_urlsafe(32), + } + + +def _access_token_hash(token: str) -> str: + return hashlib.sha256(token.encode('utf-8')).hexdigest() + + +def _load_high_rigor_job_record(job_id: str, *, access_token: str = '') -> dict | None: + return _load_async_job_record( + _HIGH_RIGOR_JOB_SCOPE, + job_id, + access_token=access_token, + ) def _write_high_rigor_job_record(job_id: str, payload: dict) -> dict: return _write_async_job_record(_HIGH_RIGOR_JOB_SCOPE, job_id, payload) -def _load_async_job_record(scope: str, job_id: str) -> dict | None: +def _load_async_job_record(scope: str, job_id: str, *, access_token: str = '') -> dict | None: path = _async_job_path(scope, job_id) if not path.exists(): return None try: - return json.loads(path.read_text(encoding='utf-8')) + record = json.loads(path.read_text(encoding='utf-8')) except (OSError, json.JSONDecodeError): return None + expires_at = record.get('expires_at_unix') + if isinstance(expires_at, (int, float)) and time.time() >= float(expires_at): + try: + path.unlink() + except OSError: + pass + return None + expected = record.get('access_token_hash') + if not isinstance(expected, str) or not access_token: + raise JobAccessDenied('Async job access token required') + if not secrets.compare_digest(expected, _access_token_hash(access_token)): + raise JobAccessDenied('Async job access token invalid') + return record def _write_async_job_record(scope: str, job_id: str, payload: dict) -> dict: - _async_job_path(scope, job_id).write_text( - json.dumps(payload, ensure_ascii=False, sort_keys=True), - encoding='utf-8', - ) + path = _async_job_path(scope, job_id) + temp_path = path.with_suffix(f'.{secrets.token_hex(8)}.tmp') + temp_path.write_text(json.dumps(payload, ensure_ascii=False, sort_keys=True), encoding='utf-8') + os.chmod(temp_path, 0o600) + os.replace(temp_path, path) return payload @@ -809,6 +867,22 @@ class BadRequest(ValueError): """Client-side request validation failed.""" +class Forbidden(PermissionError): + """Request failed the local API trust boundary.""" + + +class UnsupportedMediaType(ValueError): + """Request body media type is not supported.""" + + +class JobAccessDenied(PermissionError): + """Async job capability token is missing or invalid.""" + + +class JobQueueFull(RuntimeError): + """Bounded async worker queue has no remaining capacity.""" + + class JyotishAPIHandler(BaseHTTPRequestHandler): server_version = 'JyotishAPI/6.9.14' @@ -832,6 +906,24 @@ class JyotishAPIHandler(BaseHTTPRequestHandler): if origin in allowed: self.send_header('Access-Control-Allow-Origin', origin) + def _enforce_request_security(self, *, require_json=False): + origin = self.headers.get('Origin') + allowed = getattr(self.server, 'allowed_origins', DEFAULT_ALLOWED_ORIGINS) + if origin and origin not in allowed: + raise Forbidden('Origin is not allowed') + host = (self.headers.get('Host') or '').split(':', 1)[0].strip('[]').lower() + if host and host not in {'localhost', '127.0.0.1', '::1'}: + raise Forbidden('Host is not allowed') + if require_json: + content_type = (self.headers.get('Content-Type') or '').split(';', 1)[0].strip().lower() + if content_type != 'application/json': + raise UnsupportedMediaType('Content-Type must be application/json') + + def _job_access_token(self): + authorization = self.headers.get('Authorization') or '' + scheme, _, token = authorization.partition(' ') + return token.strip() if scheme.lower() == 'bearer' else '' + def _vedastro_status(self): adapter = _load_local_module('vedastro_service_adapter') endpoint = os.environ.get('VEDASTRO_API_ENDPOINT', '').strip() @@ -877,11 +969,16 @@ class JyotishAPIHandler(BaseHTTPRequestHandler): } def do_OPTIONS(self): - self._json({}) + try: + self._enforce_request_security() + self._json({}) + except Forbidden as exc: + self._error_json(str(exc), 403, 'ERR_FORBIDDEN') def do_GET(self): path = urlparse(self.path).path try: + self._enforce_request_security() if path == '/api/health': swisseph_available = False swisseph_version = None @@ -937,6 +1034,8 @@ class JyotishAPIHandler(BaseHTTPRequestHandler): self._json(self._real_case_revalidation()) else: self._error_json('Not found', 404, 'ERR_NOT_FOUND') + except (Forbidden, JobAccessDenied) as exc: + self._error_json(str(exc), 403, 'ERR_FORBIDDEN') except Exception: import logging logging.exception("[api_server] GET request failed for %s", path) @@ -945,6 +1044,7 @@ class JyotishAPIHandler(BaseHTTPRequestHandler): def do_POST(self): path = urlparse(self.path).path try: + self._enforce_request_security(require_json=True) body = self._read_json_body() if path == '/api/chart': result = self._compute_chart(body) @@ -1083,6 +1183,12 @@ class JyotishAPIHandler(BaseHTTPRequestHandler): self._error_json(f'Unknown endpoint: {path}', 404, 'ERR_NOT_FOUND') except BadRequest as e: self._error_json(str(e), 400, 'ERR_BAD_REQUEST') + except Forbidden as exc: + self._error_json(str(exc), 403, 'ERR_FORBIDDEN') + except UnsupportedMediaType as exc: + self._error_json(str(exc), 415, 'ERR_UNSUPPORTED_MEDIA_TYPE') + except JobQueueFull as exc: + self._error_json(str(exc), 503, 'ERR_JOB_QUEUE_FULL') except Exception: import logging logging.exception("[api_server] request failed for %s", path) @@ -1122,13 +1228,20 @@ class JyotishAPIHandler(BaseHTTPRequestHandler): tz = body.get('tz') if tz is not None and tz != "": return self._get_float(body, 'tz', 8, -14, 14) - from timezone_utils import infer_timezone from datetime import datetime try: dt = datetime(int(year), int(month), int(day), int(hour), int(minute), int(second)) - except Exception: - dt = datetime.utcnow() - return infer_timezone(lat, lon, dt) + except (TypeError, ValueError) as exc: + raise BadRequest('Invalid birth date') from exc + try: + calculation_service = _load_local_module('domain_calculation_service') + return calculation_service.infer_timezone_offset( + lat=lat, + lon=lon, + local_datetime=dt, + ) + except ValueError as exc: + raise BadRequest(str(exc)) from exc def _get_float(self, body, key, default, min_value=None, max_value=None): value = body.get(key, default) @@ -1960,7 +2073,8 @@ class JyotishAPIHandler(BaseHTTPRequestHandler): } def _enqueue_high_rigor_job(self, body): - job_id = f'hrw_{datetime.utcnow().strftime("%Y%m%d%H%M%S%f")}' + identity = _new_async_job_identity('hrw') + job_id = identity['job_id'] queued_at = datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%SZ') poll_path = f'/api/high_rigor_workflow/jobs/{job_id}' record = { @@ -1972,13 +2086,18 @@ class JyotishAPIHandler(BaseHTTPRequestHandler): 'queued_at': queued_at, 'poll_path': poll_path, 'scope': _HIGH_RIGOR_JOB_SCOPE, + 'access_token': identity['access_token'], + 'expires_at_unix': time.time() + _async_job_ttl_seconds(), } - _write_high_rigor_job_record(job_id, record) + stored_record = dict(record) + stored_record.pop('access_token') + stored_record['access_token_hash'] = _access_token_hash(identity['access_token']) + _write_high_rigor_job_record(job_id, stored_record) body_copy = dict(body or {}) def _run_job() -> None: - running = dict(record) + running = dict(stored_record) running['status'] = 'running' running['started_at'] = datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%SZ') _write_high_rigor_job_record(job_id, running) @@ -1998,15 +2117,12 @@ class JyotishAPIHandler(BaseHTTPRequestHandler): failed['error'] = str(exc) _write_high_rigor_job_record(job_id, failed) - threading.Thread( - target=_run_job, - name=f'high-rigor-job-{job_id}', - daemon=True, - ).start() + _submit_background_job(_run_job) return record def _enqueue_async_job(self, *, scope, endpoint, job_prefix, poll_base, compute_fn): - job_id = f'{job_prefix}_{datetime.utcnow().strftime("%Y%m%d%H%M%S%f")}' + identity = _new_async_job_identity(job_prefix) + job_id = identity['job_id'] queued_at = datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%SZ') poll_path = f'{poll_base}/{job_id}' record = { @@ -2018,11 +2134,16 @@ class JyotishAPIHandler(BaseHTTPRequestHandler): 'queued_at': queued_at, 'poll_path': poll_path, 'scope': scope, + 'access_token': identity['access_token'], + 'expires_at_unix': time.time() + _async_job_ttl_seconds(), } - _write_async_job_record(scope, job_id, record) + stored_record = dict(record) + stored_record.pop('access_token') + stored_record['access_token_hash'] = _access_token_hash(identity['access_token']) + _write_async_job_record(scope, job_id, stored_record) def _run_job() -> None: - running = dict(record) + running = dict(stored_record) running['status'] = 'running' running['started_at'] = datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%SZ') _write_async_job_record(scope, job_id, running) @@ -2042,18 +2163,18 @@ class JyotishAPIHandler(BaseHTTPRequestHandler): failed['error'] = str(exc) _write_async_job_record(scope, job_id, failed) - threading.Thread( - target=_run_job, - name=f'{job_prefix}-job-{job_id}', - daemon=True, - ).start() + _submit_background_job(_run_job) return record def _get_high_rigor_job(self, job_id): - return _load_high_rigor_job_record(job_id) + return _load_high_rigor_job_record(job_id, access_token=self._job_access_token()) def _get_chart_job(self, job_id): - return _load_async_job_record(_API_CHART_CACHE_SCOPE, job_id) + return _load_async_job_record( + _API_CHART_CACHE_SCOPE, + job_id, + access_token=self._job_access_token(), + ) def _high_rigor_birth_payload(self, body): required = ('year', 'month', 'day', 'hour', 'minute', 'lat', 'lon') @@ -3913,29 +4034,6 @@ class JyotishAPIHandler(BaseHTTPRequestHandler): errors.append(str(e)) raise BadRequest('PDF has no extractable text; OCR is not supported yet') - def _calc_vimshottari_periods(self, birth_dt, moon_lon): - extended_dashas = _load_local_module('extended_dashas') - DASHA_ORDER = extended_dashas.DASHA_ORDER - YEAR_DAYS = extended_dashas.YEAR_DAYS - dasha_years = [7, 20, 6, 10, 7, 18, 16, 19, 17] - nak_size = 360 / 27 - nak_idx = int(moon_lon / nak_size) % 27 - start_idx = nak_idx % len(DASHA_ORDER) - current = birth_dt - periods = [] - for i in range(len(DASHA_ORDER)): - idx = (start_idx + i) % len(DASHA_ORDER) - years = dasha_years[idx] - end_date = current + timedelta(days=years * YEAR_DAYS) - periods.append({ - 'lord': DASHA_ORDER[idx], - 'years': years, - 'start': current.strftime('%Y-%m-%d'), - 'end': end_date.strftime('%Y-%m-%d'), - }) - current = end_date - return periods - def _compute_chart(self, body): if body.get('async') or body.get('enqueue'): return self._enqueue_chart_job(body) @@ -3975,78 +4073,69 @@ class JyotishAPIHandler(BaseHTTPRequestHandler): raise BadRequest('Invalid birth date') from e try: - import swisseph as swe - swe.set_ephe_path(os.path.join(SCRIPTS_DIR, '..', 'swiss_ephemeris')) + calculation_service = _load_local_module('domain_calculation_service') + canonical_chart = calculation_service.compute_chart({ + 'year': year, + 'month': month, + 'day': day, + 'hour': hour, + 'minute': minute, + 'second': second, + 'lat': lat, + 'lon': lon, + 'tz': tz, + 'ayanamsa': body.get('ayanamsa', 'lahiri'), + 'node_mode': body.get('node_mode', body.get('nodeMode', 'mean')), + }) + canonical_birth = canonical_chart['birth_info'] + planets_data = canonical_chart['planets'] + ascendant_data = canonical_chart['ascendant'] + asc_lon = float(ascendant_data['lon']) + asc_sign = ascendant_data['sign'] + asc_sign_idx = SIGNS.index(asc_sign) birth_hour_decimal = self._birth_hour_decimal(hour, minute, second) - hour_ut = birth_hour_decimal - tz - jd = swe.julday(year, month, day, hour_ut) - ayanamsa_name = body.get('ayanamsa', 'lahiri') - try: - from jyotish_engine import _apply_ayanamsa, _ayanamsa_display_name - _apply_ayanamsa(ayanamsa_name) - ayanamsa_display = _ayanamsa_display_name(ayanamsa_name) - except ImportError: - swe.set_sid_mode(swe.SIDM_LAHIRI, 0, 0) - ayanamsa_name = 'lahiri' - ayanamsa_display = 'Lahiri' - ayanamsa = swe.get_ayanamsa(jd) + jd = float(canonical_birth['julian_day']) + ayanamsa = float(canonical_birth['ayanamsa']) + ayanamsa_name = canonical_birth['ayanamsa_name'] + ayanamsa_display = canonical_birth['ayanamsa_display'] - planets_data = {} - planet_ids = {'Sun': 0, 'Moon': 1, 'Mars': 4, 'Mercury': 2, 'Jupiter': 5, 'Venus': 3, 'Saturn': 6, 'Rahu': 10, 'Ketu': 20} - planet_names_rev = {v: k for k, v in planet_ids.items()} - - for pid, pname in planet_names_rev.items(): - if pid == 20: - rahu_result, _ = swe.calc_ut(jd, 10) - planet_lon = (rahu_result[0] - ayanamsa + 180) % 360 - else: - result, _ = swe.calc_ut(jd, pid) - planet_lon = (result[0] - ayanamsa) % 360 - sign_idx = int(planet_lon / 30) % 12 - planets_data[pname] = {'lon': planet_lon, 'sign_idx': sign_idx, 'sign': SIGNS[sign_idx], 'degree': planet_lon % 30} - - # Ascendant - asc_tropical = swe.houses_ex(jd, lat, lon, b'E')[0][0] % 360 - asc_lon = (asc_tropical - ayanamsa) % 360 - asc_sign_idx = int(asc_lon / 30) % 12 - asc_sign = SIGNS[asc_sign_idx] - - # Houses houses = {} for h in range(1, 13): - s = (asc_sign_idx + h - 1) % 12 - houses[h] = {'sign': SIGNS[s], 'sign_idx': s} - - # Planet houses - for pn, pd in planets_data.items(): - pd['house'] = ((pd['sign_idx'] - asc_sign_idx) % 12) + 1 - - # Dasha (simplified Vimshottari) - moon_lon = planets_data['Moon']['lon'] - nak_size = 360/27 - nak_idx = int(moon_lon / nak_size) - dasha_lords = ['Ketu','Venus','Sun','Moon','Mars','Rahu','Jupiter','Saturn','Mercury'] - dasha_years = [7,20,6,10,7,18,16,19,17] - nak_lord_idx = nak_idx % 9 - md_lord = dasha_lords[nak_lord_idx] - total_years = dasha_years[nak_lord_idx] - elapsed = (moon_lon % nak_size) / nak_size * total_years - remaining = total_years - elapsed + house = canonical_chart.get('houses', {}).get(f'house_{h}', {}) + sign = house.get('cusp_sign', SIGNS[(asc_sign_idx + h - 1) % 12]) + houses[h] = { + 'sign': sign, + 'sign_idx': SIGNS.index(sign), + 'cusp_degree': house.get('cusp_degree'), + } + moon_lon = float(planets_data['Moon']['lon']) birth_dt = datetime(year, month, day, int(hour), int(minute), int(second)) - elapsed_days = elapsed * 365.25636 - dasha_start = birth_dt - timedelta(days=elapsed_days) if elapsed_days < 365*120 else birth_dt + canonical_dasha = calculation_service.compute_vimshottari_timeline( + birth_dt=birth_dt, + moon_lon=moon_lon, + current_date=birth_dt, + ) + dasha_balance = canonical_dasha['birth_balance'] + md_lord = dasha_balance['lord'] + remaining = dasha_balance['remaining_years'] + total_years = canonical_dasha['periods'][0]['years'] + dasha_start = datetime.strptime(canonical_dasha['periods'][0]['start'], '%Y-%m-%d') - # Yoga detection yogas = self._detect_yogas(planets_data, asc_sign_idx) - - # Sade Sati - from sade_sati import calc_sade_sati_complete - # Transit Saturn (approximate) - saturn_year_progress = (year - 2026) * 12 / 30 # ~12 signs in 30 years - transit_saturn_sign = (planets_data['Saturn']['sign_idx'] + int(saturn_year_progress)) % 12 - transit_saturn_lon = transit_saturn_sign * 30 + 15 - sade_sati = calc_sade_sati_complete(moon_lon, asc_lon, transit_saturn_lon) + reference_date = ( + body.get('transit_date') + or body.get('today') + or body.get('current_date') + or datetime.now().strftime('%Y-%m-%d') + ) + sade_sati = calculation_service.compute_sade_sati( + moon_degree=moon_lon, + asc_degree=asc_lon, + reference_date=reference_date, + tz=tz, + ayanamsa=ayanamsa_name, + ) # Dasha清单 extended_dashas = _load_local_module('extended_dashas') @@ -4120,7 +4209,7 @@ class JyotishAPIHandler(BaseHTTPRequestHandler): 'ayanamsa': round(ayanamsa, 4), 'ayanamsa_name': ayanamsa_name, 'ayanamsa_display': ayanamsa_display, - 'node_mode': body.get('node_mode', body.get('nodeMode', 'mean')), + 'node_mode': canonical_chart['calculation_contract']['effective']['node_mode'], }, 'ascendant': { 'sign': asc_sign, @@ -4135,6 +4224,10 @@ class JyotishAPIHandler(BaseHTTPRequestHandler): 'remaining_years': round(remaining, 2), 'total_years': total_years, 'start_date': dasha_start.isoformat() if hasattr(dasha_start, 'isoformat') else str(dasha_start), + 'periods': canonical_dasha['periods'], + 'birth_balance': canonical_dasha['birth_balance'], + 'calculation_contract': canonical_dasha['calculation_contract'], + 'result_hash': canonical_dasha['result_hash'], }, 'yogas': yogas, 'sade_sati': sade_sati, @@ -4143,6 +4236,8 @@ class JyotishAPIHandler(BaseHTTPRequestHandler): 'special_lagnas': special_lagnas, 'available_dashas': dasha_list, 'dasha_count': len(dasha_list), + 'calculation_contract': canonical_chart['calculation_contract'], + 'result_hash': canonical_chart['result_hash'], } result['modules'] = { 'chart': { @@ -4150,6 +4245,8 @@ class JyotishAPIHandler(BaseHTTPRequestHandler): 'ascendant': result['ascendant'], 'houses': result['houses'], 'birth_info': result['birth'], + 'calculation_contract': result['calculation_contract'], + 'result_hash': result['result_hash'], }, 'dasha': result['dasha'], 'shadbala': {'planets': sb.get('planets', {})} if 'sb' in locals() and isinstance(sb, dict) else {}, @@ -4740,9 +4837,20 @@ class JyotishAPIHandler(BaseHTTPRequestHandler): tithi_num = self._get_int(body, 'tithi_num', 1, 1, 30) vimshottari_analysis = None + canonical_dasha = None if dasha_key == 'vimshottari': - periods = self._calc_vimshottari_periods(birth_dt, moon_lon) - precision = 'calculator' + calculation_service = _load_local_module('domain_calculation_service') + canonical_dasha = calculation_service.compute_vimshottari_timeline( + birth_dt=birth_dt, + moon_lon=moon_lon, + current_date=( + self._parse_optional_date(body.get('today') or body.get('current_date')) + if body.get('today') or body.get('current_date') + else None + ), + ) + periods = canonical_dasha['periods'] + precision = 'canonical_birth_balance' vimshottari_analysis = self._compute_vimshottari_analysis_layer( birth_dt, moon_lon, @@ -4778,6 +4886,10 @@ class JyotishAPIHandler(BaseHTTPRequestHandler): if vimshottari_analysis: result['vimshottari_analysis'] = vimshottari_analysis result['fragment_sources'] = ['dasha_analyzer.py', 'dasha_calculator_enhanced.py'] + if canonical_dasha: + result['birth_balance'] = canonical_dasha['birth_balance'] + result['calculation_contract'] = canonical_dasha['calculation_contract'] + result['result_hash'] = canonical_dasha['result_hash'] return result def _compute_vimshottari_analysis_layer(self, birth_dt, moon_lon, current_date=None): @@ -4853,11 +4965,19 @@ class JyotishAPIHandler(BaseHTTPRequestHandler): } def _compute_sade_sati(self, body): - from sade_sati import calc_sade_sati_complete - return calc_sade_sati_complete( - self._normalize_degree(body, 'moon_degree', 0), - self._normalize_degree(body, 'asc_degree', 0), - self._normalize_degree(body, 'saturn_degree', 0), + calculation_service = _load_local_module('domain_calculation_service') + reference_date = ( + body.get('reference_date') + or body.get('transit_date') + or body.get('current_date') + or datetime.now().strftime('%Y-%m-%d') + ) + return calculation_service.compute_sade_sati( + moon_degree=self._normalize_degree(body, 'moon_degree', 0), + asc_degree=self._normalize_degree(body, 'asc_degree', 0), + reference_date=reference_date, + tz=self._get_float(body, 'tz', 0, -14, 14), + ayanamsa=body.get('ayanamsa', 'lahiri'), ) def _compute_pmc(self, body): @@ -7448,7 +7568,8 @@ def _parse_allowed_origins(value): def start_server(port=5200, host='127.0.0.1', allowed_origins=None): - server = HTTPServer((host, port), JyotishAPIHandler) + server = ThreadingHTTPServer((host, port), JyotishAPIHandler) + server.daemon_threads = True server.allowed_origins = allowed_origins or DEFAULT_ALLOWED_ORIGINS print(f'Jyotish API v6.9.14 running on http://{host}:{port}') print(f' CORS origins: {", ".join(sorted(server.allowed_origins))}') diff --git a/scripts/jyotish_engine.py b/scripts/jyotish_engine.py index 2f83deb3..6a663f6b 100644 --- a/scripts/jyotish_engine.py +++ b/scripts/jyotish_engine.py @@ -735,12 +735,24 @@ def _birth_datetime_from_args(args): def _compute_chart_from_args(args): - return compute_chart_data( - args.year, args.month, args.day, args.hour, args.minute, - args.lat, args.lon, args.tz, getattr(args, 'node_mode', 'mean'), - second=_arg_second(args), - ayanamsa_name=_current_ayanamsa_name(args), - ) + from domain_calculation_service import compute_chart + + result = compute_chart({ + 'year': args.year, + 'month': args.month, + 'day': args.day, + 'hour': args.hour, + 'minute': args.minute, + 'second': _arg_second(args), + 'lat': args.lat, + 'lon': args.lon, + 'tz': args.tz, + 'node_mode': getattr(args, 'node_mode', 'mean'), + 'ayanamsa': _current_ayanamsa_name(args), + }) + asc_idx = SIGNS.index(result['ascendant']['sign']) + birth = result['birth_info'] + return result, asc_idx, birth['julian_day'], birth['ayanamsa'] def _current_ayanamsa_name(args=None): diff --git a/scripts/report_builder.py b/scripts/report_builder.py index bc0c22ea..4d791275 100644 --- a/scripts/report_builder.py +++ b/scripts/report_builder.py @@ -29,6 +29,7 @@ import sys import re import glob import argparse +from urllib.parse import urlparse try: import markdown @@ -352,6 +353,13 @@ def build_section(num, title, md_text): """ +def is_allowed_report_resource_url(url, *, report_url): + if url == report_url: + return True + parsed = urlparse(url) + return parsed.scheme in {'data', 'about', 'blob'} + + def _html_to_pdf(html_path, pdf_path): """Convert HTML to PDF using Playwright headless Chromium.""" try: @@ -364,14 +372,25 @@ def _html_to_pdf(html_path, pdf_path): print(" Launching headless Chromium...") with sync_playwright() as p: browser = p.chromium.launch(headless=True) - page = browser.new_page() - page.goto(f"file://{os.path.abspath(html_path)}", wait_until="networkidle") + context = browser.new_context(java_script_enabled=False) + page = context.new_page() + report_url = f"file://{os.path.abspath(html_path)}" + page.route( + "**/*", + lambda route: ( + route.continue_() + if is_allowed_report_resource_url(route.request.url, report_url=report_url) + else route.abort() + ), + ) + page.goto(report_url, wait_until="networkidle") page.pdf( path=pdf_path, format="A4", print_background=True, margin={"top": "22mm", "bottom": "24mm", "left": "20mm", "right": "20mm"}, ) + context.close() browser.close() size_kb = os.path.getsize(pdf_path) / 1024 diff --git a/scripts/three_engine_parity_runner.py b/scripts/three_engine_parity_runner.py new file mode 100644 index 00000000..f1800b79 --- /dev/null +++ b/scripts/three_engine_parity_runner.py @@ -0,0 +1,168 @@ +#!/usr/bin/env python3 +"""Capture a public same-chart parity packet without overstating oracle closure.""" + +from __future__ import annotations + +import argparse +import json +import sys +from datetime import datetime +from pathlib import Path +from typing import Any + +from domain_calculation_service import compute_chart + + +ROOT = Path(__file__).resolve().parents[1] +PYJHORA_ARTIFACT = ROOT / "references/oracle/artifacts/pyjhora_steve_jobs_dasha_stdout_20260627.txt" +JYOTISHGANIT_ROOT = ROOT / "references/open_source_sources/jyotishganit" + +PUBLIC_CASE = { + "case_id": "steve_jobs_public_1955_lahiri", + "year": 1955, + "month": 2, + "day": 24, + "hour": 19, + "minute": 15, + "second": 0, + "lat": 37.7749, + "lon": -122.4194, + "tz": -8.0, + "ayanamsa": "lahiri", + "node_mode": "mean", +} + + +def _write_json(path: Path, value: dict[str, Any]) -> Path: + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(json.dumps(value, ensure_ascii=False, indent=2, sort_keys=True), encoding="utf-8") + return path + + +def _capture_jyotishganit_raw(output_dir: Path) -> tuple[dict[str, Any], str]: + sys.path.insert(0, str(JYOTISHGANIT_ROOT)) + try: + from jyotishganit import calculate_birth_chart, get_birth_chart_json + + chart = calculate_birth_chart( + datetime( + PUBLIC_CASE["year"], + PUBLIC_CASE["month"], + PUBLIC_CASE["day"], + PUBLIC_CASE["hour"], + PUBLIC_CASE["minute"], + PUBLIC_CASE["second"], + ), + PUBLIC_CASE["lat"], + PUBLIC_CASE["lon"], + PUBLIC_CASE["tz"], + location_name="San Francisco, CA", + name="Steve Jobs (public benchmark)", + ) + raw = get_birth_chart_json(chart) + path = _write_json(output_dir / "jyotishganit_raw.json", raw) + return raw, str(path) + except Exception as exc: + return {"error": f"{exc.__class__.__name__}: {exc}"}, "" + finally: + try: + sys.path.remove(str(JYOTISHGANIT_ROOT)) + except ValueError: + pass + + +def _vedastro_state(*, allow_network: bool) -> dict[str, Any]: + if not allow_network: + return { + "status": "blocked", + "official_raw_response_path": "", + "reason": "network_disabled_for_public_replay", + } + return { + "status": "blocked", + "official_raw_response_path": "", + "reason": "official_runner_requires_explicit_raw_capture_workflow", + } + + +def build_public_case_replay(*, output_dir: Path, allow_vedastro_network: bool = False) -> dict[str, Any]: + output_dir.mkdir(parents=True, exist_ok=True) + local = compute_chart(PUBLIC_CASE) + jyotishganit_raw, jyotishganit_path = _capture_jyotishganit_raw(output_dir) + pyjhora_available = PYJHORA_ARTIFACT.is_file() + vedastro = _vedastro_state(allow_network=allow_vedastro_network) + + rows = [ + { + "section": "D1", + "field": "Sun.longitude", + "local_value": local["planets"]["Sun"]["lon"], + "oracle_values": { + "VedAstro": None, + "PyJHora_JHora": None, + "jyotishganit": None, + }, + "status": "blocked", + "reason": "raw_values_not_normalized_across_all_three_engines", + }, + { + "section": "Panchanga", + "field": "raw_capture", + "local_value": None, + "oracle_values": { + "VedAstro": None, + "PyJHora_JHora": "dasha_only_artifact", + "jyotishganit": "captured" if jyotishganit_path else None, + }, + "status": "not_comparable", + "reason": "three_engine_scope_does_not_share_this_normalized_field", + }, + ] + report = { + "case_id": PUBLIC_CASE["case_id"], + "birth_data_policy": "public_case_only", + "status": "partial" if pyjhora_available and jyotishganit_path else "blocked", + "tested": False, + "blocked_reason": "official_vedastro_raw_missing_or_unverified", + "engines": { + "VedAstro": vedastro, + "PyJHora_JHora": { + "status": "raw_imported" if pyjhora_available else "blocked", + "raw_output_path": str(PYJHORA_ARTIFACT) if pyjhora_available else "", + "settings": {"ayanamsa": "LAHIRI", "node_mode": "PyJHora default"}, + }, + "jyotishganit": { + "status": "raw_captured" if jyotishganit_path else "blocked", + "raw_output_path": jyotishganit_path, + "error": jyotishganit_raw.get("error") if isinstance(jyotishganit_raw, dict) else None, + }, + }, + "local": { + "result_hash": local["result_hash"], + "calculation_contract": local["calculation_contract"], + }, + "comparison_rows": rows, + "runtime_boundary": ( + "This packet has real public raw artifacts but remains unverified until a VedAstro " + "official raw response and normalized three-engine field comparison are imported." + ), + } + _write_json(output_dir / "three_engine_parity_replay.json", report) + return report + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("--output-dir", default="scratch/local/three_engine_parity") + parser.add_argument("--allow-vedastro-network", action="store_true") + args = parser.parse_args() + report = build_public_case_replay( + output_dir=ROOT / args.output_dir, + allow_vedastro_network=args.allow_vedastro_network, + ) + print(json.dumps(report, ensure_ascii=False, indent=2)) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/timezone_utils.py b/scripts/timezone_utils.py index 7918b001..a4febdb1 100644 --- a/scripts/timezone_utils.py +++ b/scripts/timezone_utils.py @@ -1,17 +1,5 @@ from datetime import datetime -import logging +def infer_timezone(lat: float, lon: float, dt: datetime) -> float: + from domain_calculation_service import infer_timezone_offset -def infer_timezone(lat: float, lon: float, dt: datetime, default: float = 8.0) -> float: - try: - from timezonefinder import TimezoneFinder - import pytz - tf = TimezoneFinder() - tz_name = tf.timezone_at(lng=lon, lat=lat) - if tz_name: - offset_seconds = pytz.timezone(tz_name).localize(dt).utcoffset().total_seconds() - offset = float(offset_seconds / 3600.0) - logging.info(f"[Timezone Auth] Detected {tz_name} offset {offset} for {dt}") - return offset - except Exception as e: - logging.warning(f"Timezone inference failed: {e}") - return float(default) + return infer_timezone_offset(lat=lat, lon=lon, local_datetime=dt) diff --git a/tests/conftest.py b/tests/conftest.py index aa374ed5..21b15bca 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -8,6 +8,16 @@ from pathlib import Path ROOT = Path(__file__).resolve().parents[1] SCRIPTS = str(ROOT / "scripts") WORKBUDDY_SKILL_SCRIPTS = ".workbuddy/skills/jyotish-vedic-astrology/scripts" +SLOW_API_SECURITY_PREFIXES = ( + "test_vedastro_", + "test_high_rigor_", + "test_professional_reading", + "test_api_prompt_pack", + "test_consultation_workflow", + "test_thematic_report", + "test_capability_audit", + "test_technique_catalog", +) def ensure_project_scripts_first() -> None: @@ -28,4 +38,10 @@ def pytest_runtest_setup() -> None: ensure_project_scripts_first() +def pytest_collection_modifyitems(items) -> None: + for item in items: + if item.fspath.basename == "test_api_server_security.py" and item.name.startswith(SLOW_API_SECURITY_PREFIXES): + item.add_marker("slow") + + ensure_project_scripts_first() diff --git a/tests/test_api_server_security.py b/tests/test_api_server_security.py index f0325d00..a58e2205 100644 --- a/tests/test_api_server_security.py +++ b/tests/test_api_server_security.py @@ -134,7 +134,10 @@ class _HighRigorJobCaptureHandler(JyotishAPIHandler): class _PostCaptureHandler(JyotishAPIHandler): def __init__(self, path: str, payload: dict) -> None: raw = json.dumps(payload).encode('utf-8') - self.headers = _FakeHeaders({'Content-Length': str(len(raw))}) + self.headers = _FakeHeaders({ + 'Content-Length': str(len(raw)), + 'Content-Type': 'application/json', + }) self.server = _FakeServer() self.path = path self.rfile = BytesIO(raw) @@ -3377,7 +3380,7 @@ def test_chart_async_submit_returns_job_id(monkeypatch: pytest.MonkeyPatch) -> N def test_high_rigor_job_poll_endpoint_returns_cached_job_payload(monkeypatch: pytest.MonkeyPatch) -> None: - monkeypatch.setattr(jyotish_api_server, '_load_high_rigor_job_record', lambda job_id: { + monkeypatch.setattr(jyotish_api_server, '_load_high_rigor_job_record', lambda job_id, **_kwargs: { 'success': True, 'endpoint': 'high_rigor_workflow_async', 'mode': 'async_result', @@ -3397,7 +3400,7 @@ def test_high_rigor_job_poll_endpoint_returns_cached_job_payload(monkeypatch: py def test_chart_job_poll_endpoint_returns_cached_job_payload(monkeypatch: pytest.MonkeyPatch) -> None: - monkeypatch.setattr(jyotish_api_server, '_load_async_job_record', lambda scope, job_id: { + monkeypatch.setattr(jyotish_api_server, '_load_async_job_record', lambda scope, job_id, **_kwargs: { 'success': True, 'endpoint': 'chart_async', 'mode': 'async_result', diff --git a/tests/test_calculation_p0_regressions.py b/tests/test_calculation_p0_regressions.py new file mode 100644 index 00000000..05338e08 --- /dev/null +++ b/tests/test_calculation_p0_regressions.py @@ -0,0 +1,117 @@ +from __future__ import annotations + +import sys +from datetime import datetime +from pathlib import Path +from types import SimpleNamespace + +import pytest + +SCRIPTS = Path(__file__).resolve().parents[1] / "scripts" +if str(SCRIPTS) not in sys.path: + sys.path.insert(0, str(SCRIPTS)) + +import domain_calculation_service as calculation_service # noqa: E402 +import jyotish_api_server # noqa: E402 +from jyotish_api_server import JyotishAPIHandler # noqa: E402 +from jyotish_engine import _compute_chart_from_args # noqa: E402 + +BIRTH = { + "year": 1990, + "month": 1, + "day": 1, + "hour": 12, + "minute": 0, + "second": 0, + "lat": 28.6139, + "lon": 77.2090, + "tz": 5.5, + "ayanamsa": "lahiri", +} + + +def test_true_node_changes_effective_rahu_and_contract() -> None: + mean = calculation_service.compute_chart({**BIRTH, "node_mode": "mean"}) + true = calculation_service.compute_chart({**BIRTH, "node_mode": "true"}) + + assert mean["planets"]["Rahu"]["lon"] != pytest.approx( + true["planets"]["Rahu"]["lon"], abs=1e-8 + ) + assert mean["calculation_contract"]["effective"]["node_mode"] == "mean" + assert true["calculation_contract"]["effective"]["node_mode"] == "true" + assert mean["result_hash"] != true["result_hash"] + + +def test_vimshottari_uses_birth_balance_as_canonical_timeline() -> None: + birth_dt = datetime(1990, 1, 1, 12, 0) + result = calculation_service.compute_vimshottari_timeline( + birth_dt=birth_dt, + moon_lon=100.0, + current_date=birth_dt, + ) + + first = result["periods"][0] + assert first["lord"] == "Saturn" + assert first["start"] == "1980-07-02" + assert first["end"] == "1999-07-02" + assert result["birth_balance"]["remaining_years"] == pytest.approx(9.5) + assert result["calculation_contract"]["algorithm"] == "vimshottari_birth_balance" + + +def test_sade_sati_uses_real_saturn_transit_for_reference_date() -> None: + result = calculation_service.compute_sade_sati( + moon_degree=300.0, + asc_degree=330.0, + reference_date="2026-07-11", + tz=5.5, + ayanamsa="lahiri", + ) + oracle = calculation_service.compute_transit_longitude( + planet="Saturn", + reference_date="2026-07-11", + tz=5.5, + ayanamsa="lahiri", + ) + + assert result["transit_saturn_lon"] == pytest.approx(oracle["longitude"], abs=1e-8) + assert result["provenance"]["data_layer"] == "true_transit_positions" + assert result["provenance"]["reference_date"] == "2026-07-11" + + +def test_timezone_inference_fails_closed(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setattr( + calculation_service, + "_lookup_timezone_name", + lambda _lat, _lon: None, + ) + + with pytest.raises(calculation_service.TimezoneInferenceError, match="timezone inference"): + calculation_service.infer_timezone_offset( + lat=0.0, + lon=0.0, + local_datetime=datetime(1990, 1, 1, 12, 0), + ) + + +def test_chart_hash_matches_domain_cli_and_rest( + monkeypatch: pytest.MonkeyPatch, +) -> None: + monkeypatch.setenv("JYOTISH_API_CHART_CACHE_TTL_SECONDS", "0") + monkeypatch.setenv("VEDASTRO_ENABLE_NETWORK", "0") + monkeypatch.setattr( + jyotish_api_server, + "_attach_vedastro_main_entry_overview", + lambda result, _birth: result, + ) + expected = calculation_service.compute_chart({**BIRTH, "node_mode": "true"}) + cli, _asc_idx, _jd, _ayanamsa = _compute_chart_from_args( + SimpleNamespace(**BIRTH, node_mode="true") + ) + rest = JyotishAPIHandler.__new__(JyotishAPIHandler)._compute_chart_sync( + {**BIRTH, "node_mode": "true", "transit_date": "2026-07-11"} + ) + + assert cli["result_hash"] == expected["result_hash"] + assert rest["result_hash"] == expected["result_hash"] + assert rest["birth"]["node_mode"] == "true" + assert rest["calculation_contract"]["effective"]["node_mode"] == "true" diff --git a/tests/test_runtime_security_p0.py b/tests/test_runtime_security_p0.py new file mode 100644 index 00000000..07bb0b81 --- /dev/null +++ b/tests/test_runtime_security_p0.py @@ -0,0 +1,156 @@ +from __future__ import annotations + +import hashlib +import sys +import time +from pathlib import Path + +import pytest + +ROOT = Path(__file__).resolve().parents[1] +SCRIPTS = ROOT / "scripts" +if str(SCRIPTS) not in sys.path: + sys.path.insert(0, str(SCRIPTS)) + +import jyotish_api_server as api # noqa: E402 +import report_builder # noqa: E402 + + +class _Headers(dict): + def get(self, key, default=None): + return super().get(key, default) + + +class _Server: + allowed_origins = {"http://localhost:3456"} + server_address = ("127.0.0.1", 5200) + + +def _handler(headers: dict[str, str]): + handler = api.JyotishAPIHandler.__new__(api.JyotishAPIHandler) + handler.headers = _Headers(headers) + handler.server = _Server() + return handler + + +def test_untrusted_origin_is_rejected_before_post_side_effects() -> None: + handler = _handler( + { + "Origin": "https://evil.example", + "Host": "127.0.0.1:5200", + "Content-Type": "application/json", + } + ) + with pytest.raises(api.Forbidden, match="Origin"): + handler._enforce_request_security(require_json=True) + + +def test_post_requires_json_content_type() -> None: + handler = _handler( + { + "Origin": "http://localhost:3456", + "Host": "127.0.0.1:5200", + "Content-Type": "text/plain", + } + ) + with pytest.raises(api.UnsupportedMediaType): + handler._enforce_request_security(require_json=True) + + +@pytest.mark.parametrize( + "url", + [ + "https://example.com/image.png", + "http://127.0.0.1:8080/private", + "file:///etc/passwd", + "ftp://example.com/file", + ], +) +def test_report_renderer_blocks_external_and_local_resources(url: str) -> None: + assert report_builder.is_allowed_report_resource_url( + url, + report_url="file:///tmp/report.html", + ) is False + + +def test_report_renderer_allows_only_document_and_embedded_resources() -> None: + assert report_builder.is_allowed_report_resource_url( + "file:///tmp/report.html", + report_url="file:///tmp/report.html", + ) is True + assert report_builder.is_allowed_report_resource_url( + "data:image/png;base64,AA==", + report_url="file:///tmp/report.html", + ) is True + + +def test_async_job_identity_is_random_and_capability_protected( + monkeypatch: pytest.MonkeyPatch, + tmp_path: Path, +) -> None: + monkeypatch.setattr(api, "_async_job_dir", lambda _scope: tmp_path) + first = api._new_async_job_identity("chart") + second = api._new_async_job_identity("chart") + assert first["job_id"] != second["job_id"] + assert len(first["job_id"].split("_", 1)[1]) >= 32 + assert first["access_token"] != second["access_token"] + + record = { + "job_id": first["job_id"], + "status": "queued", + "access_token_hash": hashlib.sha256(first["access_token"].encode()).hexdigest(), + "expires_at_unix": time.time() + 60, + } + api._write_async_job_record("chart", first["job_id"], record) + assert api._load_async_job_record( + "chart", first["job_id"], access_token=first["access_token"] + )["status"] == "queued" + with pytest.raises(api.JobAccessDenied): + api._load_async_job_record("chart", first["job_id"], access_token="wrong") + + +def test_expired_async_job_is_deleted( + monkeypatch: pytest.MonkeyPatch, + tmp_path: Path, +) -> None: + monkeypatch.setattr(api, "_async_job_dir", lambda _scope: tmp_path) + identity = api._new_async_job_identity("chart") + api._write_async_job_record( + "chart", + identity["job_id"], + { + "job_id": identity["job_id"], + "access_token_hash": hashlib.sha256(identity["access_token"].encode()).hexdigest(), + "expires_at_unix": time.time() - 1, + }, + ) + assert api._load_async_job_record( + "chart", identity["job_id"], access_token=identity["access_token"] + ) is None + assert not (tmp_path / f"{identity['job_id']}.json").exists() + + +def test_authenticated_frontend_does_not_use_local_storage_api_base() -> None: + auth_source = (ROOT / "jyotish-app" / "auth.js").read_text(encoding="utf-8") + chat_source = (ROOT / "jyotish-app" / "ai-chat.js").read_text(encoding="utf-8") + assert "localStorage.getItem(API_BASE_KEY)" not in auth_source + assert "localStorage.getItem('jyotish_api_base')" not in chat_source + + +def test_frontend_async_poll_uses_ephemeral_job_capability() -> None: + bridge_source = (ROOT / "jyotish-app" / "api-bridge.js").read_text(encoding="utf-8") + assert "pollAsyncJob(data, { base })" in bridge_source + assert "Authorization: `Bearer ${job.access_token}`" in bridge_source + assert "sessionStorage.setItem('jyotish_job" not in bridge_source + + +def test_background_job_queue_rejects_when_capacity_is_full( + monkeypatch: pytest.MonkeyPatch, +) -> None: + class _FullCapacity: + def acquire(self, blocking=False): + return False + + monkeypatch.setattr(api, "_ASYNC_JOB_CAPACITY", _FullCapacity()) + with pytest.raises(api.JobQueueFull): + api._submit_background_job(lambda: None) diff --git a/tests/test_three_engine_parity_runner.py b/tests/test_three_engine_parity_runner.py new file mode 100644 index 00000000..3887cbc8 --- /dev/null +++ b/tests/test_three_engine_parity_runner.py @@ -0,0 +1,26 @@ +from __future__ import annotations + +import sys +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] +SCRIPTS = ROOT / "scripts" +if str(SCRIPTS) not in sys.path: + sys.path.insert(0, str(SCRIPTS)) + +from three_engine_parity_runner import build_public_case_replay # noqa: E402 + + +def test_public_same_chart_replay_never_promotes_missing_vedastro_raw(tmp_path: Path) -> None: + report = build_public_case_replay(output_dir=tmp_path, allow_vedastro_network=False) + + assert report["case_id"] == "steve_jobs_public_1955_lahiri" + assert report["birth_data_policy"] == "public_case_only" + assert report["engines"]["PyJHora_JHora"]["status"] == "raw_imported" + assert report["engines"]["jyotishganit"]["status"] == "raw_captured" + assert report["engines"]["VedAstro"]["status"] == "blocked" + assert report["status"] in {"partial", "blocked"} + assert report["tested"] is False + assert report["comparison_rows"] + assert all(row["status"] in {"blocked", "not_comparable"} for row in report["comparison_rows"]) From 1856b307dff1ee96eef280a303059e4a307c31bb Mon Sep 17 00:00:00 2001 From: 732642856 <732642856@qq.com> Date: Sun, 12 Jul 2026 00:27:25 +0800 Subject: [PATCH 02/61] strengthen real-case calibration safeguards --- .gitignore | 5 +- .../real_case_calibration/catalog.schema.json | 164 ++---- .../replay_manifest.json | 146 ++++- scripts/muntha.py | 2 +- scripts/public_real_case_benchmark.py | 538 ++++++++++++++++++ scripts/public_real_case_negative_controls.py | 122 ++++ scripts/real_case_replay_validator.py | 54 +- scripts/unified_consultation_orchestrator.py | 132 ++++- tests/test_muntha_module.py | 10 + tests/test_public_real_case_benchmark.py | 204 +++++++ tests/test_public_release_privacy_scan.py | 6 + tests/test_real_case_negative_controls.py | 53 ++ tests/test_real_case_replay_validator.py | 181 +++++- .../test_unified_consultation_orchestrator.py | 7 +- 14 files changed, 1493 insertions(+), 131 deletions(-) create mode 100644 scripts/public_real_case_benchmark.py create mode 100644 scripts/public_real_case_negative_controls.py create mode 100644 tests/test_muntha_module.py create mode 100644 tests/test_public_real_case_benchmark.py create mode 100644 tests/test_real_case_negative_controls.py diff --git a/.gitignore b/.gitignore index c2675f67..5d614461 100644 --- a/.gitignore +++ b/.gitignore @@ -5,9 +5,8 @@ __pycache__/ *.sqlite .agents/ venv_vedastro/ -scratch/local/ -scratch/test_*.json -scratch/test_*.txt +/scratch/ +/.serena/ test_hooks.json hermes_memory.db/ *.egg-info/ diff --git a/references/real_case_calibration/catalog.schema.json b/references/real_case_calibration/catalog.schema.json index cc579bda..1377f301 100644 --- a/references/real_case_calibration/catalog.schema.json +++ b/references/real_case_calibration/catalog.schema.json @@ -2,138 +2,86 @@ "$schema": "https://json-schema.org/draft/2020-12/schema", "title": "Real Case Calibration Catalog", "type": "object", - "required": [ - "case_id", - "source", - "chart_signature", - "event_outcomes", - "similarity", - "replay" - ], + "required": ["case_id", "subject", "source", "chart_signature", "event_outcomes", "similarity", "replay"], "properties": { - "case_id": { - "type": "string" - }, - "source": { + "case_id": {"type": "string"}, + "subject": { "type": "object", - "required": [ - "url", - "source_grade", - "license_or_quote_boundary" - ], + "required": ["name", "year", "month", "day", "hour", "minute", "lat", "lon", "tz", "node_mode", "birth_source"], "properties": { - "url": { - "type": "string" - }, - "source_grade": { - "type": "string", - "enum": [ - "primary", - "verified_secondary", - "forum_claim", - "unverified" - ] - }, - "license_or_quote_boundary": { - "type": "string" - } - } - }, - "chart_signature": { - "type": "object", - "properties": { - "lagna": { - "type": "string" - }, - "moon_sign": { - "type": "string" - }, - "d9_lagna": { - "type": "string" - }, - "ul": { - "type": "string" - }, - "a7": { - "type": "string" - }, - "a10": { - "type": "string" - }, - "notable_yogas": { - "type": "array", - "items": { - "type": "string" + "name": {"type": "string"}, + "year": {"type": "integer"}, + "month": {"type": "integer", "minimum": 1, "maximum": 12}, + "day": {"type": "integer", "minimum": 1, "maximum": 31}, + "hour": {"type": "integer", "minimum": 0, "maximum": 23}, + "minute": {"type": "integer", "minimum": 0, "maximum": 59}, + "lat": {"type": "number"}, + "lon": {"type": "number"}, + "tz": {"type": "number"}, + "node_mode": {"enum": ["mean", "true"]}, + "birth_source": { + "type": "object", + "required": ["url", "source_grade", "time_accuracy_rating", "evidence_basis"], + "properties": { + "url": {"type": "string"}, + "source_grade": {"enum": ["primary", "verified_secondary"]}, + "time_accuracy_rating": {"enum": ["A", "AA"]}, + "evidence_basis": {"type": "string"} } } } }, + "source": { + "type": "object", + "required": ["url", "source_grade", "license_or_quote_boundary"], + "properties": { + "url": {"type": "string"}, + "source_grade": {"enum": ["primary", "verified_secondary", "forum_claim", "unverified"]}, + "license_or_quote_boundary": {"type": "string"} + } + }, + "chart_signature": {"type": "object"}, "event_outcomes": { "type": "array", + "minItems": 1, "items": { "type": "object", - "required": [ - "event_type", - "event_date", - "outcome" - ], + "required": ["event_type", "event_date", "domain", "expected_label", "outcome", "source"], "properties": { - "event_type": { - "type": "string" - }, - "event_date": { - "type": "string" - }, - "outcome": { - "type": "string" - }, - "source_excerpt_note": { - "type": "string" + "event_type": {"type": "string"}, + "event_date": {"type": "string"}, + "domain": {"enum": ["career", "marriage"]}, + "expected_label": {"enum": ["career_status", "legal_marriage"]}, + "outcome": {"type": "string"}, + "source_excerpt_note": {"type": "string"}, + "source": { + "type": "object", + "required": ["url", "source_grade"], + "properties": { + "url": {"type": "string"}, + "source_grade": {"enum": ["primary", "verified_secondary"]} + } } } } }, "similarity": { "type": "object", + "required": ["score", "matching_factors", "dissimilar_factors"], "properties": { - "score": { - "type": "number" - }, - "matching_factors": { - "type": "array", - "items": { - "type": "string" - } - }, - "dissimilar_factors": { - "type": "array", - "items": { - "type": "string" - } - } + "score": {"type": "number"}, + "matching_factors": {"type": "array", "items": {"type": "string"}}, + "dissimilar_factors": {"type": "array", "items": {"type": "string"}} } }, "replay": { "type": "object", + "required": ["outcome_replay_status", "do_not_use_for_prediction"], "properties": { - "outcome_replay_status": { - "type": "string", - "enum": [ - "not_started", - "blocked", - "partial", - "complete" - ] - }, - "conflict_notes": { - "type": "array", - "items": { - "type": "string" - } - }, - "do_not_use_for_prediction": { - "type": "boolean" - } + "outcome_replay_status": {"enum": ["pending", "replayed", "blocked"]}, + "do_not_use_for_prediction": {"type": "boolean"}, + "report_path": {"type": "string"}, + "conflict_notes": {"type": "array", "items": {"type": "string"}} } } } diff --git a/references/real_case_calibration/replay_manifest.json b/references/real_case_calibration/replay_manifest.json index 42ad725a..c323cb8a 100644 --- a/references/real_case_calibration/replay_manifest.json +++ b/references/real_case_calibration/replay_manifest.json @@ -1,8 +1,144 @@ { - "schema_version": "1.0", - "status": "contract_ready_no_cases", + "schema_version": "2.0", + "status": "ready", "case_schema": "references/real_case_calibration/catalog.schema.json", - "cases": [], - "blocked_reason": "no_structured_outcome_replay_cases_imported", - "runtime_boundary": "This manifest defines the replay import contract only. No real-case outcome replay is complete until structured cases are imported and validated." + "selection_policy": { + "birth_time_minimum": "Rodden A", + "event_source_minimum": "verified_secondary", + "domains": {"career": 5, "marriage": 5}, + "privacy": "public_figures_only_no_user_birth_data" + }, + "cases": [ + { + "case_id": "jobs_iphone_2007", + "subject": { + "name": "Steve Jobs", "year": 1955, "month": 2, "day": 24, "hour": 19, "minute": 15, + "lat": 37.7833, "lon": -122.4167, "tz": -8.0, "node_mode": "mean", + "birth_source": {"url": "https://www.astro.com/adbvip/adbvip_02_24.htm", "source_grade": "primary", "time_accuracy_rating": "AA", "evidence_basis": "BC/BR in hand"} + }, + "source": {"url": "https://www.astro.com/adbvip/adbvip_02_24.htm", "source_grade": "primary", "license_or_quote_boundary": "facts_and_short_summary_only"}, + "chart_signature": {"benchmark_role": "blind_outcome_replay", "external_oracle_status": "pending"}, + "event_outcomes": [{"event_type": "career_breakthrough", "event_date": "2007-01-09", "domain": "career", "expected_label": "career_status", "outcome": "Apple publicly introduced the iPhone", "source_excerpt_note": "Apple Newsroom dates the announcement to 9 January 2007.", "source": {"url": "https://www.apple.com/newsroom/2007/01/09Apple-Reinvents-the-Phone-with-iPhone/", "source_grade": "primary"}}], + "similarity": {"score": 0.0, "matching_factors": ["not_user_similarity_benchmark"], "dissimilar_factors": []}, + "replay": {"outcome_replay_status": "replayed", "do_not_use_for_prediction": false, "report_path": "docs/benchmark/public_real_case_benchmark_2026_07_11.json"} + }, + { + "case_id": "obama_election_2008", + "subject": { + "name": "Barack Obama", "year": 1961, "month": 8, "day": 4, "hour": 19, "minute": 24, + "lat": 21.3, "lon": -157.8667, "tz": -10.0, "node_mode": "mean", + "birth_source": {"url": "https://www.astro.com/adbvip/adbvip_08_04.htm", "source_grade": "primary", "time_accuracy_rating": "AA", "evidence_basis": "BC/BR in hand"} + }, + "source": {"url": "https://www.astro.com/adbvip/adbvip_08_04.htm", "source_grade": "primary", "license_or_quote_boundary": "facts_and_short_summary_only"}, + "chart_signature": {"benchmark_role": "blind_outcome_replay", "external_oracle_status": "pending"}, + "event_outcomes": [{"event_type": "career_status", "event_date": "2008-11-04", "domain": "career", "expected_label": "career_status", "outcome": "Won the United States presidential election", "source_excerpt_note": "Federal Elections 2008 records the presidential general election.", "source": {"url": "https://www.fec.gov/introduction-campaign-finance/election-results-and-voting-information/federal-elections-2008/", "source_grade": "primary"}}], + "similarity": {"score": 0.0, "matching_factors": ["not_user_similarity_benchmark"], "dissimilar_factors": []}, + "replay": {"outcome_replay_status": "replayed", "do_not_use_for_prediction": false, "report_path": "docs/benchmark/public_real_case_benchmark_2026_07_11.json"} + }, + { + "case_id": "schwarzenegger_governor_2003", + "subject": { + "name": "Arnold Schwarzenegger", "year": 1947, "month": 7, "day": 30, "hour": 4, "minute": 10, + "lat": 47.0833, "lon": 15.45, "tz": 2.0, "node_mode": "mean", + "birth_source": {"url": "https://www.astro.com/adbvip/adbvip_07_30.htm", "source_grade": "verified_secondary", "time_accuracy_rating": "A", "evidence_basis": "from memory"} + }, + "source": {"url": "https://www.astro.com/adbvip/adbvip_07_30.htm", "source_grade": "verified_secondary", "license_or_quote_boundary": "facts_and_short_summary_only"}, + "chart_signature": {"benchmark_role": "blind_outcome_replay", "external_oracle_status": "pending"}, + "event_outcomes": [{"event_type": "career_status", "event_date": "2003-10-07", "domain": "career", "expected_label": "career_status", "outcome": "Won the California gubernatorial recall election", "source_excerpt_note": "California Secretary of State Statement of Vote dates the election to 7 October 2003.", "source": {"url": "https://elections.cdn.sos.ca.gov/sov/2003-special/sov-complete.pdf", "source_grade": "primary"}}], + "similarity": {"score": 0.0, "matching_factors": ["not_user_similarity_benchmark"], "dissimilar_factors": []}, + "replay": {"outcome_replay_status": "replayed", "do_not_use_for_prediction": false, "report_path": "docs/benchmark/public_real_case_benchmark_2026_07_11.json"} + }, + { + "case_id": "streep_oscar_1983", + "subject": { + "name": "Meryl Streep", "year": 1949, "month": 6, "day": 22, "hour": 8, "minute": 5, + "lat": 40.7333, "lon": -74.3667, "tz": -4.0, "node_mode": "mean", + "birth_source": {"url": "https://www.astro.com/adbvip/adbvip_06_22.htm", "source_grade": "primary", "time_accuracy_rating": "AA", "evidence_basis": "BC/BR in hand"} + }, + "source": {"url": "https://www.astro.com/adbvip/adbvip_06_22.htm", "source_grade": "primary", "license_or_quote_boundary": "facts_and_short_summary_only"}, + "chart_signature": {"benchmark_role": "blind_outcome_replay", "external_oracle_status": "pending"}, + "event_outcomes": [{"event_type": "career_award", "event_date": "1983-04-11", "domain": "career", "expected_label": "career_status", "outcome": "Won Best Actress for Sophie's Choice", "source_excerpt_note": "The 55th Academy Awards ceremony occurred on 11 April 1983.", "source": {"url": "https://www.oscars.org/oscars/ceremonies/1983", "source_grade": "primary"}}], + "similarity": {"score": 0.0, "matching_factors": ["not_user_similarity_benchmark"], "dissimilar_factors": []}, + "replay": {"outcome_replay_status": "replayed", "do_not_use_for_prediction": false, "report_path": "docs/benchmark/public_real_case_benchmark_2026_07_11.json"} + }, + { + "case_id": "aniston_emmy_2002", + "subject": { + "name": "Jennifer Aniston", "year": 1969, "month": 2, "day": 11, "hour": 22, "minute": 22, + "lat": 34.05, "lon": -118.25, "tz": -8.0, "node_mode": "mean", + "birth_source": {"url": "https://www.astro.com/adbvip/adbvip_02_11.htm", "source_grade": "primary", "time_accuracy_rating": "AA", "evidence_basis": "quoted BC/BR"} + }, + "source": {"url": "https://www.astro.com/adbvip/adbvip_02_11.htm", "source_grade": "primary", "license_or_quote_boundary": "facts_and_short_summary_only"}, + "chart_signature": {"benchmark_role": "blind_outcome_replay", "external_oracle_status": "pending"}, + "event_outcomes": [{"event_type": "career_award", "event_date": "2002-09-22", "domain": "career", "expected_label": "career_status", "outcome": "Won the Primetime Emmy for lead actress in a comedy series", "source_excerpt_note": "Television Academy lists the 2002 category result.", "source": {"url": "https://www.televisionacademy.com/awards/nominees-winners/2002/outstanding-lead-actress-in-a-comedy-series", "source_grade": "primary"}}], + "similarity": {"score": 0.0, "matching_factors": ["not_user_similarity_benchmark"], "dissimilar_factors": []}, + "replay": {"outcome_replay_status": "replayed", "do_not_use_for_prediction": false, "report_path": "docs/benchmark/public_real_case_benchmark_2026_07_11.json"} + }, + { + "case_id": "william_marriage_2011", + "subject": { + "name": "William, Prince of Wales", "year": 1982, "month": 6, "day": 21, "hour": 21, "minute": 3, + "lat": 51.5333, "lon": -0.2, "tz": 1.0, "node_mode": "mean", + "birth_source": {"url": "https://www.astro.com/adbvip/adbvip_06_21.htm", "source_grade": "primary", "time_accuracy_rating": "AA", "evidence_basis": "BC/BR in hand"} + }, + "source": {"url": "https://www.astro.com/adbvip/adbvip_06_21.htm", "source_grade": "primary", "license_or_quote_boundary": "facts_and_short_summary_only"}, + "chart_signature": {"benchmark_role": "blind_outcome_replay", "external_oracle_status": "pending"}, + "event_outcomes": [{"event_type": "legal_marriage", "event_date": "2011-04-29", "domain": "marriage", "expected_label": "legal_marriage", "outcome": "Married Catherine Middleton", "source_excerpt_note": "Royal Family records the wedding on 29 April 2011.", "source": {"url": "https://www.royal.uk/wedding-prince-william-and-miss-catherine-middleton", "source_grade": "primary"}}], + "similarity": {"score": 0.0, "matching_factors": ["not_user_similarity_benchmark"], "dissimilar_factors": []}, + "replay": {"outcome_replay_status": "replayed", "do_not_use_for_prediction": false, "report_path": "docs/benchmark/public_real_case_benchmark_2026_07_11.json"} + }, + { + "case_id": "jolie_marriage_2014", + "subject": { + "name": "Angelina Jolie", "year": 1975, "month": 6, "day": 4, "hour": 9, "minute": 9, + "lat": 34.0961, "lon": -118.2944, "tz": -7.0, "node_mode": "mean", + "birth_source": {"url": "https://www.astro.com/adbvip/adbvip_06_04.htm", "source_grade": "primary", "time_accuracy_rating": "AA", "evidence_basis": "quoted BC/BR"} + }, + "source": {"url": "https://www.astro.com/adbvip/adbvip_06_04.htm", "source_grade": "primary", "license_or_quote_boundary": "facts_and_short_summary_only"}, + "chart_signature": {"benchmark_role": "blind_outcome_replay", "external_oracle_status": "pending"}, + "event_outcomes": [{"event_type": "legal_marriage", "event_date": "2014-08-23", "domain": "marriage", "expected_label": "legal_marriage", "outcome": "Married Brad Pitt", "source_excerpt_note": "Public biography records the private wedding on 23 August 2014.", "source": {"url": "https://en.wikipedia.org/wiki/Angelina_Jolie", "source_grade": "verified_secondary"}}], + "similarity": {"score": 0.0, "matching_factors": ["not_user_similarity_benchmark"], "dissimilar_factors": []}, + "replay": {"outcome_replay_status": "replayed", "do_not_use_for_prediction": false, "report_path": "docs/benchmark/public_real_case_benchmark_2026_07_11.json"} + }, + { + "case_id": "kahlo_marriage_1929", + "subject": { + "name": "Frida Kahlo", "year": 1907, "month": 7, "day": 6, "hour": 8, "minute": 30, + "lat": 19.3333, "lon": -99.1667, "tz": -6.6111, "node_mode": "mean", + "birth_source": {"url": "https://www.astro.com/adbvip/adbvip_07_06.htm", "source_grade": "primary", "time_accuracy_rating": "AA", "evidence_basis": "BC/BR in hand"} + }, + "source": {"url": "https://www.astro.com/adbvip/adbvip_07_06.htm", "source_grade": "primary", "license_or_quote_boundary": "facts_and_short_summary_only"}, + "chart_signature": {"benchmark_role": "blind_outcome_replay", "external_oracle_status": "pending", "timezone_note": "Astro-Databank LMT m99w10"}, + "event_outcomes": [{"event_type": "legal_marriage", "event_date": "1929-08-21", "domain": "marriage", "expected_label": "legal_marriage", "outcome": "Married Diego Rivera in a civil ceremony", "source_excerpt_note": "Biography cites the Coyoacan civil ceremony on 21 August 1929.", "source": {"url": "https://en.wikipedia.org/wiki/Frida_Kahlo", "source_grade": "verified_secondary"}}], + "similarity": {"score": 0.0, "matching_factors": ["not_user_similarity_benchmark"], "dissimilar_factors": []}, + "replay": {"outcome_replay_status": "replayed", "do_not_use_for_prediction": false, "report_path": "docs/benchmark/public_real_case_benchmark_2026_07_11.json"} + }, + { + "case_id": "snoop_marriage_1997", + "subject": { + "name": "Snoop Dogg", "year": 1971, "month": 10, "day": 20, "hour": 18, "minute": 20, + "lat": 33.7667, "lon": -118.1833, "tz": -7.0, "node_mode": "mean", + "birth_source": {"url": "https://www.astro.com/adbvip/adbvip_10_20.htm", "source_grade": "primary", "time_accuracy_rating": "AA", "evidence_basis": "BC/BR in hand"} + }, + "source": {"url": "https://www.astro.com/adbvip/adbvip_10_20.htm", "source_grade": "primary", "license_or_quote_boundary": "facts_and_short_summary_only"}, + "chart_signature": {"benchmark_role": "blind_outcome_replay", "external_oracle_status": "pending"}, + "event_outcomes": [{"event_type": "legal_marriage", "event_date": "1997-06-14", "domain": "marriage", "expected_label": "legal_marriage", "outcome": "Married Shante Taylor", "source_excerpt_note": "Public biography records the marriage on 14 June 1997.", "source": {"url": "https://en.wikipedia.org/wiki/Snoop_Dogg", "source_grade": "verified_secondary"}}], + "similarity": {"score": 0.0, "matching_factors": ["not_user_similarity_benchmark"], "dissimilar_factors": []}, + "replay": {"outcome_replay_status": "replayed", "do_not_use_for_prediction": false, "report_path": "docs/benchmark/public_real_case_benchmark_2026_07_11.json"} + }, + { + "case_id": "disney_marriage_1925", + "subject": { + "name": "Walt Disney", "year": 1901, "month": 12, "day": 5, "hour": 0, "minute": 35, + "lat": 41.85, "lon": -87.65, "tz": -6.0, "node_mode": "mean", + "birth_source": {"url": "https://www.astro.com/adbvip/adbvip_12_05.htm", "source_grade": "verified_secondary", "time_accuracy_rating": "A", "evidence_basis": "from memory"} + }, + "source": {"url": "https://www.astro.com/adbvip/adbvip_12_05.htm", "source_grade": "verified_secondary", "license_or_quote_boundary": "facts_and_short_summary_only"}, + "chart_signature": {"benchmark_role": "blind_outcome_replay", "external_oracle_status": "pending"}, + "event_outcomes": [{"event_type": "legal_marriage", "event_date": "1925-07-13", "domain": "marriage", "expected_label": "legal_marriage", "outcome": "Married Lillian Bounds", "source_excerpt_note": "Walt Disney Family Museum dates the marriage to 13 July 1925.", "source": {"url": "https://www.waltdisney.org/blog/who-did-walt-disney-marry", "source_grade": "primary"}}], + "similarity": {"score": 0.0, "matching_factors": ["not_user_similarity_benchmark"], "dissimilar_factors": []}, + "replay": {"outcome_replay_status": "replayed", "do_not_use_for_prediction": false, "report_path": "docs/benchmark/public_real_case_benchmark_2026_07_11.json"} + } + ], + "runtime_boundary": "Positive-event replay only. It measures technical activation recall on known dated events; it does not establish specificity, causal validity, or scientific predictive accuracy. External JHora/PyJHora/VedAstro raw parity remains separately audited." } diff --git a/scripts/muntha.py b/scripts/muntha.py index 1c3b5636..bc95102c 100644 --- a/scripts/muntha.py +++ b/scripts/muntha.py @@ -15,7 +15,7 @@ Muntha 是 Tajika 年运盘(Varshaphala)中的核心指标, 注意:不同流派对 Muntha 计算公式有微小差异。 本实现采用最广泛接受的方法。 """ -from typing import Dict, Optional +from typing import Dict, List, Optional from datetime import datetime, timedelta diff --git a/scripts/public_real_case_benchmark.py b/scripts/public_real_case_benchmark.py new file mode 100644 index 00000000..ad272f44 --- /dev/null +++ b/scripts/public_real_case_benchmark.py @@ -0,0 +1,538 @@ +#!/usr/bin/env python3 +"""Replay research-grade public events through the local Jyotish evidence stack.""" + +from __future__ import annotations + +import argparse +import copy +import json +import subprocess +import sys +from datetime import date +from pathlib import Path +from typing import Any + +from scripts.functional_benefics import derive_functional_benefic_malefic +from scripts.narayana_dasha import narayana_dasha_full_report + + +ROOT = Path(__file__).resolve().parents[1] +ENGINE = ROOT / "scripts" / "jyotish_engine.py" +SIGNS = [ + "Aries", "Taurus", "Gemini", "Cancer", "Leo", "Virgo", + "Libra", "Scorpio", "Sagittarius", "Capricorn", "Aquarius", "Pisces", +] +EVENT_HOUSES = {"career": [10, 6, 9, 11], "marriage": [7, 2, 11, 5]} +EVENT_KARAKAS = {"career": {"Sun", "Saturn", "Mercury"}, "marriage": {"Venus", "Jupiter"}} +PRIMARY_HOUSE = {"career": 10, "marriage": 7} +EXPECTED_LABEL = {"career": "career_status", "marriage": "legal_marriage"} +SIGN_LORDS = { + "Aries": "Mars", "Taurus": "Venus", "Gemini": "Mercury", "Cancer": "Moon", + "Leo": "Sun", "Virgo": "Mercury", "Libra": "Venus", "Scorpio": "Mars", + "Sagittarius": "Jupiter", "Capricorn": "Saturn", "Aquarius": "Saturn", "Pisces": "Jupiter", +} +_ENGINE_JSON_CACHE: dict[str, dict[str, Any]] = {} + + +def clear_engine_cache() -> None: + _ENGINE_JSON_CACHE.clear() + + +def summarize_results(rows: list[dict[str, Any]]) -> dict[str, Any]: + total = len(rows) + blocked = sum(bool(row.get("blocked")) for row in rows) + evaluated = total - blocked + hits = sum(row.get("result_class") in {"strong_hit", "weak_hit"} for row in rows if not row.get("blocked")) + exact = sum(bool(row.get("matched_expected_label")) for row in rows if not row.get("blocked")) + activation_rate = hits / evaluated if evaluated else None + strong_rate = exact / evaluated if evaluated else None + return { + "total_events": total, + "evaluated_events": evaluated, + "strong_hits": sum(row.get("result_class") == "strong_hit" for row in rows), + "weak_hits": sum(row.get("result_class") == "weak_hit" for row in rows), + "misses": sum(row.get("result_class") == "miss" for row in rows), + "blocked_events": blocked, + "known_event_activation_rate": activation_rate, + "strong_activation_rate": strong_rate, + "positive_event_recall": activation_rate, + "positive_event_recall_deprecated": True, + "exact_label_rate": strong_rate, + "exact_label_rate_deprecated": True, + "blocked_rate": blocked / total if total else None, + "balanced_accuracy": None, + "balanced_accuracy_blocked_reason": "no_verified_negative_control_dates", + } + + +def promotion_decision(v1: dict[str, Any], v2: dict[str, Any]) -> dict[str, Any]: + if int(v2.get("blocked_events") or 0) > int(v1.get("blocked_events") or 0): + return {"promote": False, "reason": "v2_increased_blocked_events"} + recall1 = v1.get("positive_event_recall") + recall2 = v2.get("positive_event_recall") + exact1 = v1.get("exact_label_rate") + exact2 = v2.get("exact_label_rate") + if None in {recall1, recall2, exact1, exact2}: + return {"promote": False, "reason": "comparison_metric_missing"} + improved = recall2 >= recall1 and exact2 >= exact1 and (recall2 > recall1 or exact2 > exact1) + return {"promote": improved, "reason": "holdout_metrics_improved" if improved else "no_holdout_improvement"} + + +def compare_reports(v1: dict[str, Any], v2: dict[str, Any]) -> dict[str, Any]: + """Compare frozen rule versions without reinterpreting holdout outcomes.""" + v1_cases = {row["case_id"]: row for row in v1.get("cases") or []} + v2_cases = {row["case_id"]: row for row in v2.get("cases") or []} + deltas = [] + for case_id in sorted(v1_cases.keys() & v2_cases.keys()): + before = v1_cases[case_id] + after = v2_cases[case_id] + before_signals = set(before.get("signals") or []) + deltas.append({ + "case_id": case_id, + "v1_score": before.get("score"), + "v2_score": after.get("score"), + "score_delta": (after.get("score") or 0) - (before.get("score") or 0), + "v1_result_class": before.get("result_class"), + "v2_result_class": after.get("result_class"), + "added_signals": sorted(set(after.get("signals") or []) - before_signals), + }) + return { + "benchmark_id": "public_real_case_holdout_comparison_2026_07_11", + "boundary": "Blind positive-event holdout comparison; no negative controls and no scientific accuracy claim.", + "v1_summary": v1.get("summary") or {}, + "v2_summary": v2.get("summary") or {}, + "promotion": promotion_decision(v1.get("summary") or {}, v2.get("summary") or {}), + "case_deltas": deltas, + } + + +def combine_reports(reports: list[dict[str, Any]], promotion: dict[str, Any]) -> dict[str, Any]: + rows = [row for report in reports for row in report.get("cases") or []] + return { + "benchmark_id": "public_real_case_20_case_closure_2026_07_11", + "rule_version": "v2", + "method": { + "cohorts": ["batch1_discovery_10", "frozen_holdout_10"], + "selection": "Rodden A/AA public figures with independently dated public career or legal-marriage events", + "score_thresholds": {"strong_hit": ">=7", "weak_hit": "4-6", "miss": "<4"}, + }, + "summary": summarize_results(rows), + "domain_summaries": { + domain: summarize_results([row for row in rows if row.get("domain") == domain]) + for domain in ("career", "marriage") + }, + "holdout_promotion": promotion, + "boundary": "Twenty positive public events; no negative controls, specificity estimate, or scientific accuracy claim.", + "technique_audit": [ + {"technique": "D1 + Functional Benefic/Malefic", "status": "used", "scope": "20/20"}, + {"technique": "D9/UL/Darakaraka", "status": "used", "scope": "10 marriage events"}, + {"technique": "D10/A10/Amatyakaraka", "status": "used", "scope": "10 career events"}, + {"technique": "Vimshottari MD/AD", "status": "used", "scope": "20/20"}, + {"technique": "Narayana Dasha", "status": "used", "scope": "20/20"}, + {"technique": "Double Transit PAC", "status": "used", "scope": "20/20"}, + {"technique": "Rahu/Ketu dispositor", "status": "used", "scope": "v2 scoring"}, + {"technique": "Vimshottari PD/PrAD", "status": "partial", "reason": "ratio expansion available but not externally validated or scored"}, + {"technique": "Tajika/Varshaphala/Muntha", "status": "partial", "reason": "local annual layer remains simplified and external oracle closure is incomplete"}, + {"technique": "KP exact cusp/significators", "status": "partial", "reason": "current local KP house layer uses sign-center approximation rather than exact cusps"}, + {"technique": "VedAstro official raw", "status": "blocked", "reason": "official_snapshot_budget_exhausted"}, + {"technique": "PyJHora/JHora/jyotishganit parity", "status": "blocked", "reason": "external canonical raw comparison incomplete"}, + {"technique": "MEVG / Global Web Evidence", "status": "used", "scope": "20 public birth/event source pairs"}, + {"technique": "Real Case Calibration", "status": "used", "scope": "10 discovery + 10 frozen holdout"}, + {"technique": "Negative controls", "status": "blocked", "reason": "no verified non-event dates"}, + ], + "technique_debt": { + "vimshottari_pd_prad": "available_ratio_expansion_not_scored_or_externally_validated", + "tajika_varshaphala_muntha": "available_experimental_not_scored_due_simplified_year_lord_and_oracle_gap", + "kp_cusp_significators": "partial_not_scored_house_centers_are_not_precise_cusps", + "annual_transit_to_arudha_or_ul": "untested_candidate_layer", + "negative_control_dates": "missing_blocks_balanced_accuracy", + }, + "cases": rows, + } + + +def node_dispositor_bonus( + active_lords: set[str], + domain: str, + chart: dict[str, Any], + roles: dict[str, Any], +) -> tuple[int, list[str]]: + event_houses = set(EVENT_HOUSES[domain]) + score = 0 + signals: list[str] = [] + planets = chart.get("planets") or {} + for node in sorted(active_lords & {"Rahu", "Ketu"}): + node_sign = (planets.get(node) or {}).get("sign") + dispositor = SIGN_LORDS.get(node_sign) + if not dispositor: + continue + if set((roles.get("owned_houses") or {}).get(dispositor) or []) & event_houses: + score += 1 + signals.append(f"{node}_dispositor_{dispositor}_owns_event_house") + occupied = (planets.get(dispositor) or {}).get("house") + if occupied in event_houses: + score += 1 + signals.append(f"{node}_dispositor_{dispositor}_occupies_event_house:{occupied}") + return score, signals + + +def _house_from_sign(ascendant: str, target: str) -> int | None: + if ascendant not in SIGNS or target not in SIGNS: + return None + return (SIGNS.index(target) - SIGNS.index(ascendant)) % 12 + 1 + + +def varga_and_karaka_bonus( + active_lords: set[str], + domain: str, + varga: dict[str, Any], + jaimini: dict[str, Any], +) -> tuple[int, list[str]]: + chart_key = "D10_Dasamsa" if domain == "career" else "D9_Navamsa" + chart = ((varga.get("divisional_charts") or {}).get(chart_key) or {}) + ascendant = chart.get("ascendant") + primary_house = PRIMARY_HOUSE[domain] + primary_sign = SIGNS[(SIGNS.index(ascendant) + primary_house - 1) % 12] if ascendant in SIGNS else None + primary_lord = SIGN_LORDS.get(primary_sign) + lagna_lord = SIGN_LORDS.get(ascendant) + score = 0 + signals: list[str] = [] + label = "D10" if domain == "career" else "D9" + for lord in sorted(active_lords): + if lord == lagna_lord: + score += 1 + signals.append(f"active_dasha_matches_{label}_Lagna_lord:{lord}") + if lord == primary_lord: + score += 1 + signals.append(f"active_dasha_matches_{label}_{primary_house}L:{lord}") + lord_sign = (chart.get(lord) or {}).get("sign") + if _house_from_sign(ascendant, lord_sign) == primary_house: + score += 1 + signals.append(f"active_dasha_occupies_{label}_house_{primary_house}:{lord}") + karaka_name = "Amatyakaraka" if domain == "career" else "Darakaraka" + karaka_planet = ((((jaimini.get("chara_karaka_7") or {}).get("karaka_table") or {}).get(karaka_name) or {}).get("planet")) + if karaka_planet in active_lords: + score += 1 + signals.append(f"active_dasha_matches_{karaka_name}:{karaka_planet}") + return score, signals + + +def _engine_json(command: str, subject: dict[str, Any], *extra: str, timeout: int = 30) -> dict[str, Any]: + cache_key = json.dumps( + {"command": command, "subject": subject, "extra": extra}, + sort_keys=True, + ensure_ascii=True, + default=str, + ) + if cache_key in _ENGINE_JSON_CACHE: + return copy.deepcopy(_ENGINE_JSON_CACHE[cache_key]) + args = [ + sys.executable, str(ENGINE), command, + "--year", str(subject["year"]), "--month", str(subject["month"]), + "--day", str(subject["day"]), "--hour", str(subject["hour"]), + "--minute", str(subject["minute"]), "--lat", str(subject["lat"]), + "--lon", str(subject["lon"]), "--tz", str(subject["tz"]), + "--node-mode", str(subject.get("node_mode", "mean")), + *extra, + ] + completed = subprocess.run(args, cwd=ROOT, check=True, capture_output=True, text=True, timeout=timeout) + payload = json.loads(completed.stdout) + _ENGINE_JSON_CACHE[cache_key] = payload + return copy.deepcopy(payload) + + +def _find_dasha(dasha: dict[str, Any], event_date: str) -> tuple[str | None, str | None]: + target = date.fromisoformat(event_date) + for md in dasha.get("timeline") or []: + if date.fromisoformat(md["start"][:10]) <= target < date.fromisoformat(md["end"][:10]): + for ad in md.get("antardasha_timeline") or []: + if date.fromisoformat(ad["start"][:10]) <= target < date.fromisoformat(ad["end"][:10]): + return md.get("lord"), ad.get("lord") + return md.get("lord"), None + return None, None + + +def _planet_score(planet: str | None, event_houses: set[int], chart: dict[str, Any], roles: dict[str, Any], karakas: set[str]) -> tuple[int, list[str]]: + if not planet: + return 0, [] + score = 0 + signals: list[str] = [] + owned = set((roles.get("owned_houses") or {}).get(planet) or []) + occupied = (chart.get("planets") or {}).get(planet, {}).get("house") + owned_hits = sorted(owned & event_houses) + if owned_hits: + score += 2 + signals.append(f"{planet}_owns_event_houses:{owned_hits}") + if occupied in event_houses: + score += 1 + signals.append(f"{planet}_occupies_event_house:{occupied}") + if planet in karakas: + score += 1 + signals.append(f"{planet}_domain_karaka") + return score, signals + + +def score_active_dasha_lords( + lords: list[str | None], + event_houses: set[int], + chart: dict[str, Any], + roles: dict[str, Any], + karakas: set[str], +) -> tuple[int, list[str]]: + score = 0 + signals: list[str] = [] + for lord in dict.fromkeys(lord for lord in lords if lord): + points, lord_signals = _planet_score(lord, event_houses, chart, roles, karakas) + score += points + signals.extend(lord_signals) + return score, signals + + +def _transit_json(event_date: str, subject: dict[str, Any]) -> dict[str, Any]: + target = date.fromisoformat(event_date) + command = [ + sys.executable, str(ENGINE), "transit", + "--year", str(target.year), "--month", str(target.month), "--day", str(target.day), + "--planet", "Jupiter,Saturn", "--tz", str(subject["tz"]), + "--node-mode", str(subject.get("node_mode", "mean")), + ] + completed = subprocess.run(command, cwd=ROOT, check=True, capture_output=True, text=True, timeout=30) + return json.loads(completed.stdout) + + +def ashtakavarga_audit(domain: str, packet: dict[str, Any], transit: dict[str, Any]) -> dict[str, Any]: + event_houses = EVENT_HOUSES[domain] + sav = packet.get("sav") or {} + bav = packet.get("bav") or {} + event_house_sav = { + str(house): (packet.get("house_scores") or {}).get(f"house_{house}") + for house in event_houses + } + transit_support = {} + for planet in ("Jupiter", "Saturn"): + sign = ((transit.get("planets") or {}).get(planet) or {}).get("sign") + sign_index = SIGNS.index(sign) if sign in SIGNS else None + bindus = ((bav.get(planet) or {}).get("bindus") or []) + transit_support[planet] = { + "sign": sign, + "sav": (sav.get("scores") or {}).get(sign), + "bav": bindus[sign_index] if sign_index is not None and sign_index < len(bindus) else None, + } + return { + "status": "used_non_scoring", + "scoring_effect": 0, + "method": packet.get("method"), + "version": packet.get("version"), + "sav_total": sav.get("total"), + "sav_valid": sav.get("valid"), + "all_bav_valid": packet.get("all_bav_valid"), + "event_house_sav": event_house_sav, + "transit_support": transit_support, + "settings": { + "ayanamsa": transit.get("ayanamsa"), + "node_mode": transit.get("node_mode"), + }, + "boundary": "Audit evidence only. SAV/BAV does not change V2.1 event scores until a fresh holdout validates it.", + } + + +def _narayana_at_event(subject: dict[str, Any], chart: dict[str, Any], event_date: str) -> dict[str, Any]: + asc_sign = chart["ascendant"]["sign"] + asc_idx = SIGNS.index(asc_sign) + planet_lons = {name: data["degree"] for name, data in chart["planets"].items() if "degree" in data} + born = date(subject["year"], subject["month"], subject["day"]) + target = date.fromisoformat(event_date) + age = (target - born).days / 365.2425 + report = narayana_dasha_full_report(asc_idx, planet_lons, current_age=age, birth_year=subject["year"]) + return report.get("current_dasha") or {} + + +def _arudha_lord(jaimini: dict[str, Any], domain: str) -> str | None: + arudha = jaimini.get("arudha_padas") or {} + if domain == "career": + return ((arudha.get("padas") or {}).get("A10") or {}).get("lord") + return (arudha.get("upapada") or {}).get("lord") + + +def _double_transit_score(packet: dict[str, Any]) -> tuple[int, list[str]]: + strengths = [row.get("strength") for row in packet.get("double_transit") or []] + if "strong" in strengths: + return 2, ["double_transit_pac_strong"] + if strengths: + return 1, ["double_transit_pac_present"] + return 0, [] + + +def replay_case(case: dict[str, Any], rule_version: str = "v1") -> dict[str, Any]: + subject = case["subject"] + event = case["event_outcomes"][0] + domain = event["domain"] + event_houses = set(EVENT_HOUSES[domain]) + try: + chart = _engine_json("chart", subject) + dasha = _engine_json("dasha", subject, "--years", "100") + varga = _engine_json("varga", subject, "--d10" if domain == "career" else "--d9") + jaimini = _engine_json("jaimini", subject) + pac = _engine_json( + "double-transit-pac", subject, + "--date", event["event_date"], "--house", str(PRIMARY_HOUSE[domain]), + ) + except (subprocess.SubprocessError, json.JSONDecodeError, KeyError, ValueError) as exc: + return { + "case_id": case["case_id"], "name": subject["name"], "domain": domain, + "event_date": event["event_date"], "blocked": True, "result_class": "blocked", + "matched_expected_label": False, "blocked_reason": f"{type(exc).__name__}: {exc}", + } + + roles = derive_functional_benefic_malefic(chart["ascendant"]["sign"]) + md, ad = _find_dasha(dasha, event["event_date"]) + score = 0 + signals: list[str] = [] + if rule_version == "v2_1": + score, signals = score_active_dasha_lords([md, ad], event_houses, chart, roles, EVENT_KARAKAS[domain]) + else: + for lord in (md, ad): + points, lord_signals = _planet_score(lord, event_houses, chart, roles, EVENT_KARAKAS[domain]) + score += points + signals.extend(lord_signals) + + active_lords = {lord for lord in (md, ad) if lord} + if rule_version in {"v2", "v2_1"}: + node_points, node_signals = node_dispositor_bonus(active_lords, domain, chart, roles) + varga_points, varga_signals = varga_and_karaka_bonus(active_lords, domain, varga, jaimini) + score += node_points + varga_points + signals.extend(node_signals) + signals.extend(varga_signals) + + arudha_lord = _arudha_lord(jaimini, domain) + if arudha_lord in {md, ad}: + score += 1 + signals.append(f"active_dasha_matches_{'A10' if domain == 'career' else 'UL'}_lord:{arudha_lord}") + + narayana = _narayana_at_event(subject, chart, event["event_date"]) + narayana_md = narayana.get("md") or {} + event_sign = SIGNS[(SIGNS.index(chart["ascendant"]["sign"]) + PRIMARY_HOUSE[domain] - 1) % 12] + if narayana_md.get("sign") == event_sign: + score += 2 + signals.append(f"narayana_activates_primary_event_sign:{event_sign}") + narayana_lord = narayana_md.get("lord") + if set((roles.get("owned_houses") or {}).get(narayana_lord) or []) & event_houses: + score += 1 + signals.append(f"narayana_lord_owns_event_house:{narayana_lord}") + + pac_points, pac_signals = _double_transit_score(pac) + score += pac_points + signals.extend(pac_signals) + + ashtakavarga = {"status": "not_run", "scoring_effect": 0} + if rule_version == "v2_1": + try: + ashtakavarga_packet = _engine_json("ashtakavarga", subject) + transit_packet = _transit_json(event["event_date"], subject) + ashtakavarga = ashtakavarga_audit(domain, ashtakavarga_packet, transit_packet) + except (subprocess.SubprocessError, json.JSONDecodeError, KeyError, ValueError) as exc: + ashtakavarga = { + "status": "blocked", + "scoring_effect": 0, + "blocked_reason": f"{type(exc).__name__}: {exc}", + } + + if score >= 7: + result_class = "strong_hit" + actual_label = EXPECTED_LABEL[domain] + elif score >= 4: + result_class = "weak_hit" + actual_label = "domain_activation" + else: + result_class = "miss" + actual_label = None + + return { + "case_id": case["case_id"], + "name": subject["name"], + "domain": domain, + "event_date": event["event_date"], + "outcome": event["outcome"], + "birth_time_rating": subject["birth_source"]["time_accuracy_rating"], + "rule_version": rule_version, + "blocked": False, + "result_class": result_class, + "score": score, + "expected_label": EXPECTED_LABEL[domain], + "actual_label": actual_label, + "matched_expected_label": actual_label == EXPECTED_LABEL[domain], + "signals": signals, + "evidence": { + "ascendant": chart["ascendant"], + "vimshottari": {"mahadasha": md, "antardasha": ad}, + "narayana": narayana, + "functional_benefic_malefic": roles, + "domain_varga": varga, + "arudha_lord": arudha_lord, + "double_transit_pac": pac, + "ashtakavarga_audit": ashtakavarga, + "birth_source": subject["birth_source"], + "event_source": event["source"], + }, + } + + +def build_report( + manifest: dict[str, Any], + strict_probe_blocked_reason: str | None = None, + rule_version: str = "v1", +) -> dict[str, Any]: + rows = [replay_case(case, rule_version=rule_version) for case in manifest.get("cases") or []] + return { + "benchmark_id": "public_real_case_benchmark_2026_07_11", + "rule_version": rule_version, + "method": { + "selection": "Rodden A/AA public figures with independently dated public events", + "pre_registered_layers": ["D1", "D9_or_D10", "UL_or_A10", "Functional Benefic/Malefic", "Vimshottari MD/AD", "Narayana Dasha", "Double Transit PAC"] + (["Rahu/Ketu dispositor", "D9/D10 Lagna and primary-house lord", "Amatyakaraka/Darakaraka"] if rule_version in {"v2", "v2_1"} else []) + (["SAV/BAV non-scoring audit", "deduplicated MD/AD lord scoring"] if rule_version == "v2_1" else []), + "score_thresholds": {"strong_hit": ">=7", "weak_hit": "4-6", "miss": "<4"}, + "boundary": "Positive-event technical activation replay; not scientific predictive accuracy.", + }, + "summary": summarize_results(rows), + "strict_workflow_batch": { + "status": "blocked" if strict_probe_blocked_reason else "not_run", + "blocked_reason": strict_probe_blocked_reason, + }, + "external_oracle_boundary": { + "VedAstro": "diagnostic_only_unless_official_raw_present", + "PyJHora": "blocked_or_benchmark_only_until_dependency_available", + "JHora": "manual_oracle_not_automated", + "jyotishganit": "parity_contract_separate_from_this_event_replay", + }, + "cases": rows, + } + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--manifest", default="references/real_case_calibration/replay_manifest.json") + parser.add_argument("--output") + parser.add_argument("--strict-probe-blocked-reason") + parser.add_argument("--rule-version", choices=["v1", "v2", "v2_1", "compare"], default="v1") + parser.add_argument("--comparison-v1") + parser.add_argument("--comparison-v2") + args = parser.parse_args() + manifest = json.loads((ROOT / args.manifest).read_text(encoding="utf-8")) + if args.rule_version == "compare": + if not args.comparison_v1 or not args.comparison_v2: + parser.error("compare requires --comparison-v1 and --comparison-v2 to avoid duplicate engine replay") + v1 = json.loads((ROOT / args.comparison_v1).read_text(encoding="utf-8")) + v2 = json.loads((ROOT / args.comparison_v2).read_text(encoding="utf-8")) + report = compare_reports(v1, v2) + else: + report = build_report(manifest, args.strict_probe_blocked_reason, rule_version=args.rule_version) + payload = json.dumps(report, ensure_ascii=False, indent=2, sort_keys=True) + "\n" + if args.output: + output_path = ROOT / args.output + output_path.parent.mkdir(parents=True, exist_ok=True) + output_path.write_text(payload, encoding="utf-8") + print(payload, end="") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/public_real_case_negative_controls.py b/scripts/public_real_case_negative_controls.py new file mode 100644 index 00000000..dd1e0b00 --- /dev/null +++ b/scripts/public_real_case_negative_controls.py @@ -0,0 +1,122 @@ +#!/usr/bin/env python3 +"""Rank known public event dates against deterministic non-target control dates.""" + +from __future__ import annotations + +import argparse +import copy +import json +import statistics +from datetime import date, timedelta +from pathlib import Path +from typing import Any, Iterable + +from scripts.public_real_case_benchmark import clear_engine_cache, replay_case + +ROOT = Path(__file__).resolve().parents[1] +DEFAULT_OFFSETS = (-120, -90, -60, -30, 30, 60, 90, 120) + + +def generate_control_dates(event_date: str, offsets: Iterable[int] = DEFAULT_OFFSETS) -> list[str]: + target = date.fromisoformat(event_date) + return [(target + timedelta(days=int(offset))).isoformat() for offset in offsets if int(offset) != 0] + + +def rank_positive_against_controls(positive_score: int, control_scores: list[int]) -> dict[str, Any]: + rank = 1 + sum(score >= positive_score for score in control_scores) + max_control = max(control_scores) if control_scores else None + return { + "positive_score": positive_score, + "positive_rank": rank, + "candidate_count": len(control_scores) + 1, + "reciprocal_rank": 1 / rank, + "top_1": rank == 1, + "top_3": rank <= 3, + "max_control_score": max_control, + "score_margin": positive_score - max_control if max_control is not None else None, + } + + +def summarize_negative_control_rows(rows: list[dict[str, Any]]) -> dict[str, Any]: + controls = [control for row in rows for control in row.get("controls") or [] if not control.get("blocked")] + rankings = [row["ranking"] for row in rows if row.get("ranking")] + margins = [item["score_margin"] for item in rankings if item["score_margin"] is not None] + return { + "case_count": len(rows), + "ranked_case_count": len(rankings), + "control_date_count": len(controls), + "control_activation_rate": sum((control.get("score") or 0) >= 4 for control in controls) / len(controls) if controls else None, + "control_strong_activation_rate": sum((control.get("score") or 0) >= 7 for control in controls) / len(controls) if controls else None, + "positive_top_1_rate": sum(item["top_1"] for item in rankings) / len(rankings) if rankings else None, + "positive_top_3_rate": sum(item["top_3"] for item in rankings) / len(rankings) if rankings else None, + "mean_reciprocal_rank": statistics.mean(item["reciprocal_rank"] for item in rankings) if rankings else None, + "mean_score_margin": statistics.mean(margins) if margins else None, + "balanced_accuracy": None, + "balanced_accuracy_blocked_reason": "controls_are_non_target_dates_not_independently_adjudicated_all-domain_non_events", + } + + +def build_report(manifest: dict[str, Any], offsets: Iterable[int] = DEFAULT_OFFSETS) -> dict[str, Any]: + clear_engine_cache() + rows = [] + for case in manifest.get("cases") or []: + event = case["event_outcomes"][0] + positive = replay_case(case, rule_version="v2_1") + controls = [] + for control_date in generate_control_dates(event["event_date"], offsets): + control_case = copy.deepcopy(case) + control_event = control_case["event_outcomes"][0] + control_event["event_date"] = control_date + control_event["outcome"] = f"non_target_control_date_for:{event['outcome']}" + result = replay_case(control_case, rule_version="v2_1") + controls.append({ + "date": control_date, + "score": result.get("score"), + "result_class": result.get("result_class"), + "blocked": bool(result.get("blocked")), + "blocked_reason": result.get("blocked_reason"), + }) + control_scores = [int(item["score"]) for item in controls if not item["blocked"] and item.get("score") is not None] + ranking = None + if not positive.get("blocked") and positive.get("score") is not None: + ranking = rank_positive_against_controls(int(positive["score"]), control_scores) + rows.append({ + "case_id": case["case_id"], + "name": case["subject"]["name"], + "domain": event["domain"], + "positive_date": event["event_date"], + "positive": positive, + "controls": controls, + "ranking": ranking, + }) + return { + "benchmark_id": "public_real_case_negative_control_pilot_2026_07_11", + "rule_version": "v2_1", + "control_offsets_days": list(offsets), + "summary": summarize_negative_control_rows(rows), + "boundary": ( + "Controls are dates without the exact recorded target outcome. They may contain other life events. " + "This pilot measures date ranking and false domain activation, not scientific causal validity." + ), + "cases": rows, + } + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--manifest", default="references/real_case_calibration/replay_manifest_probe3_v2.json") + parser.add_argument("--output") + args = parser.parse_args() + manifest = json.loads((ROOT / args.manifest).read_text(encoding="utf-8")) + report = build_report(manifest) + payload = json.dumps(report, ensure_ascii=False, indent=2, sort_keys=True) + "\n" + if args.output: + output_path = ROOT / args.output + output_path.parent.mkdir(parents=True, exist_ok=True) + output_path.write_text(payload, encoding="utf-8") + print(payload, end="") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/real_case_replay_validator.py b/scripts/real_case_replay_validator.py index b48fb509..8ec41ad2 100644 --- a/scripts/real_case_replay_validator.py +++ b/scripts/real_case_replay_validator.py @@ -11,6 +11,7 @@ from typing import Any CASE_REQUIRED_FIELDS = { "case_id", + "subject", "source", "chart_signature", "event_outcomes", @@ -18,7 +19,18 @@ CASE_REQUIRED_FIELDS = { "replay", } SOURCE_REQUIRED_FIELDS = {"url", "source_grade", "license_or_quote_boundary"} -EVENT_REQUIRED_FIELDS = {"event_type", "event_date", "outcome"} +SUBJECT_REQUIRED_FIELDS = { + "name", "year", "month", "day", "hour", "minute", "lat", "lon", "tz", + "node_mode", "birth_source", +} +BIRTH_SOURCE_REQUIRED_FIELDS = { + "url", "source_grade", "time_accuracy_rating", "evidence_basis", +} +EVENT_REQUIRED_FIELDS = { + "event_type", "event_date", "domain", "expected_label", "outcome", "source", +} +EVENT_SOURCE_REQUIRED_FIELDS = {"url", "source_grade"} +ALLOWED_BIRTH_TIME_RATINGS = {"A", "AA"} def _missing(mapping: dict[str, Any], required: set[str]) -> list[str]: @@ -40,6 +52,26 @@ def _case_errors(case: Any, index: int) -> list[dict[str, Any]]: elif "source" in case: errors.append({"case_id": case.get("case_id"), "field": "source", "error": "not_object"}) + subject = case.get("subject") + if isinstance(subject, dict): + for field in _missing(subject, SUBJECT_REQUIRED_FIELDS): + errors.append({"case_id": case.get("case_id"), "field": f"subject.{field}", "error": "missing"}) + birth_source = subject.get("birth_source") + if isinstance(birth_source, dict): + for field in _missing(birth_source, BIRTH_SOURCE_REQUIRED_FIELDS): + errors.append({"case_id": case.get("case_id"), "field": f"subject.birth_source.{field}", "error": "missing"}) + rating = birth_source.get("time_accuracy_rating") + if rating not in ALLOWED_BIRTH_TIME_RATINGS: + errors.append({ + "case_id": case.get("case_id"), + "field": "subject.birth_source.time_accuracy_rating", + "error": "birth_time_rating_below_A", + }) + elif "birth_source" in subject: + errors.append({"case_id": case.get("case_id"), "field": "subject.birth_source", "error": "not_object"}) + elif "subject" in case: + errors.append({"case_id": case.get("case_id"), "field": "subject", "error": "not_object"}) + events = case.get("event_outcomes") if isinstance(events, list): if not events: @@ -50,6 +82,12 @@ def _case_errors(case: Any, index: int) -> list[dict[str, Any]]: continue for field in _missing(event, EVENT_REQUIRED_FIELDS): errors.append({"case_id": case.get("case_id"), "field": f"event_outcomes[{event_index}].{field}", "error": "missing"}) + event_source = event.get("source") + if isinstance(event_source, dict): + for field in _missing(event_source, EVENT_SOURCE_REQUIRED_FIELDS): + errors.append({"case_id": case.get("case_id"), "field": f"event_outcomes[{event_index}].source.{field}", "error": "missing"}) + elif "source" in event: + errors.append({"case_id": case.get("case_id"), "field": f"event_outcomes[{event_index}].source", "error": "not_object"}) elif "event_outcomes" in case: errors.append({"case_id": case.get("case_id"), "field": "event_outcomes", "error": "not_array"}) @@ -72,12 +110,24 @@ def validate_manifest(path: str | Path) -> dict[str, Any]: errors: list[dict[str, Any]] = [] replay_ready_count = 0 + domain_counts: dict[str, int] = {} + birth_time_ratings: dict[str, int] = {} for index, case in enumerate(cases): case_errors = _case_errors(case, index) errors.extend(case_errors) replay = case.get("replay") if isinstance(case, dict) else {} if not case_errors and isinstance(replay, dict) and replay.get("outcome_replay_status") == "replayed": replay_ready_count += 1 + if isinstance(case, dict): + subject = case.get("subject") or {} + birth_source = subject.get("birth_source") if isinstance(subject, dict) else {} + rating = birth_source.get("time_accuracy_rating") if isinstance(birth_source, dict) else None + if isinstance(rating, str): + birth_time_ratings[rating] = birth_time_ratings.get(rating, 0) + 1 + for event in case.get("event_outcomes") or []: + if isinstance(event, dict) and isinstance(event.get("domain"), str): + domain = event["domain"] + domain_counts[domain] = domain_counts.get(domain, 0) + 1 if errors: status = "invalid" @@ -99,6 +149,8 @@ def validate_manifest(path: str | Path) -> dict[str, Any]: "case_schema": manifest.get("case_schema"), "case_count": len(cases), "replay_ready_count": replay_ready_count, + "domain_counts": dict(sorted(domain_counts.items())), + "birth_time_ratings": dict(sorted(birth_time_ratings.items())), "blocked_reason": blocked_reason, "errors": errors, "runtime_boundary": manifest.get("runtime_boundary", ""), diff --git a/scripts/unified_consultation_orchestrator.py b/scripts/unified_consultation_orchestrator.py index c3d85c0b..7bbeed85 100644 --- a/scripts/unified_consultation_orchestrator.py +++ b/scripts/unified_consultation_orchestrator.py @@ -4,6 +4,7 @@ from __future__ import annotations +import json from dataclasses import dataclass from pathlib import Path from typing import Any @@ -479,6 +480,109 @@ class UnifiedConsultationOrchestrator: } replay_manifest_path = Path(__file__).resolve().parents[1] / "references/real_case_calibration/replay_manifest.json" replay_manifest = validate_real_case_replay_manifest(replay_manifest_path) + holdout_manifest_path = Path(__file__).resolve().parents[1] / "references/real_case_calibration/replay_manifest_holdout_v2.json" + holdout_manifest = validate_real_case_replay_manifest(holdout_manifest_path) + benchmark_path = Path(__file__).resolve().parents[1] / "docs/benchmark/public_real_case_20_case_closure_2026_07_11.json" + if benchmark_path.exists(): + benchmark_payload = json.loads(benchmark_path.read_text(encoding="utf-8")) + public_outcome_benchmark = { + "status": "used", + "path": "docs/benchmark/public_real_case_20_case_closure_2026_07_11.json", + "summary": benchmark_payload.get("summary") or {}, + "method": benchmark_payload.get("method") or {}, + "strict_workflow_batch": benchmark_payload.get("strict_workflow_batch") or {}, + "holdout_promotion": benchmark_payload.get("holdout_promotion") or {}, + "technique_debt": benchmark_payload.get("technique_debt") or {}, + } + else: + public_outcome_benchmark = { + "status": "blocked", + "path": "docs/benchmark/public_real_case_20_case_closure_2026_07_11.json", + "blocked_reason": "public_outcome_benchmark_missing", + } + supplemental_path = Path(__file__).resolve().parents[1] / "docs/benchmark/public_real_case_probe3_v2_2026_07_11.json" + combined_observation_path = Path(__file__).resolve().parents[1] / "docs/benchmark/public_real_case_23_case_observation_2026_07_11.json" + if supplemental_path.exists() and combined_observation_path.exists(): + supplemental_payload = json.loads(supplemental_path.read_text(encoding="utf-8")) + combined_payload = json.loads(combined_observation_path.read_text(encoding="utf-8")) + supplemental_public_probe = { + "status": "used", + "path": "docs/benchmark/public_real_case_probe3_v2_2026_07_11.json", + "summary": supplemental_payload.get("summary") or {}, + "combined_observation": combined_payload.get("summary") or {}, + "boundary": "Three-case independent probe is contradictory generalization evidence, not a promotion or accuracy estimate.", + } + else: + supplemental_public_probe = { + "status": "blocked", + "blocked_reason": "supplemental_public_probe_missing", + } + corrected_v21_path = Path(__file__).resolve().parents[1] / "docs/benchmark/public_real_case_23_case_v21_corrected_observation_2026_07_11.json" + if corrected_v21_path.exists(): + corrected_payload = json.loads(corrected_v21_path.read_text(encoding="utf-8")) + corrected_v21_observation = { + "status": "used", + "path": "docs/benchmark/public_real_case_23_case_v21_corrected_observation_2026_07_11.json", + "summary": corrected_payload.get("summary") or {}, + "domain_summaries": corrected_payload.get("domain_summaries") or {}, + "ashtakavarga_audit_status": corrected_payload.get("ashtakavarga_audit_status"), + "ashtakavarga_descriptive": corrected_payload.get("ashtakavarga_descriptive") or {}, + "boundary": corrected_payload.get("boundary"), + } + else: + corrected_v21_observation = { + "status": "blocked", + "blocked_reason": "corrected_v21_observation_missing", + } + negative_control_path = Path(__file__).resolve().parents[1] / "docs/benchmark/public_real_case_negative_control_pilot_2026_07_11.json" + if negative_control_path.exists(): + negative_payload = json.loads(negative_control_path.read_text(encoding="utf-8")) + negative_summary = negative_payload.get("summary") or {} + negative_control_pilot = { + "status": "used", + "path": "docs/benchmark/public_real_case_negative_control_pilot_2026_07_11.json", + "summary": negative_summary, + "boundary": negative_payload.get("boundary"), + } + else: + negative_control_pilot = { + "status": "blocked", + "blocked_reason": "negative_control_pilot_missing", + } + negative_summary = {} + annual_control_path = Path(__file__).resolve().parents[1] / "docs/benchmark/public_real_case_annual_control_pilot_2026_07_11.json" + if annual_control_path.exists(): + annual_payload = json.loads(annual_control_path.read_text(encoding="utf-8")) + annual_control_pilot = { + "status": "used", + "path": "docs/benchmark/public_real_case_annual_control_pilot_2026_07_11.json", + "summary": annual_payload.get("summary") or {}, + "boundary": annual_payload.get("boundary"), + } + else: + annual_control_pilot = { + "status": "blocked", + "blocked_reason": "annual_control_pilot_missing", + } + if negative_control_pilot.get("status") == "used" and annual_control_pilot.get("status") == "used": + timing_precision_gate = { + "status": "blocked", + "maximum_supported_precision": "unvalidated_broad_window", + "blocked_claims": ["exact_day", "exact_month_from_current_replay_score"], + "domain_support": {"career": "blocked", "marriage": "partial_candidate"}, + "reason": "near_and_annual_control_rankings_below_gate", + "observed_positive_top_1_rate": negative_summary.get("positive_top_1_rate"), + "observed_positive_top_3_rate": negative_summary.get("positive_top_3_rate"), + "annual_positive_top_1_rate": (annual_control_pilot.get("summary") or {}).get("positive_top_1_rate"), + } + else: + timing_precision_gate = { + "status": "blocked", + "maximum_supported_precision": "unvalidated_broad_window", + "blocked_claims": ["exact_day", "exact_month_from_current_replay_score"], + "domain_support": {"career": "blocked", "marriage": "partial_candidate"}, + "reason": "control_pilot_missing", + } candidate_refs = case_index_by_domain.get(route, []) packet = machine_evidence_packet if isinstance(machine_evidence_packet, dict) else {} sections = packet.get("sections") if isinstance(packet.get("sections"), dict) else {} @@ -532,16 +636,24 @@ class UnifiedConsultationOrchestrator: "status": "partial_scored" if scored_candidates else "catalog_available_matching_not_run", "batch_id": "real_case_studies_batch1", "route": route, - "source_roots": ["references/real_case_studies", "docs/benchmark"], + "source_roots": ["references/real_case_studies", "references/real_case_calibration", "docs/benchmark"], "case_index_by_domain": case_index_by_domain, "required_replay_schema": "references/real_case_calibration/catalog.schema.json", "outcome_replay_manifest": replay_manifest, + "holdout_replay_manifest": holdout_manifest, + "public_outcome_benchmark": public_outcome_benchmark, + "supplemental_public_probe": supplemental_public_probe, + "corrected_v21_observation": corrected_v21_observation, + "negative_control_pilot": negative_control_pilot, + "annual_control_pilot": annual_control_pilot, + "timing_precision_gate": timing_precision_gate, "candidate_refs": list(candidate_refs), "scored_candidates": scored_candidates, "reference_grade": scored_candidates[0]["reference_grade"] if scored_candidates else "ungraded_until_similarity_scored", "boundary": ( - "Local case catalog has route, evidence-section, and timing-evidence scoring only; concrete event " - "outcome matching must run before a case can be used as complete calibration evidence." + "The public benchmark replays twenty dated outcomes, including a frozen ten-case holdout, but it contains positive events only. It can " + "measure activation recall, not specificity or scientific predictive accuracy; user-chart " + "similarity still requires separate structured matching." ), } @@ -586,6 +698,8 @@ class UnifiedConsultationOrchestrator: blocked_items.append("vedastro_official_raw_archive_manifest_missing") case_packet = real_case_calibration if isinstance(real_case_calibration, dict) else {} case_status = case_packet.get("status") or "required_not_satisfied" + timing_precision = case_packet.get("timing_precision_gate") if isinstance(case_packet.get("timing_precision_gate"), dict) else {} + timing_precision_status = timing_precision.get("status") or "blocked" functional_packet = packet.get("functional_benefic_malefic") if isinstance(packet.get("functional_benefic_malefic"), dict) else {} functional_status = functional_packet.get("status") or "blocked" if functional_status != "used": @@ -594,6 +708,8 @@ class UnifiedConsultationOrchestrator: blocked_items.append("real_case_calibration_not_yet_materialized") elif case_status != "complete": blocked_items.append("real_case_calibration_partial") + if timing_precision_status != "pass": + blocked_items.append("timing_precision_gate_blocked") cross_system_arbitration = build_cross_system_arbitration( route_packet=route_packet, jyotish_evidence=packet, @@ -657,6 +773,15 @@ class UnifiedConsultationOrchestrator: "used": bool(case_packet), "effect_on_confidence": "partial_reference_only_until_outcome_replay" if case_status != "complete" else "supports_calibration", }, + { + "technique": "Timing Precision Gate", + "status": timing_precision_status, + "used": bool(timing_precision), + "maximum_supported_precision": timing_precision.get("maximum_supported_precision", "unvalidated_broad_window"), + "blocked_claims": timing_precision.get("blocked_claims", ["exact_day", "exact_month_from_current_replay_score"]), + "domain_support": timing_precision.get("domain_support", {}), + "effect_on_confidence": "blocks_false_precision_until_control_date_rankings_pass", + }, { "technique": "Functional Benefic/Malefic", "status": functional_status, @@ -727,6 +852,7 @@ class UnifiedConsultationOrchestrator: "Blind Technical Mode", "MEVG / Global Web Evidence", "Real Case Calibration", + "Timing Precision Gate", "Functional Benefic/Malefic", ], "status": "blocked" if blocked_items else "pass", diff --git a/tests/test_muntha_module.py b/tests/test_muntha_module.py new file mode 100644 index 00000000..8f4be13d --- /dev/null +++ b/tests/test_muntha_module.py @@ -0,0 +1,10 @@ +from __future__ import annotations + +from scripts.muntha import calc_muntha_from_sun_sign + + +def test_standalone_muntha_module_imports_and_calculates() -> None: + result = calc_muntha_from_sun_sign(0, 12) + + assert result["muntha_sign"] == 11 + assert result["muntha_lord"] == "Jupiter" diff --git a/tests/test_public_real_case_benchmark.py b/tests/test_public_real_case_benchmark.py new file mode 100644 index 00000000..644bf5c9 --- /dev/null +++ b/tests/test_public_real_case_benchmark.py @@ -0,0 +1,204 @@ +from __future__ import annotations + +import json +from pathlib import Path + +from scripts.public_real_case_benchmark import ( + _engine_json, + ashtakavarga_audit, + clear_engine_cache, + combine_reports, + compare_reports, + node_dispositor_bonus, + promotion_decision, + score_active_dasha_lords, + summarize_results, + varga_and_karaka_bonus, +) + + +ROOT = Path(__file__).resolve().parents[1] + + +def test_summary_reports_positive_recall_without_inventing_specificity() -> None: + summary = summarize_results( + [ + {"result_class": "strong_hit", "matched_expected_label": True, "blocked": False}, + {"result_class": "weak_hit", "matched_expected_label": False, "blocked": False}, + {"result_class": "miss", "matched_expected_label": False, "blocked": False}, + {"result_class": "blocked", "matched_expected_label": False, "blocked": True}, + ] + ) + assert summary["positive_event_recall"] == 2 / 3 + assert summary["exact_label_rate"] == 1 / 3 + assert summary["blocked_rate"] == 1 / 4 + assert summary["balanced_accuracy"] is None + assert summary["balanced_accuracy_blocked_reason"] == "no_verified_negative_control_dates" + assert summary["known_event_activation_rate"] == 2 / 3 + assert summary["strong_activation_rate"] == 1 / 3 + assert summary["positive_event_recall_deprecated"] is True + assert summary["exact_label_rate_deprecated"] is True + + +def test_v21_deduplicates_same_md_ad_lord() -> None: + chart = {"planets": {"Jupiter": {"house": 10}}} + roles = {"owned_houses": {"Jupiter": [9, 11]}} + + score, signals = score_active_dasha_lords( + ["Jupiter", "Jupiter"], {6, 9, 10, 11}, chart, roles, {"Sun", "Saturn", "Mercury"} + ) + + assert score == 3 + assert signals.count("Jupiter_owns_event_houses:[9, 11]") == 1 + assert signals.count("Jupiter_occupies_event_house:10") == 1 + + +def test_ashtakavarga_audit_reports_event_houses_and_transit_bav_without_scoring() -> None: + audit = ashtakavarga_audit( + "marriage", + { + "method": "Ashtakavarga", + "version": "2.1", + "sav": {"total": 337, "valid": True, "scores": {"Libra": 30, "Sagittarius": 27}}, + "all_bav_valid": True, + "house_scores": { + "house_2": {"sign": "Leo", "sav_score": 24}, + "house_5": {"sign": "Scorpio", "sav_score": 29}, + "house_7": {"sign": "Capricorn", "sav_score": 31}, + "house_11": {"sign": "Taurus", "sav_score": 32}, + }, + "bav": { + "Jupiter": {"bindus": [0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0], "total": 56, "valid": True}, + "Saturn": {"bindus": [0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0], "total": 39, "valid": True}, + }, + }, + { + "ayanamsa": 24.1, + "node_mode": "mean", + "planets": {"Jupiter": {"sign": "Libra"}, "Saturn": {"sign": "Sagittarius"}}, + }, + ) + + assert audit["status"] == "used_non_scoring" + assert audit["sav_total"] == 337 + assert audit["event_house_sav"]["7"]["sav_score"] == 31 + assert audit["transit_support"]["Jupiter"] == {"sign": "Libra", "sav": 30, "bav": 5} + assert audit["transit_support"]["Saturn"] == {"sign": "Sagittarius", "sav": 27, "bav": 4} + + +def test_engine_json_cache_reuses_identical_subject_command(monkeypatch) -> None: + calls = [] + + class Completed: + stdout = '{"ok": true}' + + def fake_run(*args, **kwargs): + calls.append((args, kwargs)) + return Completed() + + clear_engine_cache() + monkeypatch.setattr("scripts.public_real_case_benchmark.subprocess.run", fake_run) + subject = {"year": 2000, "month": 1, "day": 1, "hour": 12, "minute": 0, "lat": 0, "lon": 0, "tz": 0} + + assert _engine_json("chart", subject) == {"ok": True} + assert _engine_json("chart", subject) == {"ok": True} + assert len(calls) == 1 + clear_engine_cache() + + +def test_committed_report_matches_the_ten_case_manifest() -> None: + manifest = json.loads((ROOT / "references/real_case_calibration/replay_manifest.json").read_text(encoding="utf-8")) + report = json.loads((ROOT / "docs/benchmark/public_real_case_benchmark_2026_07_11.json").read_text(encoding="utf-8")) + assert {case["case_id"] for case in manifest["cases"]} == {case["case_id"] for case in report["cases"]} + assert report["summary"]["total_events"] == 10 + assert report["summary"]["positive_event_recall"] == 0.8 + assert report["summary"]["balanced_accuracy"] is None + + +def test_v2_node_dispositor_adds_general_event_house_support() -> None: + chart = { + "planets": { + "Rahu": {"sign": "Taurus", "house": 3}, + "Venus": {"sign": "Capricorn", "house": 10}, + } + } + roles = {"owned_houses": {"Venus": [3, 10]}} + score, signals = node_dispositor_bonus({"Rahu"}, "career", chart, roles) + assert score == 2 + assert "Rahu_dispositor_Venus_owns_event_house" in signals + assert "Rahu_dispositor_Venus_occupies_event_house:10" in signals + + +def test_v2_varga_and_chara_karaka_support_is_domain_specific() -> None: + varga = { + "divisional_charts": { + "D10_Dasamsa": { + "ascendant": "Taurus", + "Saturn": {"sign": "Aquarius"}, + } + } + } + jaimini = { + "chara_karaka_7": { + "karaka_table": {"Amatyakaraka": {"planet": "Saturn"}} + } + } + score, signals = varga_and_karaka_bonus({"Saturn"}, "career", varga, jaimini) + assert score == 3 + assert "active_dasha_matches_D10_10L:Saturn" in signals + assert "active_dasha_occupies_D10_house_10:Saturn" in signals + assert "active_dasha_matches_Amatyakaraka:Saturn" in signals + + +def test_v2_promotion_requires_holdout_improvement_without_more_blocking() -> None: + promoted = promotion_decision( + {"positive_event_recall": 0.6, "exact_label_rate": 0.2, "blocked_events": 0}, + {"positive_event_recall": 0.8, "exact_label_rate": 0.4, "blocked_events": 0}, + ) + assert promoted["promote"] is True + blocked = promotion_decision( + {"positive_event_recall": 0.6, "exact_label_rate": 0.2, "blocked_events": 0}, + {"positive_event_recall": 0.8, "exact_label_rate": 0.4, "blocked_events": 1}, + ) + assert blocked["promote"] is False + assert blocked["reason"] == "v2_increased_blocked_events" + + +def test_compare_reports_keeps_case_level_deltas_auditable() -> None: + v1 = { + "rule_version": "v1", + "summary": {"positive_event_recall": 0.5, "exact_label_rate": 0.0, "blocked_events": 0}, + "cases": [{"case_id": "case-a", "score": 3, "result_class": "miss", "signals": ["base"]}], + } + v2 = { + "rule_version": "v2", + "summary": {"positive_event_recall": 1.0, "exact_label_rate": 1.0, "blocked_events": 0}, + "cases": [{"case_id": "case-a", "score": 7, "result_class": "strong_hit", "signals": ["base", "new"]}], + } + + comparison = compare_reports(v1, v2) + + assert comparison["promotion"]["promote"] is True + assert comparison["case_deltas"] == [ + { + "case_id": "case-a", + "v1_score": 3, + "v2_score": 7, + "score_delta": 4, + "v1_result_class": "miss", + "v2_result_class": "strong_hit", + "added_signals": ["new"], + } + ] + + +def test_combine_reports_recomputes_twenty_case_summary() -> None: + batch1 = {"cases": [{"case_id": "a", "result_class": "strong_hit", "matched_expected_label": True, "blocked": False}]} + holdout = {"cases": [{"case_id": "b", "result_class": "miss", "matched_expected_label": False, "blocked": False}]} + + combined = combine_reports([batch1, holdout], {"promote": True, "reason": "holdout_metrics_improved"}) + + assert combined["summary"]["total_events"] == 2 + assert combined["summary"]["positive_event_recall"] == 0.5 + assert combined["holdout_promotion"]["promote"] is True + assert [row["case_id"] for row in combined["cases"]] == ["a", "b"] diff --git a/tests/test_public_release_privacy_scan.py b/tests/test_public_release_privacy_scan.py index 24f9d15d..95e6b53f 100644 --- a/tests/test_public_release_privacy_scan.py +++ b/tests/test_public_release_privacy_scan.py @@ -29,3 +29,9 @@ def test_public_release_privacy_scan_supports_unpacked_zip_without_git(tmp_path: assert [path.name for path in iter_release_files(tmp_path)] == ["INSTALL.md"] report = build_report(tmp_path) assert report["status"] == "pass", report["findings"] + + +def test_private_workspace_directories_are_gitignored() -> None: + gitignore = (Path(__file__).resolve().parents[1] / ".gitignore").read_text(encoding="utf-8").splitlines() + assert "/scratch/" in gitignore + assert "/.serena/" in gitignore diff --git a/tests/test_real_case_negative_controls.py b/tests/test_real_case_negative_controls.py new file mode 100644 index 00000000..2eb8183d --- /dev/null +++ b/tests/test_real_case_negative_controls.py @@ -0,0 +1,53 @@ +from __future__ import annotations + +from scripts.public_real_case_negative_controls import ( + generate_control_dates, + rank_positive_against_controls, + summarize_negative_control_rows, +) + + +def test_generate_control_dates_uses_fixed_offsets_without_positive_date() -> None: + dates = generate_control_dates("2018-05-19", offsets=(-60, -30, 30, 60)) + + assert dates == ["2018-03-20", "2018-04-19", "2018-06-18", "2018-07-18"] + assert "2018-05-19" not in dates + + +def test_rank_positive_uses_conservative_tie_ordering() -> None: + result = rank_positive_against_controls(5, [7, 5, 4]) + + assert result == { + "positive_score": 5, + "positive_rank": 3, + "candidate_count": 4, + "reciprocal_rank": 1 / 3, + "top_1": False, + "top_3": True, + "max_control_score": 7, + "score_margin": -2, + } + + +def test_negative_control_summary_reports_false_activations() -> None: + summary = summarize_negative_control_rows( + [ + { + "ranking": {"top_1": True, "top_3": True, "reciprocal_rank": 1.0, "score_margin": 2}, + "controls": [{"score": 2}, {"score": 3}], + }, + { + "ranking": {"top_1": False, "top_3": True, "reciprocal_rank": 0.5, "score_margin": -1}, + "controls": [{"score": 4}, {"score": 7}], + }, + ] + ) + + assert summary["case_count"] == 2 + assert summary["control_date_count"] == 4 + assert summary["control_activation_rate"] == 0.5 + assert summary["control_strong_activation_rate"] == 0.25 + assert summary["positive_top_1_rate"] == 0.5 + assert summary["positive_top_3_rate"] == 1.0 + assert summary["mean_reciprocal_rank"] == 0.75 + assert summary["mean_score_margin"] == 0.5 diff --git a/tests/test_real_case_replay_validator.py b/tests/test_real_case_replay_validator.py index 85bb107c..fd9e6a24 100644 --- a/tests/test_real_case_replay_validator.py +++ b/tests/test_real_case_replay_validator.py @@ -9,13 +9,50 @@ from scripts.unified_consultation_orchestrator import UnifiedConsultationOrchest ROOT = Path(__file__).resolve().parents[1] -def test_real_case_replay_manifest_blocks_when_no_cases_are_imported() -> None: +def test_real_case_replay_manifest_contains_ten_research_grade_cases() -> None: result = validate_manifest(ROOT / "references/real_case_calibration/replay_manifest.json") - assert result["status"] == "blocked" - assert result["case_count"] == 0 - assert result["replay_ready_count"] == 0 - assert result["blocked_reason"] == "no_structured_outcome_replay_cases_imported" + assert result["status"] == "pass" + assert result["case_count"] == 10 + assert result["replay_ready_count"] == 10 + assert result["domain_counts"] == {"career": 5, "marriage": 5} + assert result["birth_time_ratings"] == {"A": 2, "AA": 8} + + +def test_holdout_manifest_contains_ten_new_balanced_cases() -> None: + batch1_path = ROOT / "references/real_case_calibration/replay_manifest.json" + holdout_path = ROOT / "references/real_case_calibration/replay_manifest_holdout_v2.json" + result = validate_manifest(holdout_path) + assert result["status"] == "pass" + assert result["case_count"] == 10 + assert result["domain_counts"] == {"career": 5, "marriage": 5} + assert result["birth_time_ratings"] == {"A": 5, "AA": 5} + batch1 = json.loads(batch1_path.read_text(encoding="utf-8")) + holdout = json.loads(holdout_path.read_text(encoding="utf-8")) + assert {case["subject"]["name"] for case in batch1["cases"]}.isdisjoint( + {case["subject"]["name"] for case in holdout["cases"]} + ) + + +def test_three_case_probe_is_aa_and_disjoint_from_prior_twenty() -> None: + probe_path = ROOT / "references/real_case_calibration/replay_manifest_probe3_v2.json" + result = validate_manifest(probe_path) + + assert result["status"] == "pass" + assert result["case_count"] == 3 + assert result["replay_ready_count"] == 3 + assert result["domain_counts"] == {"career": 2, "marriage": 1} + assert result["birth_time_ratings"] == {"AA": 3} + + prior_names = set() + for path in ( + ROOT / "references/real_case_calibration/replay_manifest.json", + ROOT / "references/real_case_calibration/replay_manifest_holdout_v2.json", + ): + payload = json.loads(path.read_text(encoding="utf-8")) + prior_names.update(case["subject"]["name"] for case in payload["cases"]) + probe = json.loads(probe_path.read_text(encoding="utf-8")) + assert prior_names.isdisjoint(case["subject"]["name"] for case in probe["cases"]) def test_real_case_replay_validator_accepts_one_structured_case(tmp_path: Path) -> None: @@ -26,6 +63,24 @@ def test_real_case_replay_validator_accepts_one_structured_case(tmp_path: Path) "cases": [ { "case_id": "public_case_001", + "subject": { + "name": "Public Case", + "year": 1970, + "month": 1, + "day": 1, + "hour": 12, + "minute": 0, + "lat": 0.0, + "lon": 0.0, + "tz": 0.0, + "node_mode": "mean", + "birth_source": { + "url": "https://example.com/birth-record", + "source_grade": "primary", + "time_accuracy_rating": "AA", + "evidence_basis": "birth_record_in_hand", + }, + }, "source": { "url": "https://example.com/public-case", "source_grade": "verified_secondary", @@ -36,7 +91,13 @@ def test_real_case_replay_validator_accepts_one_structured_case(tmp_path: Path) { "event_type": "career_breakthrough", "event_date": "2000-01", + "domain": "career", + "expected_label": "career_status", "outcome": "public_success", + "source": { + "url": "https://example.com/event", + "source_grade": "verified_secondary", + }, } ], "similarity": { @@ -61,6 +122,62 @@ def test_real_case_replay_validator_accepts_one_structured_case(tmp_path: Path) assert result["replay_ready_count"] == 1 +def test_real_case_replay_validator_rejects_low_accuracy_birth_time_and_unsourced_event(tmp_path: Path) -> None: + manifest = { + "schema_version": "2.0", + "status": "ready", + "case_schema": "references/real_case_calibration/catalog.schema.json", + "cases": [ + { + "case_id": "weak_case", + "subject": { + "name": "Weak Case", + "year": 1970, + "month": 1, + "day": 1, + "hour": 12, + "minute": 0, + "lat": 0.0, + "lon": 0.0, + "tz": 0.0, + "node_mode": "mean", + "birth_source": { + "url": "https://example.com/birth", + "source_grade": "unverified", + "time_accuracy_rating": "DD", + "evidence_basis": "conflicting_times", + }, + }, + "source": { + "url": "https://example.com/case", + "source_grade": "unverified", + "license_or_quote_boundary": "summary_only", + }, + "chart_signature": {}, + "event_outcomes": [ + { + "event_type": "legal_marriage", + "event_date": "2000-01-01", + "domain": "marriage", + "expected_label": "legal_marriage", + "outcome": "married", + } + ], + "similarity": {"score": 0.0, "matching_factors": [], "dissimilar_factors": []}, + "replay": {"outcome_replay_status": "replayed", "do_not_use_for_prediction": False}, + } + ], + } + path = tmp_path / "replay_manifest.json" + path.write_text(json.dumps(manifest), encoding="utf-8") + result = validate_manifest(path) + assert result["status"] == "invalid" + assert {error["error"] for error in result["errors"]} >= { + "birth_time_rating_below_A", + "missing", + } + + def test_orchestrator_exposes_real_case_replay_manifest_status() -> None: orchestrator = UnifiedConsultationOrchestrator() route = {"question_type": "career", "primary_theme": "career"} @@ -68,6 +185,56 @@ def test_orchestrator_exposes_real_case_replay_manifest_status() -> None: packet = orchestrator.real_case_calibration_catalog(route_packet=route, machine_evidence_packet={}) replay = packet["outcome_replay_manifest"] - assert replay["status"] == "blocked" - assert replay["case_count"] == 0 + assert replay["status"] == "pass" + assert replay["case_count"] == 10 + holdout = packet["holdout_replay_manifest"] + assert holdout["status"] == "pass" + assert holdout["case_count"] == 10 + benchmark = packet["public_outcome_benchmark"] + assert benchmark["status"] == "used" + assert benchmark["summary"]["total_events"] == 20 + assert benchmark["summary"]["positive_event_recall"] == 0.8 + assert benchmark["summary"]["exact_label_rate"] == 0.4 + assert benchmark["summary"]["balanced_accuracy"] is None + assert benchmark["holdout_promotion"] == {"promote": True, "reason": "holdout_metrics_improved"} + supplemental = packet["supplemental_public_probe"] + assert supplemental["status"] == "used" + assert supplemental["summary"]["total_events"] == 3 + assert supplemental["summary"]["positive_event_recall"] == 1 / 3 + assert supplemental["combined_observation"]["total_events"] == 23 + assert supplemental["combined_observation"]["positive_event_recall"] == 17 / 23 + corrected = packet["corrected_v21_observation"] + assert corrected["status"] == "used" + assert corrected["summary"]["total_events"] == 23 + assert corrected["summary"]["positive_event_recall_deprecated"] is True + assert corrected["ashtakavarga_audit_status"] == "used_non_scoring" + negative = packet["negative_control_pilot"] + assert negative["status"] == "used" + assert negative["summary"]["control_date_count"] == 24 + assert negative["summary"]["positive_top_1_rate"] == 0.0 + assert negative["summary"]["positive_top_3_rate"] == 0.0 + annual = packet["annual_control_pilot"] + assert annual["status"] == "used" + assert annual["summary"]["control_date_count"] == 12 + assert annual["summary"]["positive_top_1_rate"] == 1 / 3 + timing_gate = packet["timing_precision_gate"] + assert timing_gate["status"] == "blocked" + assert timing_gate["maximum_supported_precision"] == "unvalidated_broad_window" + assert timing_gate["blocked_claims"] == ["exact_day", "exact_month_from_current_replay_score"] + assert timing_gate["domain_support"] == {"career": "blocked", "marriage": "partial_candidate"} + runtime_log = orchestrator.runtime_evidence_log( + surface="api_web", + entry_mode="direct_chart", + route_packet=route, + executed_steps=["compute_chart"], + skipped_steps=[], + real_case_calibration=packet, + ) + assert "timing_precision_gate_blocked" in runtime_log["quality_gate"]["blocked_items"] + timing_row = next( + row for row in runtime_log["quality_gate"]["technique_audit_table"] + if row["technique"] == "Timing Precision Gate" + ) + assert timing_row["status"] == "blocked" + assert timing_row["maximum_supported_precision"] == "unvalidated_broad_window" assert packet["required_replay_schema"] == "references/real_case_calibration/catalog.schema.json" diff --git a/tests/test_unified_consultation_orchestrator.py b/tests/test_unified_consultation_orchestrator.py index d73bab02..865bec22 100644 --- a/tests/test_unified_consultation_orchestrator.py +++ b/tests/test_unified_consultation_orchestrator.py @@ -198,9 +198,10 @@ def test_runtime_evidence_log_exposes_blind_packet_case_and_quality_gate_contrac "Cross-System Arbitration", "Evidence Packet", "Blind Technical Mode", - "MEVG / Global Web Evidence", - "Real Case Calibration", - "Functional Benefic/Malefic", + "MEVG / Global Web Evidence", + "Real Case Calibration", + "Timing Precision Gate", + "Functional Benefic/Malefic", ] engines = log["external_engine_cross_validation"]["engines"] assert engines["VedAstro"]["status"] == "local_fallback" From a707f670deb43a483503ed3ecc72aace7c62ff76 Mon Sep 17 00:00:00 2001 From: 732642856 <732642856@qq.com> Date: Sun, 12 Jul 2026 00:30:32 +0800 Subject: [PATCH 03/61] docs: publish calibrated public-case evidence --- ..._v21_corrected_observation_2026_07_11.json | 8012 +++++++++++++++++ ..._case_annual_control_pilot_2026_07_11.json | 1167 +++ ...ase_negative_control_pilot_2026_07_11.json | 1254 +++ ...eal_case_calibration_release_2026_07_12.md | 33 + ..._case_negative_control_pilot_2026_07_11.md | 45 + ...real_case_v21_sav_correction_2026_07_11.md | 42 + 6 files changed, 10553 insertions(+) create mode 100644 docs/benchmark/public_real_case_23_case_v21_corrected_observation_2026_07_11.json create mode 100644 docs/benchmark/public_real_case_annual_control_pilot_2026_07_11.json create mode 100644 docs/benchmark/public_real_case_negative_control_pilot_2026_07_11.json create mode 100644 docs/research/public_real_case_calibration_release_2026_07_12.md create mode 100644 docs/research/public_real_case_negative_control_pilot_2026_07_11.md create mode 100644 docs/research/public_real_case_v21_sav_correction_2026_07_11.md diff --git a/docs/benchmark/public_real_case_23_case_v21_corrected_observation_2026_07_11.json b/docs/benchmark/public_real_case_23_case_v21_corrected_observation_2026_07_11.json new file mode 100644 index 00000000..72d970e2 --- /dev/null +++ b/docs/benchmark/public_real_case_23_case_v21_corrected_observation_2026_07_11.json @@ -0,0 +1,8012 @@ +{ + "ashtakavarga_audit_status": "used_non_scoring", + "ashtakavarga_descriptive": { + "hit": { + "event_house_sav_mean": 28.809, + "transit_bav_mean": 3.882, + "transit_sav_mean": 28.735 + }, + "miss": { + "event_house_sav_mean": 30.375, + "transit_bav_mean": 3.917, + "transit_sav_mean": 27.583 + } + }, + "ashtakavarga_descriptive_boundary": "Positive events only; these averages cannot establish discrimination or scoring weights.", + "benchmark_id": "public_real_case_23_case_v21_corrected_observation_2026_07_11", + "boundary": "Corrected observational replay on already-seen positive events. Requires a fresh holdout and negative controls before promotion or accuracy claims.", + "cases": [ + { + "actual_label": "career_status", + "birth_time_rating": "AA", + "blocked": false, + "case_id": "jobs_iphone_2007", + "domain": "career", + "event_date": "2007-01-09", + "evidence": { + "arudha_lord": "Moon", + "ascendant": { + "degree": 29.0634, + "degree_in_sign": 29.0634, + "degree_in_sign_raw": 149.06338525851115, + "degree_raw": 149.0634, + "lon": 149.0634, + "lord": "Sun", + "sign": "Leo", + "sign_cn": "狮子座" + }, + "ashtakavarga_audit": { + "all_bav_valid": true, + "boundary": "Audit evidence only. SAV/BAV does not change V2.1 event scores until a fresh holdout validates it.", + "event_house_sav": { + "10": { + "level": "极吉", + "sav_score": 36, + "sign": "Taurus" + }, + "11": { + "level": "吉利", + "sav_score": 28, + "sign": "Gemini" + }, + "6": { + "level": "极吉", + "sav_score": 35, + "sign": "Capricorn" + }, + "9": { + "level": "吉利", + "sav_score": 28, + "sign": "Aries" + } + }, + "method": "Ashtakavarga八分法(BPHS/PVR书例校准v2.1)", + "sav_total": 337, + "sav_valid": true, + "scoring_effect": 0, + "settings": { + "ayanamsa": 23.9552, + "node_mode": "mean" + }, + "status": "used_non_scoring", + "transit_support": { + "Jupiter": { + "bav": 5, + "sav": 33, + "sign": "Scorpio" + }, + "Saturn": { + "bav": 6, + "sav": 29, + "sign": "Leo" + } + }, + "version": "2.1" + }, + "birth_source": { + "evidence_basis": "BC/BR in hand", + "source_grade": "primary", + "time_accuracy_rating": "AA", + "url": "https://www.astro.com/adbvip/adbvip_02_24.htm" + }, + "domain_varga": { + "birth_info": "1955-02-24 19:15", + "divisional_charts": { + "D10_Dasamsa": { + "Jupiter": { + "sign": "Pisces", + "sign_cn": "双鱼座" + }, + "Ketu": { + "sign": "Virgo", + "sign_cn": "处女座" + }, + "Mars": { + "sign": "Taurus", + "sign_cn": "金牛座" + }, + "Mercury": { + "sign": "Aries", + "sign_cn": "白羊座" + }, + "Moon": { + "sign": "Pisces", + "sign_cn": "双鱼座" + }, + "Rahu": { + "sign": "Pisces", + "sign_cn": "双鱼座" + }, + "Saturn": { + "sign": "Cancer", + "sign_cn": "巨蟹座" + }, + "Sun": { + "sign": "Gemini", + "sign_cn": "双子座" + }, + "Venus": { + "sign": "Virgo", + "sign_cn": "处女座" + }, + "ascendant": "Taurus" + } + } + }, + "double_transit_pac": { + "cl": { + "jupiter": { + "CL_Jupiter(宫主)": [ + { + "desc": "7宫相位", + "offset": 7, + "type": "Aspect" + } + ] + }, + "saturn": { + "CL_Jupiter(宫主)": [ + { + "desc": "10宫相位", + "offset": 10, + "type": "Aspect" + } + ] + } + }, + "d1": { + "jupiter": {}, + "saturn": {} + }, + "d9": { + "jupiter": {}, + "saturn": {} + }, + "double_transit": [ + { + "jupiter_pac": [ + { + "desc": "7宫相位", + "offset": 7, + "type": "Aspect" + } + ], + "layer": "CL", + "saturn_pac": [ + { + "desc": "10宫相位", + "offset": 10, + "type": "Aspect" + } + ], + "strength": "strong", + "target": "CL_Jupiter(宫主)" + } + ], + "event_house": 10, + "stats": { + "chandra_lagna": "Pisces", + "cl_jupiter_targets": [ + "CL_Jupiter(宫主)" + ], + "cl_overlap": [ + "CL_Jupiter(宫主)" + ], + "cl_saturn_targets": [ + "CL_Jupiter(宫主)" + ], + "d1_jupiter_targets": [], + "d1_overlap": [], + "d1_saturn_targets": [], + "d9_ascendant": "Sagittarius", + "d9_jupiter_targets": [], + "d9_overlap": [], + "d9_saturn_targets": [], + "event_lord_d9_sign": "Sagittarius" + }, + "summary": "⚠️ Chandra Lagna 层 Double Transit 激活,D1/D9 未确认", + "transit_date": "2007-01-09" + }, + "event_source": { + "source_grade": "primary", + "url": "https://www.apple.com/newsroom/2007/01/09Apple-Reinvents-the-Phone-with-iPhone/" + }, + "functional_benefic_malefic": { + "ascendant": "Leo", + "effect_on_confidence": "高严谨模式下必须叠加功能性宫主吉凶与自然吉凶;若功能属性与自然属性冲突,应降低置信度或显式标记冲突。", + "functional_benefics": [ + "Jupiter", + "Mars", + "Sun" + ], + "functional_malefics": [ + "Mercury", + "Moon", + "Saturn", + "Venus" + ], + "functional_neutrals": [], + "owned_houses": { + "Jupiter": [ + 5, + 8 + ], + "Mars": [ + 4, + 9 + ], + "Mercury": [ + 2, + 11 + ], + "Moon": [ + 12 + ], + "Saturn": [ + 6, + 7 + ], + "Sun": [ + 1 + ], + "Venus": [ + 3, + 10 + ] + }, + "source": "strict_functional_benefic_malefic_v1", + "status": "used", + "yogakarakas": [ + "Mars" + ] + }, + "narayana": { + "ad": { + "end_age": 51.8831, + "lord": "Jupiter", + "sign": "Sagittarius", + "sign_idx": 8, + "start_age": 50.9481, + "years": 0.9351 + }, + "md": { + "end_age": 55.0, + "lord": "Mars", + "sign": "Aries", + "sign_idx": 0, + "start_age": 43.0, + "years": 12 + }, + "pd": { + "end_age": 51.8831, + "lord": "Mars", + "sign": "Scorpio", + "sign_idx": 7, + "start_age": 51.8225, + "years": 0.0606 + }, + "remaining_years": 3.12 + }, + "vimshottari": { + "antardasha": "Ketu", + "mahadasha": "Sun" + } + }, + "expected_label": "career_status", + "matched_expected_label": true, + "name": "Steve Jobs", + "outcome": "Apple publicly introduced the iPhone", + "result_class": "strong_hit", + "rule_version": "v2_1", + "score": 7, + "signals": [ + "Sun_domain_karaka", + "Ketu_occupies_event_house:11", + "Ketu_dispositor_Mercury_owns_event_house", + "Ketu_dispositor_Mercury_occupies_event_house:6", + "narayana_lord_owns_event_house:Mars", + "double_transit_pac_strong" + ] + }, + { + "actual_label": null, + "birth_time_rating": "AA", + "blocked": false, + "case_id": "obama_election_2008", + "domain": "career", + "event_date": "2008-11-04", + "evidence": { + "arudha_lord": "Saturn", + "ascendant": { + "degree": 24.724, + "degree_in_sign": 24.724, + "degree_in_sign_raw": 294.72398910355867, + "degree_raw": 294.724, + "lon": 294.724, + "lord": "Saturn", + "sign": "Capricorn", + "sign_cn": "摩羯座" + }, + "ashtakavarga_audit": { + "all_bav_valid": true, + "boundary": "Audit evidence only. SAV/BAV does not change V2.1 event scores until a fresh holdout validates it.", + "event_house_sav": { + "10": { + "level": "极吉", + "sav_score": 31, + "sign": "Libra" + }, + "11": { + "level": "极吉", + "sav_score": 39, + "sign": "Scorpio" + }, + "6": { + "level": "极吉", + "sav_score": 30, + "sign": "Gemini" + }, + "9": { + "level": "中等", + "sav_score": 26, + "sign": "Virgo" + } + }, + "method": "Ashtakavarga八分法(BPHS/PVR书例校准v2.1)", + "sav_total": 337, + "sav_valid": true, + "scoring_effect": 0, + "settings": { + "ayanamsa": 23.9806, + "node_mode": "mean" + }, + "status": "used_non_scoring", + "transit_support": { + "Jupiter": { + "bav": 2, + "sav": 20, + "sign": "Sagittarius" + }, + "Saturn": { + "bav": 1, + "sav": 23, + "sign": "Leo" + } + }, + "version": "2.1" + }, + "birth_source": { + "evidence_basis": "BC/BR in hand", + "source_grade": "primary", + "time_accuracy_rating": "AA", + "url": "https://www.astro.com/adbvip/adbvip_08_04.htm" + }, + "domain_varga": { + "birth_info": "1961-08-04 19:24", + "divisional_charts": { + "D10_Dasamsa": { + "Jupiter": { + "sign": "Scorpio", + "sign_cn": "天蝎座" + }, + "Ketu": { + "sign": "Pisces", + "sign_cn": "双鱼座" + }, + "Mars": { + "sign": "Taurus", + "sign_cn": "金牛座" + }, + "Mercury": { + "sign": "Gemini", + "sign_cn": "双子座" + }, + "Moon": { + "sign": "Aries", + "sign_cn": "白羊座" + }, + "Rahu": { + "sign": "Virgo", + "sign_cn": "处女座" + }, + "Saturn": { + "sign": "Virgo", + "sign_cn": "处女座" + }, + "Sun": { + "sign": "Virgo", + "sign_cn": "处女座" + }, + "Venus": { + "sign": "Leo", + "sign_cn": "狮子座" + }, + "ascendant": "Taurus" + } + } + }, + "double_transit_pac": { + "cl": { + "jupiter": { + "CL_Saturn(宫主)": [ + { + "desc": "合相(8.55°)", + "type": "Conjunction" + } + ] + }, + "saturn": {} + }, + "d1": { + "jupiter": { + "Moon(对宫主)": [ + { + "desc": "5宫相位", + "offset": 5, + "type": "Aspect" + } + ], + "Saturn(LL)": [ + { + "desc": "合相(8.55°)", + "type": "Conjunction" + } + ] + }, + "saturn": { + "Venus(宫主)": [ + { + "desc": "10宫相位", + "offset": 10, + "type": "Aspect" + } + ] + } + }, + "d9": { + "jupiter": { + "Venus_D9(Sagittarius)": [ + { + "desc": "同宫(5宫)", + "type": "Position" + }, + { + "desc": "合相(8.46°)", + "type": "Conjunction" + } + ] + }, + "saturn": { + "D9_10宫(Taurus)": [ + { + "desc": "同宫(1宫)", + "type": "Position" + } + ], + "D9_Venus(宫主)": [ + { + "desc": "10宫相位", + "offset": 10, + "type": "Aspect" + } + ] + } + }, + "double_transit": [ + { + "jupiter_pac": [ + { + "desc": "同宫(5宫)", + "type": "Position" + }, + { + "desc": "合相(8.46°)", + "type": "Conjunction" + } + ], + "layer": "D1+D9", + "saturn_pac": [ + { + "desc": "10宫相位", + "offset": 10, + "type": "Aspect" + } + ], + "strength": "moderate", + "target": "Saturn(D1)Venus(宫主) + Jupiter(D9)Venus_D9(Sagittarius)" + } + ], + "event_house": 10, + "stats": { + "chandra_lagna": "Taurus", + "cl_jupiter_targets": [ + "CL_Saturn(宫主)" + ], + "cl_overlap": [], + "cl_saturn_targets": [], + "d1_jupiter_targets": [ + "Moon(对宫主)", + "Saturn(LL)" + ], + "d1_overlap": [], + "d1_saturn_targets": [ + "Venus(宫主)" + ], + "d9_ascendant": "Leo", + "d9_jupiter_targets": [ + "Venus_D9(Sagittarius)" + ], + "d9_overlap": [], + "d9_saturn_targets": [ + "D9_10宫(Taurus)", + "D9_Venus(宫主)" + ], + "event_lord_d9_sign": "Sagittarius" + }, + "summary": "⚠️ 跨层间接 Double Transit (D1+D9),需结合 Dasha 确认", + "transit_date": "2008-11-04" + }, + "event_source": { + "source_grade": "primary", + "url": "https://www.fec.gov/introduction-campaign-finance/election-results-and-voting-information/federal-elections-2008/" + }, + "functional_benefic_malefic": { + "ascendant": "Capricorn", + "effect_on_confidence": "高严谨模式下必须叠加功能性宫主吉凶与自然吉凶;若功能属性与自然属性冲突,应降低置信度或显式标记冲突。", + "functional_benefics": [ + "Mercury", + "Saturn", + "Venus" + ], + "functional_malefics": [ + "Jupiter", + "Mars" + ], + "functional_neutrals": [ + "Moon", + "Sun" + ], + "owned_houses": { + "Jupiter": [ + 3, + 12 + ], + "Mars": [ + 4, + 11 + ], + "Mercury": [ + 6, + 9 + ], + "Moon": [ + 7 + ], + "Saturn": [ + 1, + 2 + ], + "Sun": [ + 8 + ], + "Venus": [ + 5, + 10 + ] + }, + "source": "strict_functional_benefic_malefic_v1", + "status": "used", + "yogakarakas": [ + "Saturn", + "Venus" + ] + }, + "narayana": { + "ad": { + "end_age": 48.3182, + "lord": "Jupiter", + "sign": "Pisces", + "sign_idx": 11, + "start_age": 47.1818, + "years": 1.1364 + }, + "md": { + "end_age": 49.0, + "lord": "Moon", + "sign": "Cancer", + "sign_idx": 3, + "start_age": 39.0, + "years": 10 + }, + "pd": { + "end_age": 47.3109, + "lord": "Jupiter", + "sign": "Pisces", + "sign_idx": 11, + "start_age": 47.1818, + "years": 0.1291 + }, + "remaining_years": 1.75 + }, + "vimshottari": { + "antardasha": "Moon", + "mahadasha": "Jupiter" + } + }, + "expected_label": "career_status", + "matched_expected_label": false, + "name": "Barack Obama", + "outcome": "Won the United States presidential election", + "result_class": "miss", + "rule_version": "v2_1", + "score": 1, + "signals": [ + "double_transit_pac_present" + ] + }, + { + "actual_label": null, + "birth_time_rating": "A", + "blocked": false, + "case_id": "schwarzenegger_governor_2003", + "domain": "career", + "event_date": "2003-10-07", + "evidence": { + "arudha_lord": "Venus", + "ascendant": { + "degree": 25.9942, + "degree_in_sign": 25.9942, + "degree_in_sign_raw": 85.99419230270675, + "degree_raw": 85.9942, + "lon": 85.9942, + "lord": "Mercury", + "sign": "Gemini", + "sign_cn": "双子座" + }, + "ashtakavarga_audit": { + "all_bav_valid": true, + "boundary": "Audit evidence only. SAV/BAV does not change V2.1 event scores until a fresh holdout validates it.", + "event_house_sav": { + "10": { + "level": "极吉", + "sav_score": 35, + "sign": "Pisces" + }, + "11": { + "level": "极吉", + "sav_score": 32, + "sign": "Aries" + }, + "6": { + "level": "吉利", + "sav_score": 29, + "sign": "Scorpio" + }, + "9": { + "level": "极吉", + "sav_score": 33, + "sign": "Aquarius" + } + }, + "method": "Ashtakavarga八分法(BPHS/PVR书例校准v2.1)", + "sav_total": 337, + "sav_valid": true, + "scoring_effect": 0, + "settings": { + "ayanamsa": 23.9097, + "node_mode": "mean" + }, + "status": "used_non_scoring", + "transit_support": { + "Jupiter": { + "bav": 5, + "sav": 33, + "sign": "Leo" + }, + "Saturn": { + "bav": 2, + "sav": 23, + "sign": "Gemini" + } + }, + "version": "2.1" + }, + "birth_source": { + "evidence_basis": "from memory", + "source_grade": "verified_secondary", + "time_accuracy_rating": "A", + "url": "https://www.astro.com/adbvip/adbvip_07_30.htm" + }, + "domain_varga": { + "birth_info": "1947-07-30 04:10", + "divisional_charts": { + "D10_Dasamsa": { + "Jupiter": { + "sign": "Gemini", + "sign_cn": "双子座" + }, + "Ketu": { + "sign": "Leo", + "sign_cn": "狮子座" + }, + "Mars": { + "sign": "Virgo", + "sign_cn": "处女座" + }, + "Mercury": { + "sign": "Aquarius", + "sign_cn": "水瓶座" + }, + "Moon": { + "sign": "Pisces", + "sign_cn": "双鱼座" + }, + "Rahu": { + "sign": "Aquarius", + "sign_cn": "水瓶座" + }, + "Saturn": { + "sign": "Virgo", + "sign_cn": "处女座" + }, + "Sun": { + "sign": "Cancer", + "sign_cn": "巨蟹座" + }, + "Venus": { + "sign": "Aries", + "sign_cn": "白羊座" + }, + "ascendant": "Aquarius" + } + } + }, + "double_transit_pac": { + "cl": { + "jupiter": {}, + "saturn": { + "CL_10宫(Virgo)": [ + { + "desc": "3宫相位", + "offset": 3, + "type": "Aspect" + } + ], + "CL_Mercury(宫主)": [ + { + "desc": "同宫(7宫)", + "type": "Position" + }, + { + "desc": "合相(5.91°)", + "type": "Conjunction" + } + ] + } + }, + "d1": { + "jupiter": { + "10宫(Pisces)": [ + { + "desc": "7宫相位", + "offset": 7, + "type": "Aspect" + } + ] + }, + "saturn": { + "Mercury(LL)": [ + { + "desc": "同宫(1宫)", + "type": "Position" + }, + { + "desc": "合相(5.91°)", + "type": "Conjunction" + } + ] + } + }, + "d9": { + "jupiter": { + "D9_10宫(Aquarius)": [ + { + "desc": "9宫相位", + "offset": 9, + "type": "Aspect" + } + ], + "Jupiter_D9(Taurus)": [ + { + "desc": "9宫相位", + "offset": 9, + "type": "Aspect" + } + ], + "Mercury_D9(Taurus)": [ + { + "desc": "9宫相位", + "offset": 9, + "type": "Aspect" + } + ] + }, + "saturn": {} + }, + "double_transit": [], + "event_house": 10, + "stats": { + "chandra_lagna": "Sagittarius", + "cl_jupiter_targets": [], + "cl_overlap": [], + "cl_saturn_targets": [ + "CL_10宫(Virgo)", + "CL_Mercury(宫主)" + ], + "d1_jupiter_targets": [ + "10宫(Pisces)" + ], + "d1_overlap": [], + "d1_saturn_targets": [ + "Mercury(LL)" + ], + "d9_ascendant": "Taurus", + "d9_jupiter_targets": [ + "D9_10宫(Aquarius)", + "Jupiter_D9(Taurus)", + "Mercury_D9(Taurus)" + ], + "d9_overlap": [], + "d9_saturn_targets": [], + "event_lord_d9_sign": "Taurus" + }, + "summary": "❌ 无 Double Transit PAC 激活", + "transit_date": "2003-10-07" + }, + "event_source": { + "source_grade": "primary", + "url": "https://elections.cdn.sos.ca.gov/sov/2003-special/sov-complete.pdf" + }, + "functional_benefic_malefic": { + "ascendant": "Gemini", + "effect_on_confidence": "高严谨模式下必须叠加功能性宫主吉凶与自然吉凶;若功能属性与自然属性冲突,应降低置信度或显式标记冲突。", + "functional_benefics": [ + "Mercury", + "Saturn", + "Venus" + ], + "functional_malefics": [ + "Mars", + "Sun" + ], + "functional_neutrals": [ + "Jupiter", + "Moon" + ], + "owned_houses": { + "Jupiter": [ + 7, + 10 + ], + "Mars": [ + 6, + 11 + ], + "Mercury": [ + 1, + 4 + ], + "Moon": [ + 2 + ], + "Saturn": [ + 8, + 9 + ], + "Sun": [ + 3 + ], + "Venus": [ + 5, + 12 + ] + }, + "source": "strict_functional_benefic_malefic_v1", + "status": "used", + "yogakarakas": [ + "Mercury" + ] + }, + "narayana": { + "ad": { + "end_age": 57.1807, + "lord": "Mercury", + "sign": "Gemini", + "sign_idx": 2, + "start_age": 55.7349, + "years": 1.4458 + }, + "md": { + "end_age": 62.0, + "lord": "Jupiter", + "sign": "Sagittarius", + "sign_idx": 8, + "start_age": 52.0, + "years": 10 + }, + "pd": { + "end_age": 56.2226, + "lord": "Sun", + "sign": "Leo", + "sign_idx": 4, + "start_age": 56.031, + "years": 0.1916 + }, + "remaining_years": 5.81 + }, + "vimshottari": { + "antardasha": "Venus", + "mahadasha": "Rahu" + } + }, + "expected_label": "career_status", + "matched_expected_label": false, + "name": "Arnold Schwarzenegger", + "outcome": "Won the California gubernatorial recall election", + "result_class": "miss", + "rule_version": "v2_1", + "score": 2, + "signals": [ + "active_dasha_matches_A10_lord:Venus", + "narayana_lord_owns_event_house:Jupiter" + ] + }, + { + "actual_label": "domain_activation", + "birth_time_rating": "AA", + "blocked": false, + "case_id": "streep_oscar_1983", + "domain": "career", + "event_date": "1983-04-11", + "evidence": { + "arudha_lord": "Mercury", + "ascendant": { + "degree": 9.596, + "degree_in_sign": 9.596, + "degree_in_sign_raw": 99.59604722350997, + "degree_raw": 99.596, + "lon": 99.596, + "lord": "Moon", + "sign": "Cancer", + "sign_cn": "巨蟹座" + }, + "ashtakavarga_audit": { + "all_bav_valid": true, + "boundary": "Audit evidence only. SAV/BAV does not change V2.1 event scores until a fresh holdout validates it.", + "event_house_sav": { + "10": { + "level": "极吉", + "sav_score": 32, + "sign": "Aries" + }, + "11": { + "level": "吉利", + "sav_score": 29, + "sign": "Taurus" + }, + "6": { + "level": "吉利", + "sav_score": 28, + "sign": "Sagittarius" + }, + "9": { + "level": "极吉", + "sav_score": 30, + "sign": "Pisces" + } + }, + "method": "Ashtakavarga八分法(BPHS/PVR书例校准v2.1)", + "sav_total": 337, + "sav_valid": true, + "scoring_effect": 0, + "settings": { + "ayanamsa": 23.6235, + "node_mode": "mean" + }, + "status": "used_non_scoring", + "transit_support": { + "Jupiter": { + "bav": 4, + "sav": 27, + "sign": "Scorpio" + }, + "Saturn": { + "bav": 4, + "sav": 30, + "sign": "Libra" + } + }, + "version": "2.1" + }, + "birth_source": { + "evidence_basis": "BC/BR in hand", + "source_grade": "primary", + "time_accuracy_rating": "AA", + "url": "https://www.astro.com/adbvip/adbvip_06_22.htm" + }, + "domain_varga": { + "birth_info": "1949-06-22 08:05", + "divisional_charts": { + "D10_Dasamsa": { + "Jupiter": { + "sign": "Scorpio", + "sign_cn": "天蝎座" + }, + "Ketu": { + "sign": "Aquarius", + "sign_cn": "水瓶座" + }, + "Mars": { + "sign": "Gemini", + "sign_cn": "双子座" + }, + "Mercury": { + "sign": "Gemini", + "sign_cn": "双子座" + }, + "Moon": { + "sign": "Scorpio", + "sign_cn": "天蝎座" + }, + "Rahu": { + "sign": "Leo", + "sign_cn": "狮子座" + }, + "Saturn": { + "sign": "Libra", + "sign_cn": "天秤座" + }, + "Sun": { + "sign": "Leo", + "sign_cn": "狮子座" + }, + "Venus": { + "sign": "Aquarius", + "sign_cn": "水瓶座" + }, + "ascendant": "Gemini" + } + } + }, + "double_transit_pac": { + "cl": { + "jupiter": { + "CL_Saturn(宫主)": [ + { + "desc": "9宫相位", + "offset": 9, + "type": "Aspect" + } + ] + }, + "saturn": { + "CL_10宫(Capricorn)": [ + { + "desc": "3宫相位", + "offset": 3, + "type": "Aspect" + } + ], + "CL_Saturn(宫主)": [ + { + "desc": "10宫相位", + "offset": 10, + "type": "Aspect" + } + ] + } + }, + "d1": { + "jupiter": { + "10宫(Aries)": [ + { + "desc": "5宫相位", + "offset": 5, + "type": "Aspect" + } + ], + "Moon(LL)": [ + { + "desc": "5宫相位", + "offset": 5, + "type": "Aspect" + } + ], + "Saturn(对宫主)": [ + { + "desc": "9宫相位", + "offset": 9, + "type": "Aspect" + } + ] + }, + "saturn": { + "Mars(宫主)": [ + { + "desc": "7宫相位", + "offset": 7, + "type": "Aspect" + } + ], + "Saturn(对宫主)": [ + { + "desc": "10宫相位", + "offset": 10, + "type": "Aspect" + } + ] + } + }, + "d9": { + "jupiter": {}, + "saturn": { + "D9_Mercury(宫主)": [ + { + "desc": "7宫相位", + "offset": 7, + "type": "Aspect" + } + ], + "Mars_D9(Taurus)": [ + { + "desc": "7宫相位", + "offset": 7, + "type": "Aspect" + } + ], + "Moon_D9(Libra)": [ + { + "desc": "同宫(2宫)", + "type": "Position" + }, + { + "desc": "合相(6.73°)", + "type": "Conjunction" + } + ] + } + }, + "double_transit": [ + { + "jupiter_pac": [ + { + "desc": "9宫相位", + "offset": 9, + "type": "Aspect" + } + ], + "layer": "D1", + "saturn_pac": [ + { + "desc": "10宫相位", + "offset": 10, + "type": "Aspect" + } + ], + "strength": "strong", + "target": "Saturn(对宫主)" + }, + { + "jupiter_pac": [ + { + "desc": "9宫相位", + "offset": 9, + "type": "Aspect" + } + ], + "layer": "CL", + "saturn_pac": [ + { + "desc": "10宫相位", + "offset": 10, + "type": "Aspect" + } + ], + "strength": "strong", + "target": "CL_Saturn(宫主)" + } + ], + "event_house": 10, + "stats": { + "chandra_lagna": "Aries", + "cl_jupiter_targets": [ + "CL_Saturn(宫主)" + ], + "cl_overlap": [ + "CL_Saturn(宫主)" + ], + "cl_saturn_targets": [ + "CL_10宫(Capricorn)", + "CL_Saturn(宫主)" + ], + "d1_jupiter_targets": [ + "10宫(Aries)", + "Moon(LL)", + "Saturn(对宫主)" + ], + "d1_overlap": [ + "Saturn(对宫主)" + ], + "d1_saturn_targets": [ + "Mars(宫主)", + "Saturn(对宫主)" + ], + "d9_ascendant": "Virgo", + "d9_jupiter_targets": [], + "d9_overlap": [], + "d9_saturn_targets": [ + "D9_Mercury(宫主)", + "Mars_D9(Taurus)", + "Moon_D9(Libra)" + ], + "event_lord_d9_sign": "Taurus" + }, + "summary": "✅ Double Transit PAC 确认: D1+CL 多层激活10宫主题", + "transit_date": "1983-04-11" + }, + "event_source": { + "source_grade": "primary", + "url": "https://www.oscars.org/oscars/ceremonies/1983" + }, + "functional_benefic_malefic": { + "ascendant": "Cancer", + "effect_on_confidence": "高严谨模式下必须叠加功能性宫主吉凶与自然吉凶;若功能属性与自然属性冲突,应降低置信度或显式标记冲突。", + "functional_benefics": [ + "Jupiter", + "Mars", + "Moon" + ], + "functional_malefics": [ + "Mercury", + "Saturn", + "Venus" + ], + "functional_neutrals": [ + "Sun" + ], + "owned_houses": { + "Jupiter": [ + 6, + 9 + ], + "Mars": [ + 5, + 10 + ], + "Mercury": [ + 3, + 12 + ], + "Moon": [ + 1 + ], + "Saturn": [ + 7, + 8 + ], + "Sun": [ + 2 + ], + "Venus": [ + 4, + 11 + ] + }, + "source": "strict_functional_benefic_malefic_v1", + "status": "used", + "yogakarakas": [ + "Mars" + ] + }, + "narayana": { + "ad": { + "end_age": 34.1795, + "lord": "Sun", + "sign": "Leo", + "sign_idx": 4, + "start_age": 33.1538, + "years": 1.0256 + }, + "md": { + "end_age": 35.0, + "lord": "Venus", + "sign": "Libra", + "sign_idx": 6, + "start_age": 27.0, + "years": 8 + }, + "pd": { + "end_age": 33.8901, + "lord": "Jupiter", + "sign": "Pisces", + "sign_idx": 11, + "start_age": 33.7586, + "years": 0.1315 + }, + "remaining_years": 1.2 + }, + "vimshottari": { + "antardasha": "Rahu", + "mahadasha": "Rahu" + } + }, + "expected_label": "career_status", + "matched_expected_label": false, + "name": "Meryl Streep", + "outcome": "Won Best Actress for Sophie's Choice", + "result_class": "weak_hit", + "rule_version": "v2_1", + "score": 5, + "signals": [ + "Rahu_occupies_event_house:9", + "Rahu_dispositor_Jupiter_owns_event_house", + "narayana_lord_owns_event_house:Venus", + "double_transit_pac_strong" + ] + }, + { + "actual_label": "domain_activation", + "birth_time_rating": "AA", + "blocked": false, + "case_id": "aniston_emmy_2002", + "domain": "career", + "event_date": "2002-09-22", + "evidence": { + "arudha_lord": "Jupiter", + "ascendant": { + "degree": 1.2377, + "degree_in_sign": 1.2377, + "degree_in_sign_raw": 181.2377135283859, + "degree_raw": 181.2377, + "lon": 181.2377, + "lord": "Venus", + "sign": "Libra", + "sign_cn": "天秤座" + }, + "ashtakavarga_audit": { + "all_bav_valid": true, + "boundary": "Audit evidence only. SAV/BAV does not change V2.1 event scores until a fresh holdout validates it.", + "event_house_sav": { + "10": { + "level": "极吉", + "sav_score": 32, + "sign": "Cancer" + }, + "11": { + "level": "极吉", + "sav_score": 33, + "sign": "Leo" + }, + "6": { + "level": "中等", + "sav_score": 25, + "sign": "Pisces" + }, + "9": { + "level": "吉利", + "sav_score": 28, + "sign": "Gemini" + } + }, + "method": "Ashtakavarga八分法(BPHS/PVR书例校准v2.1)", + "sav_total": 337, + "sav_valid": true, + "scoring_effect": 0, + "settings": { + "ayanamsa": 23.8952, + "node_mode": "mean" + }, + "status": "used_non_scoring", + "transit_support": { + "Jupiter": { + "bav": 6, + "sav": 32, + "sign": "Cancer" + }, + "Saturn": { + "bav": 1, + "sav": 28, + "sign": "Gemini" + } + }, + "version": "2.1" + }, + "birth_source": { + "evidence_basis": "quoted BC/BR", + "source_grade": "primary", + "time_accuracy_rating": "AA", + "url": "https://www.astro.com/adbvip/adbvip_02_11.htm" + }, + "domain_varga": { + "birth_info": "1969-02-11 22:22", + "divisional_charts": { + "D10_Dasamsa": { + "Jupiter": { + "sign": "Leo", + "sign_cn": "狮子座" + }, + "Ketu": { + "sign": "Cancer", + "sign_cn": "巨蟹座" + }, + "Mars": { + "sign": "Cancer", + "sign_cn": "巨蟹座" + }, + "Mercury": { + "sign": "Scorpio", + "sign_cn": "天蝎座" + }, + "Moon": { + "sign": "Aries", + "sign_cn": "白羊座" + }, + "Rahu": { + "sign": "Capricorn", + "sign_cn": "摩羯座" + }, + "Saturn": { + "sign": "Leo", + "sign_cn": "狮子座" + }, + "Sun": { + "sign": "Gemini", + "sign_cn": "双子座" + }, + "Venus": { + "sign": "Aries", + "sign_cn": "白羊座" + }, + "ascendant": "Libra" + } + } + }, + "double_transit_pac": { + "cl": { + "jupiter": {}, + "saturn": { + "CL_Sun(宫主)": [ + { + "desc": "7宫相位", + "offset": 7, + "type": "Aspect" + } + ] + } + }, + "d1": { + "jupiter": { + "10宫(Cancer)": [ + { + "desc": "同宫(10宫)", + "type": "Position" + }, + { + "desc": "合相(1.81°)", + "type": "Conjunction" + } + ] + }, + "saturn": {} + }, + "d9": { + "jupiter": {}, + "saturn": {} + }, + "double_transit": [], + "event_house": 10, + "stats": { + "chandra_lagna": "Scorpio", + "cl_jupiter_targets": [], + "cl_overlap": [], + "cl_saturn_targets": [ + "CL_Sun(宫主)" + ], + "d1_jupiter_targets": [ + "10宫(Cancer)" + ], + "d1_overlap": [], + "d1_saturn_targets": [], + "d9_ascendant": "Libra", + "d9_jupiter_targets": [], + "d9_overlap": [], + "d9_saturn_targets": [], + "event_lord_d9_sign": "Pisces" + }, + "summary": "❌ 无 Double Transit PAC 激活", + "transit_date": "2002-09-22" + }, + "event_source": { + "source_grade": "primary", + "url": "https://www.televisionacademy.com/awards/nominees-winners/2002/outstanding-lead-actress-in-a-comedy-series" + }, + "functional_benefic_malefic": { + "ascendant": "Libra", + "effect_on_confidence": "高严谨模式下必须叠加功能性宫主吉凶与自然吉凶;若功能属性与自然属性冲突,应降低置信度或显式标记冲突。", + "functional_benefics": [ + "Mercury", + "Saturn", + "Venus" + ], + "functional_malefics": [ + "Jupiter", + "Sun" + ], + "functional_neutrals": [ + "Mars", + "Moon" + ], + "owned_houses": { + "Jupiter": [ + 3, + 6 + ], + "Mars": [ + 2, + 7 + ], + "Mercury": [ + 9, + 12 + ], + "Moon": [ + 10 + ], + "Saturn": [ + 4, + 5 + ], + "Sun": [ + 11 + ], + "Venus": [ + 1, + 8 + ] + }, + "source": "strict_functional_benefic_malefic_v1", + "status": "used", + "yogakarakas": [ + "Saturn", + "Venus" + ] + }, + "narayana": { + "ad": { + "end_age": 34.0, + "lord": "Mars", + "sign": "Scorpio", + "sign_idx": 7, + "start_age": 33.0, + "years": 1.0 + }, + "md": { + "end_age": 35.0, + "lord": "Jupiter", + "sign": "Pisces", + "sign_idx": 11, + "start_age": 29.0, + "years": 6 + }, + "pd": { + "end_age": 33.6528, + "lord": "Venus", + "sign": "Taurus", + "sign_idx": 1, + "start_age": 33.5139, + "years": 0.1389 + }, + "remaining_years": 1.39 + }, + "vimshottari": { + "antardasha": "Moon", + "mahadasha": "Moon" + } + }, + "expected_label": "career_status", + "matched_expected_label": false, + "name": "Jennifer Aniston", + "outcome": "Won the Primetime Emmy for lead actress in a comedy series", + "result_class": "weak_hit", + "rule_version": "v2_1", + "score": 5, + "signals": [ + "Moon_owns_event_houses:[10]", + "active_dasha_matches_D10_10L:Moon", + "active_dasha_matches_Amatyakaraka:Moon", + "narayana_lord_owns_event_house:Jupiter" + ] + }, + { + "actual_label": "domain_activation", + "birth_time_rating": "AA", + "blocked": false, + "case_id": "william_marriage_2011", + "domain": "marriage", + "event_date": "2011-04-29", + "evidence": { + "arudha_lord": "Moon", + "ascendant": { + "degree": 3.824, + "degree_in_sign": 3.824, + "degree_in_sign_raw": 243.8240154811706, + "degree_raw": 243.824, + "lon": 243.824, + "lord": "Jupiter", + "sign": "Sagittarius", + "sign_cn": "射手座" + }, + "ashtakavarga_audit": { + "all_bav_valid": true, + "boundary": "Audit evidence only. SAV/BAV does not change V2.1 event scores until a fresh holdout validates it.", + "event_house_sav": { + "11": { + "level": "极吉", + "sav_score": 30, + "sign": "Libra" + }, + "2": { + "level": "中等", + "sav_score": 27, + "sign": "Capricorn" + }, + "5": { + "level": "极吉", + "sav_score": 32, + "sign": "Aries" + }, + "7": { + "level": "挑战", + "sav_score": 22, + "sign": "Gemini" + } + }, + "method": "Ashtakavarga八分法(BPHS/PVR书例校准v2.1)", + "sav_total": 337, + "sav_valid": true, + "scoring_effect": 0, + "settings": { + "ayanamsa": 24.0153, + "node_mode": "mean" + }, + "status": "used_non_scoring", + "transit_support": { + "Jupiter": { + "bav": 5, + "sav": 37, + "sign": "Pisces" + }, + "Saturn": { + "bav": 3, + "sav": 31, + "sign": "Virgo" + } + }, + "version": "2.1" + }, + "birth_source": { + "evidence_basis": "BC/BR in hand", + "source_grade": "primary", + "time_accuracy_rating": "AA", + "url": "https://www.astro.com/adbvip/adbvip_06_21.htm" + }, + "domain_varga": { + "birth_info": "1982-06-21 21:03", + "divisional_charts": { + "D9_Navamsa": { + "Jupiter": { + "sign": "Sagittarius", + "sign_cn": "射手座" + }, + "Ketu": { + "sign": "Libra", + "sign_cn": "天秤座" + }, + "Mars": { + "sign": "Taurus", + "sign_cn": "金牛座" + }, + "Mercury": { + "sign": "Taurus", + "sign_cn": "金牛座" + }, + "Moon": { + "sign": "Capricorn", + "sign_cn": "摩羯座" + }, + "Rahu": { + "sign": "Aries", + "sign_cn": "白羊座" + }, + "Saturn": { + "sign": "Cancer", + "sign_cn": "巨蟹座" + }, + "Sun": { + "sign": "Scorpio", + "sign_cn": "天蝎座" + }, + "Venus": { + "sign": "Capricorn", + "sign_cn": "摩羯座" + }, + "ascendant": "Taurus" + } + } + }, + "double_transit_pac": { + "cl": { + "jupiter": { + "CL_7宫(Sagittarius)": [ + { + "desc": "9宫相位", + "offset": 9, + "type": "Aspect" + } + ], + "CL_Jupiter(宫主)": [ + { + "desc": "7宫相位", + "offset": 7, + "type": "Aspect" + } + ] + }, + "saturn": { + "CL_7宫(Sagittarius)": [ + { + "desc": "3宫相位", + "offset": 3, + "type": "Aspect" + } + ] + } + }, + "d1": { + "jupiter": { + "Jupiter(LL)": [ + { + "desc": "7宫相位", + "offset": 7, + "type": "Aspect" + } + ] + }, + "saturn": {} + }, + "d9": { + "jupiter": { + "Jupiter_D9(Sagittarius)": [ + { + "desc": "9宫相位", + "offset": 9, + "type": "Aspect" + } + ] + }, + "saturn": { + "D9_Mars(宫主)": [ + { + "desc": "同宫(5宫)", + "type": "Position" + }, + { + "desc": "合相(2.40°)", + "type": "Conjunction" + } + ], + "Jupiter_D9(Sagittarius)": [ + { + "desc": "3宫相位", + "offset": 3, + "type": "Aspect" + } + ] + } + }, + "double_transit": [ + { + "jupiter_pac": [ + { + "desc": "9宫相位", + "offset": 9, + "type": "Aspect" + } + ], + "layer": "D9", + "saturn_pac": [ + { + "desc": "3宫相位", + "offset": 3, + "type": "Aspect" + } + ], + "strength": "strong", + "target": "Jupiter_D9(Sagittarius)" + }, + { + "jupiter_pac": [ + { + "desc": "9宫相位", + "offset": 9, + "type": "Aspect" + } + ], + "layer": "CL", + "saturn_pac": [ + { + "desc": "3宫相位", + "offset": 3, + "type": "Aspect" + } + ], + "strength": "strong", + "target": "CL_7宫(Sagittarius)" + } + ], + "event_house": 7, + "stats": { + "chandra_lagna": "Gemini", + "cl_jupiter_targets": [ + "CL_7宫(Sagittarius)", + "CL_Jupiter(宫主)" + ], + "cl_overlap": [ + "CL_7宫(Sagittarius)" + ], + "cl_saturn_targets": [ + "CL_7宫(Sagittarius)" + ], + "d1_jupiter_targets": [ + "Jupiter(LL)" + ], + "d1_overlap": [], + "d1_saturn_targets": [], + "d9_ascendant": "Taurus", + "d9_jupiter_targets": [ + "Jupiter_D9(Sagittarius)" + ], + "d9_overlap": [ + "Jupiter_D9(Sagittarius)" + ], + "d9_saturn_targets": [ + "D9_Mars(宫主)", + "Jupiter_D9(Sagittarius)" + ], + "event_lord_d9_sign": "Taurus" + }, + "summary": "✅ Double Transit PAC 确认: D9+CL 多层激活7宫主题", + "transit_date": "2011-04-29" + }, + "event_source": { + "source_grade": "primary", + "url": "https://www.royal.uk/wedding-prince-william-and-miss-catherine-middleton" + }, + "functional_benefic_malefic": { + "ascendant": "Sagittarius", + "effect_on_confidence": "高严谨模式下必须叠加功能性宫主吉凶与自然吉凶;若功能属性与自然属性冲突,应降低置信度或显式标记冲突。", + "functional_benefics": [ + "Jupiter", + "Mars", + "Sun" + ], + "functional_malefics": [ + "Saturn", + "Venus" + ], + "functional_neutrals": [ + "Mercury", + "Moon" + ], + "owned_houses": { + "Jupiter": [ + 1, + 4 + ], + "Mars": [ + 5, + 12 + ], + "Mercury": [ + 7, + 10 + ], + "Moon": [ + 8 + ], + "Saturn": [ + 2, + 3 + ], + "Sun": [ + 9 + ], + "Venus": [ + 6, + 11 + ] + }, + "source": "strict_functional_benefic_malefic_v1", + "status": "used", + "yogakarakas": [ + "Jupiter" + ] + }, + "narayana": { + "ad": { + "end_age": 29.2264, + "lord": "Mercury", + "sign": "Virgo", + "sign_idx": 5, + "start_age": 28.6981, + "years": 0.5283 + }, + "md": { + "end_age": 32.0, + "lord": "Jupiter", + "sign": "Pisces", + "sign_idx": 11, + "start_age": 25.0, + "years": 7 + }, + "pd": { + "end_age": 28.8725, + "lord": "Jupiter", + "sign": "Sagittarius", + "sign_idx": 8, + "start_age": 28.8227, + "years": 0.0498 + }, + "remaining_years": 3.15 + }, + "vimshottari": { + "antardasha": "Saturn", + "mahadasha": "Saturn" + } + }, + "expected_label": "legal_marriage", + "matched_expected_label": false, + "name": "William, Prince of Wales", + "outcome": "Married Catherine Middleton", + "result_class": "weak_hit", + "rule_version": "v2_1", + "score": 4, + "signals": [ + "Saturn_owns_event_houses:[2]", + "double_transit_pac_strong" + ] + }, + { + "actual_label": "domain_activation", + "birth_time_rating": "AA", + "blocked": false, + "case_id": "jolie_marriage_2014", + "domain": "marriage", + "event_date": "2014-08-23", + "evidence": { + "arudha_lord": "Mars", + "ascendant": { + "degree": 5.3578, + "degree_in_sign": 5.3578, + "degree_in_sign_raw": 95.35778170382213, + "degree_raw": 95.3578, + "lon": 95.3578, + "lord": "Moon", + "sign": "Cancer", + "sign_cn": "巨蟹座" + }, + "ashtakavarga_audit": { + "all_bav_valid": true, + "boundary": "Audit evidence only. SAV/BAV does not change V2.1 event scores until a fresh holdout validates it.", + "event_house_sav": { + "11": { + "level": "吉利", + "sav_score": 29, + "sign": "Taurus" + }, + "2": { + "level": "中等", + "sav_score": 27, + "sign": "Leo" + }, + "5": { + "level": "挑战", + "sav_score": 22, + "sign": "Scorpio" + }, + "7": { + "level": "极吉", + "sav_score": 36, + "sign": "Capricorn" + } + }, + "method": "Ashtakavarga八分法(BPHS/PVR书例校准v2.1)", + "sav_total": 337, + "sav_valid": true, + "scoring_effect": 0, + "settings": { + "ayanamsa": 24.0616, + "node_mode": "mean" + }, + "status": "used_non_scoring", + "transit_support": { + "Jupiter": { + "bav": 3, + "sav": 24, + "sign": "Cancer" + }, + "Saturn": { + "bav": 3, + "sav": 31, + "sign": "Libra" + } + }, + "version": "2.1" + }, + "birth_source": { + "evidence_basis": "quoted BC/BR", + "source_grade": "primary", + "time_accuracy_rating": "AA", + "url": "https://www.astro.com/adbvip/adbvip_06_04.htm" + }, + "domain_varga": { + "birth_info": "1975-06-04 09:09", + "divisional_charts": { + "D9_Navamsa": { + "Jupiter": { + "sign": "Aquarius", + "sign_cn": "水瓶座" + }, + "Ketu": { + "sign": "Pisces", + "sign_cn": "双鱼座" + }, + "Mars": { + "sign": "Sagittarius", + "sign_cn": "射手座" + }, + "Mercury": { + "sign": "Virgo", + "sign_cn": "处女座" + }, + "Moon": { + "sign": "Sagittarius", + "sign_cn": "射手座" + }, + "Rahu": { + "sign": "Virgo", + "sign_cn": "处女座" + }, + "Saturn": { + "sign": "Taurus", + "sign_cn": "金牛座" + }, + "Sun": { + "sign": "Gemini", + "sign_cn": "双子座" + }, + "Venus": { + "sign": "Leo", + "sign_cn": "狮子座" + }, + "ascendant": "Leo" + } + } + }, + "double_transit_pac": { + "cl": { + "jupiter": {}, + "saturn": { + "CL_Mercury(宫主)": [ + { + "desc": "7宫相位", + "offset": 7, + "type": "Aspect" + } + ] + } + }, + "d1": { + "jupiter": {}, + "saturn": { + "7宫(Capricorn)": [ + { + "desc": "3宫相位", + "offset": 3, + "type": "Aspect" + } + ] + } + }, + "d9": { + "jupiter": { + "Moon_D9(Sagittarius)": [ + { + "desc": "5宫相位", + "offset": 5, + "type": "Aspect" + } + ] + }, + "saturn": { + "D9_7宫(Aquarius)": [ + { + "desc": "10宫相位", + "offset": 10, + "type": "Aspect" + } + ], + "Saturn_D9(Taurus)": [ + { + "desc": "7宫相位", + "offset": 7, + "type": "Aspect" + } + ] + } + }, + "double_transit": [], + "event_house": 7, + "stats": { + "chandra_lagna": "Pisces", + "cl_jupiter_targets": [], + "cl_overlap": [], + "cl_saturn_targets": [ + "CL_Mercury(宫主)" + ], + "d1_jupiter_targets": [], + "d1_overlap": [], + "d1_saturn_targets": [ + "7宫(Capricorn)" + ], + "d9_ascendant": "Leo", + "d9_jupiter_targets": [ + "Moon_D9(Sagittarius)" + ], + "d9_overlap": [], + "d9_saturn_targets": [ + "D9_7宫(Aquarius)", + "Saturn_D9(Taurus)" + ], + "event_lord_d9_sign": "Taurus" + }, + "summary": "❌ 无 Double Transit PAC 激活", + "transit_date": "2014-08-23" + }, + "event_source": { + "source_grade": "verified_secondary", + "url": "https://en.wikipedia.org/wiki/Angelina_Jolie" + }, + "functional_benefic_malefic": { + "ascendant": "Cancer", + "effect_on_confidence": "高严谨模式下必须叠加功能性宫主吉凶与自然吉凶;若功能属性与自然属性冲突,应降低置信度或显式标记冲突。", + "functional_benefics": [ + "Jupiter", + "Mars", + "Moon" + ], + "functional_malefics": [ + "Mercury", + "Saturn", + "Venus" + ], + "functional_neutrals": [ + "Sun" + ], + "owned_houses": { + "Jupiter": [ + 6, + 9 + ], + "Mars": [ + 5, + 10 + ], + "Mercury": [ + 3, + 12 + ], + "Moon": [ + 1 + ], + "Saturn": [ + 7, + 8 + ], + "Sun": [ + 2 + ], + "Venus": [ + 4, + 11 + ] + }, + "source": "strict_functional_benefic_malefic_v1", + "status": "used", + "yogakarakas": [ + "Mars" + ] + }, + "narayana": { + "ad": { + "end_age": 39.2209, + "lord": "Mars", + "sign": "Aries", + "sign_idx": 0, + "start_age": 38.8372, + "years": 0.3837 + }, + "md": { + "end_age": 41.0, + "lord": "Jupiter", + "sign": "Sagittarius", + "sign_idx": 8, + "start_age": 38.0, + "years": 3 + }, + "pd": { + "end_age": 39.2209, + "lord": "Jupiter", + "sign": "Pisces", + "sign_idx": 11, + "start_age": 39.1674, + "years": 0.0535 + }, + "remaining_years": 1.78 + }, + "vimshottari": { + "antardasha": "Ketu", + "mahadasha": "Venus" + } + }, + "expected_label": "legal_marriage", + "matched_expected_label": false, + "name": "Angelina Jolie", + "outcome": "Married Brad Pitt", + "result_class": "weak_hit", + "rule_version": "v2_1", + "score": 6, + "signals": [ + "Venus_owns_event_houses:[11]", + "Venus_domain_karaka", + "Ketu_occupies_event_house:11", + "Ketu_dispositor_Venus_owns_event_house", + "active_dasha_matches_Darakaraka:Venus" + ] + }, + { + "actual_label": "legal_marriage", + "birth_time_rating": "AA", + "blocked": false, + "case_id": "kahlo_marriage_1929", + "domain": "marriage", + "event_date": "1929-08-21", + "evidence": { + "arudha_lord": "Jupiter", + "ascendant": { + "degree": 0.9523, + "degree_in_sign": 0.9523, + "degree_in_sign_raw": 120.95226624616197, + "degree_raw": 120.9523, + "lon": 120.9523, + "lord": "Sun", + "sign": "Leo", + "sign_cn": "狮子座" + }, + "ashtakavarga_audit": { + "all_bav_valid": true, + "boundary": "Audit evidence only. SAV/BAV does not change V2.1 event scores until a fresh holdout validates it.", + "event_house_sav": { + "11": { + "level": "吉利", + "sav_score": 28, + "sign": "Gemini" + }, + "2": { + "level": "中等", + "sav_score": 27, + "sign": "Virgo" + }, + "5": { + "level": "中等", + "sav_score": 26, + "sign": "Sagittarius" + }, + "7": { + "level": "挑战", + "sav_score": 19, + "sign": "Aquarius" + } + }, + "method": "Ashtakavarga八分法(BPHS/PVR书例校准v2.1)", + "sav_total": 337, + "sav_valid": true, + "scoring_effect": 0, + "settings": { + "ayanamsa": 22.8744, + "node_mode": "mean" + }, + "status": "used_non_scoring", + "transit_support": { + "Jupiter": { + "bav": 3, + "sav": 30, + "sign": "Taurus" + }, + "Saturn": { + "bav": 2, + "sav": 26, + "sign": "Sagittarius" + } + }, + "version": "2.1" + }, + "birth_source": { + "evidence_basis": "BC/BR in hand", + "source_grade": "primary", + "time_accuracy_rating": "AA", + "url": "https://www.astro.com/adbvip/adbvip_07_06.htm" + }, + "domain_varga": { + "birth_info": "1907-07-06 08:30", + "divisional_charts": { + "D9_Navamsa": { + "Jupiter": { + "sign": "Gemini", + "sign_cn": "双子座" + }, + "Ketu": { + "sign": "Capricorn", + "sign_cn": "摩羯座" + }, + "Mars": { + "sign": "Libra", + "sign_cn": "天秤座" + }, + "Mercury": { + "sign": "Scorpio", + "sign_cn": "天蝎座" + }, + "Moon": { + "sign": "Pisces", + "sign_cn": "双鱼座" + }, + "Rahu": { + "sign": "Cancer", + "sign_cn": "巨蟹座" + }, + "Saturn": { + "sign": "Leo", + "sign_cn": "狮子座" + }, + "Sun": { + "sign": "Aries", + "sign_cn": "白羊座" + }, + "Venus": { + "sign": "Libra", + "sign_cn": "天秤座" + }, + "ascendant": "Aries" + } + } + }, + "double_transit_pac": { + "cl": { + "jupiter": { + "CL_Mars(宫主)": [ + { + "desc": "7宫相位", + "offset": 7, + "type": "Aspect" + } + ] + }, + "saturn": { + "CL_Mars(宫主)": [ + { + "desc": "同宫(8宫)", + "type": "Position" + } + ] + } + }, + "d1": { + "jupiter": { + "7宫(Aquarius)": [ + { + "desc": "9宫相位", + "offset": 9, + "type": "Aspect" + } + ] + }, + "saturn": { + "Saturn(宫主)": [ + { + "desc": "3宫相位", + "offset": 3, + "type": "Aspect" + } + ], + "Saturn(对宫主)": [ + { + "desc": "3宫相位", + "offset": 3, + "type": "Aspect" + } + ] + } + }, + "d9": { + "jupiter": {}, + "saturn": {} + }, + "double_transit": [ + { + "jupiter_pac": [ + { + "desc": "7宫相位", + "offset": 7, + "type": "Aspect" + } + ], + "layer": "CL", + "saturn_pac": [ + { + "desc": "同宫(8宫)", + "type": "Position" + } + ], + "strength": "strong", + "target": "CL_Mars(宫主)" + } + ], + "event_house": 7, + "stats": { + "chandra_lagna": "Taurus", + "cl_jupiter_targets": [ + "CL_Mars(宫主)" + ], + "cl_overlap": [ + "CL_Mars(宫主)" + ], + "cl_saturn_targets": [ + "CL_Mars(宫主)" + ], + "d1_jupiter_targets": [ + "7宫(Aquarius)" + ], + "d1_overlap": [], + "d1_saturn_targets": [ + "Saturn(宫主)", + "Saturn(对宫主)" + ], + "d9_ascendant": "Aries", + "d9_jupiter_targets": [], + "d9_overlap": [], + "d9_saturn_targets": [], + "event_lord_d9_sign": "Leo" + }, + "summary": "⚠️ Chandra Lagna 层 Double Transit 激活,D1/D9 未确认", + "transit_date": "1929-08-21" + }, + "event_source": { + "source_grade": "verified_secondary", + "url": "https://en.wikipedia.org/wiki/Frida_Kahlo" + }, + "functional_benefic_malefic": { + "ascendant": "Leo", + "effect_on_confidence": "高严谨模式下必须叠加功能性宫主吉凶与自然吉凶;若功能属性与自然属性冲突,应降低置信度或显式标记冲突。", + "functional_benefics": [ + "Jupiter", + "Mars", + "Sun" + ], + "functional_malefics": [ + "Mercury", + "Moon", + "Saturn", + "Venus" + ], + "functional_neutrals": [], + "owned_houses": { + "Jupiter": [ + 5, + 8 + ], + "Mars": [ + 4, + 9 + ], + "Mercury": [ + 2, + 11 + ], + "Moon": [ + 12 + ], + "Saturn": [ + 6, + 7 + ], + "Sun": [ + 1 + ], + "Venus": [ + 3, + 10 + ] + }, + "source": "strict_functional_benefic_malefic_v1", + "status": "used", + "yogakarakas": [ + "Mars" + ] + }, + "narayana": { + "ad": { + "end_age": 22.2295, + "lord": "Saturn", + "sign": "Capricorn", + "sign_idx": 9, + "start_age": 21.9672, + "years": 0.2623 + }, + "md": { + "end_age": 28.0, + "lord": "Venus", + "sign": "Libra", + "sign_idx": 6, + "start_age": 20.0, + "years": 8 + }, + "pd": { + "end_age": 22.165, + "lord": "Mercury", + "sign": "Virgo", + "sign_idx": 5, + "start_age": 22.122, + "years": 0.043 + }, + "remaining_years": 5.87 + }, + "vimshottari": { + "antardasha": "Jupiter", + "mahadasha": "Rahu" + } + }, + "expected_label": "legal_marriage", + "matched_expected_label": true, + "name": "Frida Kahlo", + "outcome": "Married Diego Rivera in a civil ceremony", + "result_class": "strong_hit", + "rule_version": "v2_1", + "score": 7, + "signals": [ + "Jupiter_owns_event_houses:[5]", + "Jupiter_occupies_event_house:11", + "Jupiter_domain_karaka", + "active_dasha_matches_UL_lord:Jupiter", + "double_transit_pac_strong" + ] + }, + { + "actual_label": "legal_marriage", + "birth_time_rating": "AA", + "blocked": false, + "case_id": "snoop_marriage_1997", + "domain": "marriage", + "event_date": "1997-06-14", + "evidence": { + "arudha_lord": "Moon", + "ascendant": { + "degree": 7.3126, + "degree_in_sign": 7.3126, + "degree_in_sign_raw": 7.312583875536397, + "degree_raw": 7.3126, + "lon": 7.3126, + "lord": "Mars", + "sign": "Aries", + "sign_cn": "白羊座" + }, + "ashtakavarga_audit": { + "all_bav_valid": true, + "boundary": "Audit evidence only. SAV/BAV does not change V2.1 event scores until a fresh holdout validates it.", + "event_house_sav": { + "11": { + "level": "极吉", + "sav_score": 32, + "sign": "Aquarius" + }, + "2": { + "level": "挑战", + "sav_score": 22, + "sign": "Taurus" + }, + "5": { + "level": "极吉", + "sav_score": 41, + "sign": "Leo" + }, + "7": { + "level": "挑战", + "sav_score": 23, + "sign": "Libra" + } + }, + "method": "Ashtakavarga八分法(BPHS/PVR书例校准v2.1)", + "sav_total": 337, + "sav_valid": true, + "scoring_effect": 0, + "settings": { + "ayanamsa": 23.8215, + "node_mode": "mean" + }, + "status": "used_non_scoring", + "transit_support": { + "Jupiter": { + "bav": 5, + "sav": 25, + "sign": "Capricorn" + }, + "Saturn": { + "bav": 6, + "sav": 31, + "sign": "Pisces" + } + }, + "version": "2.1" + }, + "birth_source": { + "evidence_basis": "BC/BR in hand", + "source_grade": "primary", + "time_accuracy_rating": "AA", + "url": "https://www.astro.com/adbvip/adbvip_10_20.htm" + }, + "domain_varga": { + "birth_info": "1971-10-20 18:20", + "divisional_charts": { + "D9_Navamsa": { + "Jupiter": { + "sign": "Libra", + "sign_cn": "天秤座" + }, + "Ketu": { + "sign": "Sagittarius", + "sign_cn": "射手座" + }, + "Mars": { + "sign": "Virgo", + "sign_cn": "处女座" + }, + "Mercury": { + "sign": "Capricorn", + "sign_cn": "摩羯座" + }, + "Moon": { + "sign": "Aries", + "sign_cn": "白羊座" + }, + "Rahu": { + "sign": "Gemini", + "sign_cn": "双子座" + }, + "Saturn": { + "sign": "Aries", + "sign_cn": "白羊座" + }, + "Sun": { + "sign": "Scorpio", + "sign_cn": "天蝎座" + }, + "Venus": { + "sign": "Pisces", + "sign_cn": "双鱼座" + }, + "ascendant": "Gemini" + } + } + }, + "double_transit_pac": { + "cl": { + "jupiter": { + "CL_Mars(宫主)": [ + { + "desc": "同宫(4宫)", + "type": "Position" + }, + { + "desc": "合相(0.48°)", + "type": "Conjunction" + } + ] + }, + "saturn": { + "CL_Mars(宫主)": [ + { + "desc": "10宫相位", + "offset": 10, + "type": "Aspect" + } + ] + } + }, + "d1": { + "jupiter": { + "7宫(Libra)": [ + { + "desc": "9宫相位", + "offset": 9, + "type": "Aspect" + } + ], + "Mars(LL)": [ + { + "desc": "同宫(10宫)", + "type": "Position" + }, + { + "desc": "合相(0.48°)", + "type": "Conjunction" + } + ], + "Venus(宫主)": [ + { + "desc": "9宫相位", + "offset": 9, + "type": "Aspect" + } + ], + "Venus(对宫主)": [ + { + "desc": "9宫相位", + "offset": 9, + "type": "Aspect" + } + ] + }, + "saturn": { + "7宫(Libra)": [ + { + "desc": "7宫相位", + "offset": 7, + "type": "Aspect" + } + ], + "Mars(LL)": [ + { + "desc": "10宫相位", + "offset": 10, + "type": "Aspect" + } + ], + "Venus(宫主)": [ + { + "desc": "7宫相位", + "offset": 7, + "type": "Aspect" + } + ], + "Venus(对宫主)": [ + { + "desc": "7宫相位", + "offset": 7, + "type": "Aspect" + } + ] + } + }, + "d9": { + "jupiter": { + "D9_7宫(Sagittarius)": [ + { + "desc": "5宫相位", + "offset": 5, + "type": "Aspect" + } + ] + }, + "saturn": { + "D9_7宫(Sagittarius)": [ + { + "desc": "3宫相位", + "offset": 3, + "type": "Aspect" + } + ], + "Venus_D9(Pisces)": [ + { + "desc": "同宫(10宫)", + "type": "Position" + }, + { + "desc": "合相(9.65°)", + "type": "Conjunction" + } + ] + } + }, + "double_transit": [ + { + "jupiter_pac": [ + { + "desc": "9宫相位", + "offset": 9, + "type": "Aspect" + } + ], + "layer": "D1", + "saturn_pac": [ + { + "desc": "7宫相位", + "offset": 7, + "type": "Aspect" + } + ], + "strength": "strong", + "target": "Venus(对宫主)" + }, + { + "jupiter_pac": [ + { + "desc": "9宫相位", + "offset": 9, + "type": "Aspect" + } + ], + "layer": "D1", + "saturn_pac": [ + { + "desc": "7宫相位", + "offset": 7, + "type": "Aspect" + } + ], + "strength": "strong", + "target": "Venus(宫主)" + }, + { + "jupiter_pac": [ + { + "desc": "9宫相位", + "offset": 9, + "type": "Aspect" + } + ], + "layer": "D1", + "saturn_pac": [ + { + "desc": "7宫相位", + "offset": 7, + "type": "Aspect" + } + ], + "strength": "strong", + "target": "7宫(Libra)" + }, + { + "jupiter_pac": [ + { + "desc": "同宫(10宫)", + "type": "Position" + }, + { + "desc": "合相(0.48°)", + "type": "Conjunction" + } + ], + "layer": "D1", + "saturn_pac": [ + { + "desc": "10宫相位", + "offset": 10, + "type": "Aspect" + } + ], + "strength": "strong", + "target": "Mars(LL)" + }, + { + "jupiter_pac": [ + { + "desc": "5宫相位", + "offset": 5, + "type": "Aspect" + } + ], + "layer": "D9", + "saturn_pac": [ + { + "desc": "3宫相位", + "offset": 3, + "type": "Aspect" + } + ], + "strength": "strong", + "target": "D9_7宫(Sagittarius)" + }, + { + "jupiter_pac": [ + { + "desc": "同宫(4宫)", + "type": "Position" + }, + { + "desc": "合相(0.48°)", + "type": "Conjunction" + } + ], + "layer": "CL", + "saturn_pac": [ + { + "desc": "10宫相位", + "offset": 10, + "type": "Aspect" + } + ], + "strength": "strong", + "target": "CL_Mars(宫主)" + }, + { + "jupiter_pac": [ + { + "desc": "9宫相位", + "offset": 9, + "type": "Aspect" + } + ], + "layer": "D1+D9", + "saturn_pac": [ + { + "desc": "同宫(10宫)", + "type": "Position" + }, + { + "desc": "合相(9.65°)", + "type": "Conjunction" + } + ], + "strength": "moderate", + "target": "Jupiter(D1)Venus(宫主) + Saturn(D9)Venus_D9(Pisces)" + }, + { + "jupiter_pac": [ + { + "desc": "9宫相位", + "offset": 9, + "type": "Aspect" + } + ], + "layer": "D1+D9", + "saturn_pac": [ + { + "desc": "同宫(10宫)", + "type": "Position" + }, + { + "desc": "合相(9.65°)", + "type": "Conjunction" + } + ], + "strength": "moderate", + "target": "Jupiter(D1)Venus(对宫主) + Saturn(D9)Venus_D9(Pisces)" + } + ], + "event_house": 7, + "stats": { + "chandra_lagna": "Libra", + "cl_jupiter_targets": [ + "CL_Mars(宫主)" + ], + "cl_overlap": [ + "CL_Mars(宫主)" + ], + "cl_saturn_targets": [ + "CL_Mars(宫主)" + ], + "d1_jupiter_targets": [ + "7宫(Libra)", + "Mars(LL)", + "Venus(宫主)", + "Venus(对宫主)" + ], + "d1_overlap": [ + "7宫(Libra)", + "Mars(LL)", + "Venus(宫主)", + "Venus(对宫主)" + ], + "d1_saturn_targets": [ + "7宫(Libra)", + "Mars(LL)", + "Venus(宫主)", + "Venus(对宫主)" + ], + "d9_ascendant": "Gemini", + "d9_jupiter_targets": [ + "D9_7宫(Sagittarius)" + ], + "d9_overlap": [ + "D9_7宫(Sagittarius)" + ], + "d9_saturn_targets": [ + "D9_7宫(Sagittarius)", + "Venus_D9(Pisces)" + ], + "event_lord_d9_sign": "Pisces" + }, + "summary": "✅ Double Transit PAC 确认: D1+D9+CL 多层激活7宫主题", + "transit_date": "1997-06-14" + }, + "event_source": { + "source_grade": "verified_secondary", + "url": "https://en.wikipedia.org/wiki/Snoop_Dogg" + }, + "functional_benefic_malefic": { + "ascendant": "Aries", + "effect_on_confidence": "高严谨模式下必须叠加功能性宫主吉凶与自然吉凶;若功能属性与自然属性冲突,应降低置信度或显式标记冲突。", + "functional_benefics": [ + "Jupiter", + "Mars", + "Sun" + ], + "functional_malefics": [ + "Mercury", + "Saturn" + ], + "functional_neutrals": [ + "Moon", + "Venus" + ], + "owned_houses": { + "Jupiter": [ + 9, + 12 + ], + "Mars": [ + 1, + 8 + ], + "Mercury": [ + 3, + 6 + ], + "Moon": [ + 4 + ], + "Saturn": [ + 10, + 11 + ], + "Sun": [ + 5 + ], + "Venus": [ + 2, + 7 + ] + }, + "source": "strict_functional_benefic_malefic_v1", + "status": "used", + "yogakarakas": [ + "Mars" + ] + }, + "narayana": { + "ad": { + "end_age": 26.25, + "lord": "Venus", + "sign": "Libra", + "sign_idx": 6, + "start_age": 24.0, + "years": 2.25 + }, + "md": { + "end_age": 36.0, + "lord": "Venus", + "sign": "Libra", + "sign_idx": 6, + "start_age": 24.0, + "years": 12 + }, + "pd": { + "end_age": 25.7227, + "lord": "Mars", + "sign": "Aries", + "sign_idx": 0, + "start_age": 25.4062, + "years": 0.3164 + }, + "remaining_years": 10.35 + }, + "vimshottari": { + "antardasha": "Moon", + "mahadasha": "Saturn" + } + }, + "expected_label": "legal_marriage", + "matched_expected_label": true, + "name": "Snoop Dogg", + "outcome": "Married Shante Taylor", + "result_class": "strong_hit", + "rule_version": "v2_1", + "score": 10, + "signals": [ + "Saturn_owns_event_houses:[11]", + "Saturn_occupies_event_house:2", + "Moon_occupies_event_house:7", + "active_dasha_matches_UL_lord:Moon", + "narayana_activates_primary_event_sign:Libra", + "narayana_lord_owns_event_house:Venus", + "double_transit_pac_strong" + ] + }, + { + "actual_label": "legal_marriage", + "birth_time_rating": "A", + "blocked": false, + "case_id": "disney_marriage_1925", + "domain": "marriage", + "event_date": "1925-07-13", + "evidence": { + "arudha_lord": "Venus", + "ascendant": { + "degree": 3.1696, + "degree_in_sign": 3.1696, + "degree_in_sign_raw": 153.16955776302456, + "degree_raw": 153.1696, + "lon": 153.1696, + "lord": "Mercury", + "sign": "Virgo", + "sign_cn": "处女座" + }, + "ashtakavarga_audit": { + "all_bav_valid": true, + "boundary": "Audit evidence only. SAV/BAV does not change V2.1 event scores until a fresh holdout validates it.", + "event_house_sav": { + "11": { + "level": "极吉", + "sav_score": 36, + "sign": "Cancer" + }, + "2": { + "level": "极吉", + "sav_score": 34, + "sign": "Libra" + }, + "5": { + "level": "挑战", + "sav_score": 23, + "sign": "Capricorn" + }, + "7": { + "level": "中等", + "sav_score": 25, + "sign": "Pisces" + } + }, + "method": "Ashtakavarga八分法(BPHS/PVR书例校准v2.1)", + "sav_total": 337, + "sav_valid": true, + "scoring_effect": 0, + "settings": { + "ayanamsa": 22.817, + "node_mode": "mean" + }, + "status": "used_non_scoring", + "transit_support": { + "Jupiter": { + "bav": 5, + "sav": 23, + "sign": "Sagittarius" + }, + "Saturn": { + "bav": 4, + "sav": 34, + "sign": "Libra" + } + }, + "version": "2.1" + }, + "birth_source": { + "evidence_basis": "from memory", + "source_grade": "verified_secondary", + "time_accuracy_rating": "A", + "url": "https://www.astro.com/adbvip/adbvip_12_05.htm" + }, + "domain_varga": { + "birth_info": "1901-12-05 00:35", + "divisional_charts": { + "D9_Navamsa": { + "Jupiter": { + "sign": "Libra", + "sign_cn": "天秤座" + }, + "Ketu": { + "sign": "Virgo", + "sign_cn": "处女座" + }, + "Mars": { + "sign": "Leo", + "sign_cn": "狮子座" + }, + "Mercury": { + "sign": "Leo", + "sign_cn": "狮子座" + }, + "Moon": { + "sign": "Gemini", + "sign_cn": "双子座" + }, + "Rahu": { + "sign": "Pisces", + "sign_cn": "双鱼座" + }, + "Saturn": { + "sign": "Libra", + "sign_cn": "天秤座" + }, + "Sun": { + "sign": "Sagittarius", + "sign_cn": "射手座" + }, + "Venus": { + "sign": "Pisces", + "sign_cn": "双鱼座" + }, + "ascendant": "Capricorn" + } + } + }, + "double_transit_pac": { + "cl": { + "jupiter": { + "CL_Jupiter(宫主)": [ + { + "desc": "同宫(4宫)", + "type": "Position" + }, + { + "desc": "合相(1.48°)", + "type": "Conjunction" + } + ] + }, + "saturn": {} + }, + "d1": { + "jupiter": { + "Jupiter(宫主)": [ + { + "desc": "同宫(4宫)", + "type": "Position" + }, + { + "desc": "合相(1.48°)", + "type": "Conjunction" + } + ], + "Jupiter(对宫主)": [ + { + "desc": "同宫(4宫)", + "type": "Position" + }, + { + "desc": "合相(1.48°)", + "type": "Conjunction" + } + ] + }, + "saturn": {} + }, + "d9": { + "jupiter": { + "D9_Moon(宫主)": [ + { + "desc": "9宫相位", + "offset": 9, + "type": "Aspect" + } + ] + }, + "saturn": { + "D9_7宫(Cancer)": [ + { + "desc": "3宫相位", + "offset": 3, + "type": "Aspect" + } + ], + "Jupiter_D9(Libra)": [ + { + "desc": "同宫(10宫)", + "type": "Position" + }, + { + "desc": "合相(0.19°)", + "type": "Conjunction" + } + ], + "Mercury_D9(Leo)": [ + { + "desc": "10宫相位", + "offset": 10, + "type": "Aspect" + } + ] + } + }, + "double_transit": [ + { + "jupiter_pac": [ + { + "desc": "同宫(4宫)", + "type": "Position" + }, + { + "desc": "合相(1.48°)", + "type": "Conjunction" + } + ], + "layer": "D1+D9", + "saturn_pac": [ + { + "desc": "同宫(10宫)", + "type": "Position" + }, + { + "desc": "合相(0.19°)", + "type": "Conjunction" + } + ], + "strength": "moderate", + "target": "Jupiter(D1)Jupiter(对宫主) + Saturn(D9)Jupiter_D9(Libra)" + }, + { + "jupiter_pac": [ + { + "desc": "同宫(4宫)", + "type": "Position" + }, + { + "desc": "合相(1.48°)", + "type": "Conjunction" + } + ], + "layer": "D1+D9", + "saturn_pac": [ + { + "desc": "同宫(10宫)", + "type": "Position" + }, + { + "desc": "合相(0.19°)", + "type": "Conjunction" + } + ], + "strength": "moderate", + "target": "Jupiter(D1)Jupiter(宫主) + Saturn(D9)Jupiter_D9(Libra)" + } + ], + "event_house": 7, + "stats": { + "chandra_lagna": "Virgo", + "cl_jupiter_targets": [ + "CL_Jupiter(宫主)" + ], + "cl_overlap": [], + "cl_saturn_targets": [], + "d1_jupiter_targets": [ + "Jupiter(宫主)", + "Jupiter(对宫主)" + ], + "d1_overlap": [], + "d1_saturn_targets": [], + "d9_ascendant": "Capricorn", + "d9_jupiter_targets": [ + "D9_Moon(宫主)" + ], + "d9_overlap": [], + "d9_saturn_targets": [ + "D9_7宫(Cancer)", + "Jupiter_D9(Libra)", + "Mercury_D9(Leo)" + ], + "event_lord_d9_sign": "Libra" + }, + "summary": "⚠️ 跨层间接 Double Transit (D1+D9),需结合 Dasha 确认", + "transit_date": "1925-07-13" + }, + "event_source": { + "source_grade": "primary", + "url": "https://www.waltdisney.org/blog/who-did-walt-disney-marry" + }, + "functional_benefic_malefic": { + "ascendant": "Virgo", + "effect_on_confidence": "高严谨模式下必须叠加功能性宫主吉凶与自然吉凶;若功能属性与自然属性冲突,应降低置信度或显式标记冲突。", + "functional_benefics": [ + "Mercury", + "Saturn", + "Venus" + ], + "functional_malefics": [ + "Mars", + "Moon", + "Sun" + ], + "functional_neutrals": [ + "Jupiter" + ], + "owned_houses": { + "Jupiter": [ + 4, + 7 + ], + "Mars": [ + 3, + 8 + ], + "Mercury": [ + 1, + 10 + ], + "Moon": [ + 11 + ], + "Saturn": [ + 5, + 6 + ], + "Sun": [ + 12 + ], + "Venus": [ + 2, + 9 + ] + }, + "source": "strict_functional_benefic_malefic_v1", + "status": "used", + "yogakarakas": [ + "Mercury" + ] + }, + "narayana": { + "ad": { + "end_age": 23.6486, + "lord": "Mars", + "sign": "Aries", + "sign_idx": 0, + "start_age": 22.4595, + "years": 1.1892 + }, + "md": { + "end_age": 29.0, + "lord": "Saturn", + "sign": "Capricorn", + "sign_idx": 9, + "start_age": 18.0, + "years": 11 + }, + "pd": { + "end_age": 23.6486, + "lord": "Jupiter", + "sign": "Pisces", + "sign_idx": 11, + "start_age": 23.5041, + "years": 0.1445 + }, + "remaining_years": 5.4 + }, + "vimshottari": { + "antardasha": "Venus", + "mahadasha": "Rahu" + } + }, + "expected_label": "legal_marriage", + "matched_expected_label": true, + "name": "Walt Disney", + "outcome": "Married Lillian Bounds", + "result_class": "strong_hit", + "rule_version": "v2_1", + "score": 10, + "signals": [ + "Rahu_occupies_event_house:2", + "Venus_owns_event_houses:[2]", + "Venus_occupies_event_house:5", + "Venus_domain_karaka", + "Rahu_dispositor_Venus_owns_event_house", + "Rahu_dispositor_Venus_occupies_event_house:5", + "active_dasha_matches_UL_lord:Venus", + "narayana_lord_owns_event_house:Saturn", + "double_transit_pac_present" + ] + }, + { + "actual_label": "domain_activation", + "birth_time_rating": "AA", + "blocked": false, + "case_id": "albert_ii_accession_2005", + "domain": "career", + "event_date": "2005-04-06", + "evidence": { + "arudha_lord": "Venus", + "ascendant": { + "degree": 28.0929, + "degree_in_sign": 28.0929, + "degree_in_sign_raw": 58.0929006900906, + "degree_raw": 58.0929, + "lon": 58.0929, + "lord": "Venus", + "sign": "Taurus", + "sign_cn": "金牛座" + }, + "ashtakavarga_audit": { + "all_bav_valid": true, + "boundary": "Audit evidence only. SAV/BAV does not change V2.1 event scores until a fresh holdout validates it.", + "event_house_sav": { + "10": { + "level": "极吉", + "sav_score": 31, + "sign": "Aquarius" + }, + "11": { + "level": "吉利", + "sav_score": 29, + "sign": "Pisces" + }, + "6": { + "level": "极吉", + "sav_score": 37, + "sign": "Libra" + }, + "9": { + "level": "吉利", + "sav_score": 29, + "sign": "Capricorn" + } + }, + "method": "Ashtakavarga八分法(BPHS/PVR书例校准v2.1)", + "sav_total": 337, + "sav_valid": true, + "scoring_effect": 0, + "settings": { + "ayanamsa": 23.9306, + "node_mode": "mean" + }, + "status": "used_non_scoring", + "transit_support": { + "Jupiter": { + "bav": 3, + "sav": 24, + "sign": "Virgo" + }, + "Saturn": { + "bav": 3, + "sav": 22, + "sign": "Gemini" + } + }, + "version": "2.1" + }, + "birth_source": { + "evidence_basis": "quoted BC/BR", + "source_grade": "primary", + "time_accuracy_rating": "AA", + "url": "https://www.astro.com/adbvip/adbvip_03_14.htm" + }, + "domain_varga": { + "birth_info": "1958-03-14 10:50", + "divisional_charts": { + "D10_Dasamsa": { + "Jupiter": { + "sign": "Sagittarius", + "sign_cn": "射手座" + }, + "Ketu": { + "sign": "Cancer", + "sign_cn": "巨蟹座" + }, + "Mars": { + "sign": "Libra", + "sign_cn": "天秤座" + }, + "Mercury": { + "sign": "Aquarius", + "sign_cn": "水瓶座" + }, + "Moon": { + "sign": "Cancer", + "sign_cn": "巨蟹座" + }, + "Rahu": { + "sign": "Capricorn", + "sign_cn": "摩羯座" + }, + "Saturn": { + "sign": "Sagittarius", + "sign_cn": "射手座" + }, + "Sun": { + "sign": "Scorpio", + "sign_cn": "天蝎座" + }, + "Venus": { + "sign": "Aquarius", + "sign_cn": "水瓶座" + }, + "ascendant": "Libra" + } + } + }, + "double_transit_pac": { + "cl": { + "jupiter": { + "CL_10宫(Virgo)": [ + { + "desc": "同宫(10宫)", + "type": "Position" + }, + { + "desc": "合相(4.68°)", + "type": "Conjunction" + } + ] + }, + "saturn": { + "CL_10宫(Virgo)": [ + { + "desc": "3宫相位", + "offset": 3, + "type": "Aspect" + } + ] + } + }, + "d1": { + "jupiter": { + "10宫(Aquarius)": [ + { + "desc": "5宫相位", + "offset": 5, + "type": "Aspect" + } + ] + }, + "saturn": { + "Mars(对宫主)": [ + { + "desc": "7宫相位", + "offset": 7, + "type": "Aspect" + } + ], + "Venus(LL)": [ + { + "desc": "7宫相位", + "offset": 7, + "type": "Aspect" + } + ] + } + }, + "d9": { + "jupiter": { + "D9_10宫(Gemini)": [ + { + "desc": "同宫(1宫)", + "type": "Position" + }, + { + "desc": "合相(4.68°)", + "type": "Conjunction" + } + ], + "Saturn_D9(Aries)": [ + { + "desc": "7宫相位", + "offset": 7, + "type": "Aspect" + } + ], + "Venus_D9(Gemini)": [ + { + "desc": "9宫相位", + "offset": 9, + "type": "Aspect" + } + ] + }, + "saturn": { + "D9_10宫(Gemini)": [ + { + "desc": "3宫相位", + "offset": 3, + "type": "Aspect" + } + ], + "Saturn_D9(Aries)": [ + { + "desc": "10宫相位", + "offset": 10, + "type": "Aspect" + } + ], + "Venus_D9(Gemini)": [ + { + "desc": "同宫(10宫)", + "type": "Position" + } + ] + } + }, + "double_transit": [ + { + "jupiter_pac": [ + { + "desc": "7宫相位", + "offset": 7, + "type": "Aspect" + } + ], + "layer": "D9", + "saturn_pac": [ + { + "desc": "10宫相位", + "offset": 10, + "type": "Aspect" + } + ], + "strength": "strong", + "target": "Saturn_D9(Aries)" + }, + { + "jupiter_pac": [ + { + "desc": "9宫相位", + "offset": 9, + "type": "Aspect" + } + ], + "layer": "D9", + "saturn_pac": [ + { + "desc": "同宫(10宫)", + "type": "Position" + } + ], + "strength": "strong", + "target": "Venus_D9(Gemini)" + }, + { + "jupiter_pac": [ + { + "desc": "同宫(1宫)", + "type": "Position" + }, + { + "desc": "合相(4.68°)", + "type": "Conjunction" + } + ], + "layer": "D9", + "saturn_pac": [ + { + "desc": "3宫相位", + "offset": 3, + "type": "Aspect" + } + ], + "strength": "strong", + "target": "D9_10宫(Gemini)" + }, + { + "jupiter_pac": [ + { + "desc": "同宫(10宫)", + "type": "Position" + }, + { + "desc": "合相(4.68°)", + "type": "Conjunction" + } + ], + "layer": "CL", + "saturn_pac": [ + { + "desc": "3宫相位", + "offset": 3, + "type": "Aspect" + } + ], + "strength": "strong", + "target": "CL_10宫(Virgo)" + } + ], + "event_house": 10, + "stats": { + "chandra_lagna": "Sagittarius", + "cl_jupiter_targets": [ + "CL_10宫(Virgo)" + ], + "cl_overlap": [ + "CL_10宫(Virgo)" + ], + "cl_saturn_targets": [ + "CL_10宫(Virgo)" + ], + "d1_jupiter_targets": [ + "10宫(Aquarius)" + ], + "d1_overlap": [], + "d1_saturn_targets": [ + "Mars(对宫主)", + "Venus(LL)" + ], + "d9_ascendant": "Virgo", + "d9_jupiter_targets": [ + "D9_10宫(Gemini)", + "Saturn_D9(Aries)", + "Venus_D9(Gemini)" + ], + "d9_overlap": [ + "D9_10宫(Gemini)", + "Saturn_D9(Aries)", + "Venus_D9(Gemini)" + ], + "d9_saturn_targets": [ + "D9_10宫(Gemini)", + "Saturn_D9(Aries)", + "Venus_D9(Gemini)" + ], + "event_lord_d9_sign": "Aries" + }, + "summary": "✅ Double Transit PAC 确认: D9+CL 多层激活10宫主题", + "transit_date": "2005-04-06" + }, + "event_source": { + "source_grade": "primary", + "url": "https://www.palais.mc/en/princely-family/h-s-h-prince-albert-ii/biography-1-9.html" + }, + "functional_benefic_malefic": { + "ascendant": "Taurus", + "effect_on_confidence": "高严谨模式下必须叠加功能性宫主吉凶与自然吉凶;若功能属性与自然属性冲突,应降低置信度或显式标记冲突。", + "functional_benefics": [ + "Mercury", + "Saturn", + "Venus" + ], + "functional_malefics": [ + "Jupiter", + "Mars", + "Moon" + ], + "functional_neutrals": [ + "Sun" + ], + "owned_houses": { + "Jupiter": [ + 8, + 11 + ], + "Mars": [ + 7, + 12 + ], + "Mercury": [ + 2, + 5 + ], + "Moon": [ + 3 + ], + "Saturn": [ + 9, + 10 + ], + "Sun": [ + 4 + ], + "Venus": [ + 1, + 6 + ] + }, + "source": "strict_functional_benefic_malefic_v1", + "status": "used", + "yogakarakas": [ + "Saturn", + "Venus" + ] + }, + "narayana": { + "ad": { + "end_age": 47.3563, + "lord": "Mercury", + "sign": "Gemini", + "sign_idx": 2, + "start_age": 46.3218, + "years": 1.0345 + }, + "md": { + "end_age": 50.0, + "lord": "Jupiter", + "sign": "Sagittarius", + "sign_idx": 8, + "start_age": 40.0, + "years": 10 + }, + "pd": { + "end_age": 47.0709, + "lord": "Saturn", + "sign": "Aquarius", + "sign_idx": 10, + "start_age": 46.952, + "years": 0.1189 + }, + "remaining_years": 2.94 + }, + "vimshottari": { + "antardasha": "Jupiter", + "mahadasha": "Jupiter" + } + }, + "expected_label": "career_status", + "matched_expected_label": false, + "name": "Albert II, Prince of Monaco", + "outcome": "Began reign as Prince of Monaco", + "result_class": "weak_hit", + "rule_version": "v2_1", + "score": 6, + "signals": [ + "Jupiter_owns_event_houses:[11]", + "Jupiter_occupies_event_house:6", + "narayana_lord_owns_event_house:Jupiter", + "double_transit_pac_strong" + ] + }, + { + "actual_label": "domain_activation", + "birth_time_rating": "AA", + "blocked": false, + "case_id": "boy_george_grammy_1984", + "domain": "career", + "event_date": "1984-02-28", + "evidence": { + "arudha_lord": "Venus", + "ascendant": { + "degree": 19.5865, + "degree_in_sign": 19.5865, + "degree_in_sign_raw": 19.58652184346338, + "degree_raw": 19.5865, + "lon": 19.5865, + "lord": "Mars", + "sign": "Aries", + "sign_cn": "白羊座" + }, + "ashtakavarga_audit": { + "all_bav_valid": true, + "boundary": "Audit evidence only. SAV/BAV does not change V2.1 event scores until a fresh holdout validates it.", + "event_house_sav": { + "10": { + "level": "中等", + "sav_score": 25, + "sign": "Capricorn" + }, + "11": { + "level": "极吉", + "sav_score": 34, + "sign": "Aquarius" + }, + "6": { + "level": "中等", + "sav_score": 25, + "sign": "Virgo" + }, + "9": { + "level": "挑战", + "sav_score": 21, + "sign": "Sagittarius" + } + }, + "method": "Ashtakavarga八分法(BPHS/PVR书例校准v2.1)", + "sav_total": 337, + "sav_valid": true, + "scoring_effect": 0, + "settings": { + "ayanamsa": 23.6358, + "node_mode": "mean" + }, + "status": "used_non_scoring", + "transit_support": { + "Jupiter": { + "bav": 5, + "sav": 21, + "sign": "Sagittarius" + }, + "Saturn": { + "bav": 0, + "sav": 27, + "sign": "Libra" + } + }, + "version": "2.1" + }, + "birth_source": { + "evidence_basis": "quoted BC/BR", + "source_grade": "primary", + "time_accuracy_rating": "AA", + "url": "https://www.astro.com/adbvip/adbvip_06_14.htm" + }, + "domain_varga": { + "birth_info": "1961-06-14 02:50", + "divisional_charts": { + "D10_Dasamsa": { + "Jupiter": { + "sign": "Capricorn", + "sign_cn": "摩羯座" + }, + "Ketu": { + "sign": "Aries", + "sign_cn": "白羊座" + }, + "Mars": { + "sign": "Sagittarius", + "sign_cn": "射手座" + }, + "Mercury": { + "sign": "Scorpio", + "sign_cn": "天蝎座" + }, + "Moon": { + "sign": "Virgo", + "sign_cn": "处女座" + }, + "Rahu": { + "sign": "Libra", + "sign_cn": "天秤座" + }, + "Saturn": { + "sign": "Libra", + "sign_cn": "天秤座" + }, + "Sun": { + "sign": "Libra", + "sign_cn": "天秤座" + }, + "Venus": { + "sign": "Leo", + "sign_cn": "狮子座" + }, + "ascendant": "Libra" + } + } + }, + "double_transit_pac": { + "cl": { + "jupiter": {}, + "saturn": { + "CL_Jupiter(宫主)": [ + { + "desc": "3宫相位", + "offset": 3, + "type": "Aspect" + } + ] + } + }, + "d1": { + "jupiter": { + "Mars(LL)": [ + { + "desc": "7宫相位", + "offset": 7, + "type": "Aspect" + } + ] + }, + "saturn": { + "10宫(Capricorn)": [ + { + "desc": "3宫相位", + "offset": 3, + "type": "Aspect" + } + ], + "Saturn(宫主)": [ + { + "desc": "3宫相位", + "offset": 3, + "type": "Aspect" + } + ] + } + }, + "d9": { + "jupiter": { + "D9_10宫(Gemini)": [ + { + "desc": "9宫相位", + "offset": 9, + "type": "Aspect" + } + ] + }, + "saturn": {} + }, + "double_transit": [], + "event_house": 10, + "stats": { + "chandra_lagna": "Gemini", + "cl_jupiter_targets": [], + "cl_overlap": [], + "cl_saturn_targets": [ + "CL_Jupiter(宫主)" + ], + "d1_jupiter_targets": [ + "Mars(LL)" + ], + "d1_overlap": [], + "d1_saturn_targets": [ + "10宫(Capricorn)", + "Saturn(宫主)" + ], + "d9_ascendant": "Virgo", + "d9_jupiter_targets": [ + "D9_10宫(Gemini)" + ], + "d9_overlap": [], + "d9_saturn_targets": [], + "event_lord_d9_sign": "Aquarius" + }, + "summary": "❌ 无 Double Transit PAC 激活", + "transit_date": "1984-02-28" + }, + "event_source": { + "source_grade": "verified_secondary", + "url": "https://en.wikipedia.org/wiki/26th_Annual_Grammy_Awards" + }, + "functional_benefic_malefic": { + "ascendant": "Aries", + "effect_on_confidence": "高严谨模式下必须叠加功能性宫主吉凶与自然吉凶;若功能属性与自然属性冲突,应降低置信度或显式标记冲突。", + "functional_benefics": [ + "Jupiter", + "Mars", + "Sun" + ], + "functional_malefics": [ + "Mercury", + "Saturn" + ], + "functional_neutrals": [ + "Moon", + "Venus" + ], + "owned_houses": { + "Jupiter": [ + 9, + 12 + ], + "Mars": [ + 1, + 8 + ], + "Mercury": [ + 3, + 6 + ], + "Moon": [ + 4 + ], + "Saturn": [ + 10, + 11 + ], + "Sun": [ + 5 + ], + "Venus": [ + 2, + 7 + ] + }, + "source": "strict_functional_benefic_malefic_v1", + "status": "used", + "yogakarakas": [ + "Mars" + ] + }, + "narayana": { + "ad": { + "end_age": 23.2039, + "lord": "Saturn", + "sign": "Aquarius", + "sign_idx": 10, + "start_age": 21.9223, + "years": 1.2816 + }, + "md": { + "end_age": 26.0, + "lord": "Mercury", + "sign": "Gemini", + "sign_idx": 2, + "start_age": 14.0, + "years": 12 + }, + "pd": { + "end_age": 22.756, + "lord": "Sun", + "sign": "Leo", + "sign_idx": 4, + "start_age": 22.644, + "years": 0.112 + }, + "remaining_years": 3.29 + }, + "vimshottari": { + "antardasha": "Venus", + "mahadasha": "Jupiter" + } + }, + "expected_label": "career_status", + "matched_expected_label": false, + "name": "Boy George", + "outcome": "Culture Club won the Grammy for Best New Artist", + "result_class": "weak_hit", + "rule_version": "v2_1", + "score": 6, + "signals": [ + "Jupiter_owns_event_houses:[9]", + "Jupiter_occupies_event_house:10", + "active_dasha_matches_D10_Lagna_lord:Venus", + "active_dasha_matches_A10_lord:Venus", + "narayana_lord_owns_event_house:Mercury" + ] + }, + { + "actual_label": "career_status", + "birth_time_rating": "AA", + "blocked": false, + "case_id": "bergman_oscar_1945", + "domain": "career", + "event_date": "1945-03-15", + "evidence": { + "arudha_lord": "Saturn", + "ascendant": { + "degree": 0.9464, + "degree_in_sign": 0.9464, + "degree_in_sign_raw": 120.94642922543406, + "degree_raw": 120.9464, + "lon": 120.9464, + "lord": "Sun", + "sign": "Leo", + "sign_cn": "狮子座" + }, + "ashtakavarga_audit": { + "all_bav_valid": true, + "boundary": "Audit evidence only. SAV/BAV does not change V2.1 event scores until a fresh holdout validates it.", + "event_house_sav": { + "10": { + "level": "中等", + "sav_score": 26, + "sign": "Taurus" + }, + "11": { + "level": "极吉", + "sav_score": 41, + "sign": "Gemini" + }, + "6": { + "level": "极吉", + "sav_score": 37, + "sign": "Capricorn" + }, + "9": { + "level": "极吉", + "sav_score": 31, + "sign": "Aries" + } + }, + "method": "Ashtakavarga八分法(BPHS/PVR书例校准v2.1)", + "sav_total": 337, + "sav_valid": true, + "scoring_effect": 0, + "settings": { + "ayanamsa": 23.0917, + "node_mode": "mean" + }, + "status": "used_non_scoring", + "transit_support": { + "Jupiter": { + "bav": 5, + "sav": 26, + "sign": "Leo" + }, + "Saturn": { + "bav": 5, + "sav": 41, + "sign": "Gemini" + } + }, + "version": "2.1" + }, + "birth_source": { + "evidence_basis": "BC/BR in hand", + "source_grade": "primary", + "time_accuracy_rating": "AA", + "url": "https://www.astro.com/adbvip/adbvip_08_29.htm" + }, + "domain_varga": { + "birth_info": "1915-08-29 03:30", + "divisional_charts": { + "D10_Dasamsa": { + "Jupiter": { + "sign": "Sagittarius", + "sign_cn": "射手座" + }, + "Ketu": { + "sign": "Libra", + "sign_cn": "天秤座" + }, + "Mars": { + "sign": "Libra", + "sign_cn": "天秤座" + }, + "Mercury": { + "sign": "Aries", + "sign_cn": "白羊座" + }, + "Moon": { + "sign": "Aries", + "sign_cn": "白羊座" + }, + "Rahu": { + "sign": "Aries", + "sign_cn": "白羊座" + }, + "Saturn": { + "sign": "Sagittarius", + "sign_cn": "射手座" + }, + "Sun": { + "sign": "Scorpio", + "sign_cn": "天蝎座" + }, + "Venus": { + "sign": "Libra", + "sign_cn": "天秤座" + }, + "ascendant": "Leo" + } + } + }, + "double_transit_pac": { + "cl": { + "jupiter": { + "CL_10宫(Capricorn)": [ + { + "desc": "5宫相位", + "offset": 5, + "type": "Aspect" + } + ] + }, + "saturn": { + "CL_10宫(Capricorn)": [ + { + "desc": "7宫相位", + "offset": 7, + "type": "Aspect" + } + ], + "CL_Saturn(宫主)": [ + { + "desc": "同宫(3宫)", + "type": "Position" + }, + { + "desc": "合相(9.73°)", + "type": "Conjunction" + } + ] + } + }, + "d1": { + "jupiter": { + "10宫(Taurus)": [ + { + "desc": "9宫相位", + "offset": 9, + "type": "Aspect" + } + ], + "Sun(LL)": [ + { + "desc": "同宫(1宫)", + "type": "Position" + } + ], + "Venus(宫主)": [ + { + "desc": "同宫(1宫)", + "type": "Position" + } + ] + }, + "saturn": { + "Saturn(对宫主)": [ + { + "desc": "同宫(11宫)", + "type": "Position" + }, + { + "desc": "合相(9.73°)", + "type": "Conjunction" + } + ] + } + }, + "d9": { + "jupiter": {}, + "saturn": { + "D9_10宫(Capricorn)": [ + { + "desc": "10宫相位", + "offset": 10, + "type": "Aspect" + } + ], + "D9_Saturn(宫主)": [ + { + "desc": "同宫(3宫)", + "type": "Position" + }, + { + "desc": "合相(9.73°)", + "type": "Conjunction" + } + ], + "Venus_D9(Gemini)": [ + { + "desc": "同宫(3宫)", + "type": "Position" + }, + { + "desc": "合相(4.18°)", + "type": "Conjunction" + } + ] + } + }, + "double_transit": [ + { + "jupiter_pac": [ + { + "desc": "5宫相位", + "offset": 5, + "type": "Aspect" + } + ], + "layer": "CL", + "saturn_pac": [ + { + "desc": "7宫相位", + "offset": 7, + "type": "Aspect" + } + ], + "strength": "strong", + "target": "CL_10宫(Capricorn)" + }, + { + "jupiter_pac": [ + { + "desc": "同宫(1宫)", + "type": "Position" + } + ], + "layer": "D1+D9", + "saturn_pac": [ + { + "desc": "同宫(3宫)", + "type": "Position" + }, + { + "desc": "合相(4.18°)", + "type": "Conjunction" + } + ], + "strength": "moderate", + "target": "Jupiter(D1)Venus(宫主) + Saturn(D9)Venus_D9(Gemini)" + } + ], + "event_house": 10, + "stats": { + "chandra_lagna": "Aries", + "cl_jupiter_targets": [ + "CL_10宫(Capricorn)" + ], + "cl_overlap": [ + "CL_10宫(Capricorn)" + ], + "cl_saturn_targets": [ + "CL_10宫(Capricorn)", + "CL_Saturn(宫主)" + ], + "d1_jupiter_targets": [ + "10宫(Taurus)", + "Sun(LL)", + "Venus(宫主)" + ], + "d1_overlap": [], + "d1_saturn_targets": [ + "Saturn(对宫主)" + ], + "d9_ascendant": "Aries", + "d9_jupiter_targets": [], + "d9_overlap": [], + "d9_saturn_targets": [ + "D9_10宫(Capricorn)", + "D9_Saturn(宫主)", + "Venus_D9(Gemini)" + ], + "event_lord_d9_sign": "Gemini" + }, + "summary": "⚠️ Chandra Lagna 层 Double Transit 激活,D1/D9 未确认", + "transit_date": "1945-03-15" + }, + "event_source": { + "source_grade": "primary", + "url": "https://www.oscars.org/oscars/ceremonies/1945" + }, + "functional_benefic_malefic": { + "ascendant": "Leo", + "effect_on_confidence": "高严谨模式下必须叠加功能性宫主吉凶与自然吉凶;若功能属性与自然属性冲突,应降低置信度或显式标记冲突。", + "functional_benefics": [ + "Jupiter", + "Mars", + "Sun" + ], + "functional_malefics": [ + "Mercury", + "Moon", + "Saturn", + "Venus" + ], + "functional_neutrals": [], + "owned_houses": { + "Jupiter": [ + 5, + 8 + ], + "Mars": [ + 4, + 9 + ], + "Mercury": [ + 2, + 11 + ], + "Moon": [ + 12 + ], + "Saturn": [ + 6, + 7 + ], + "Sun": [ + 1 + ], + "Venus": [ + 3, + 10 + ] + }, + "source": "strict_functional_benefic_malefic_v1", + "status": "used", + "yogakarakas": [ + "Mars" + ] + }, + "narayana": { + "ad": { + "end_age": 30.125, + "lord": "Moon", + "sign": "Cancer", + "sign_idx": 3, + "start_age": 29.0, + "years": 1.125 + }, + "md": { + "end_age": 33.0, + "lord": "Venus", + "sign": "Libra", + "sign_idx": 6, + "start_age": 23.0, + "years": 10 + }, + "pd": { + "end_age": 29.5906, + "lord": "Venus", + "sign": "Libra", + "sign_idx": 6, + "start_age": 29.45, + "years": 0.1406 + }, + "remaining_years": 3.46 + }, + "vimshottari": { + "antardasha": "Mercury", + "mahadasha": "Sun" + } + }, + "expected_label": "career_status", + "matched_expected_label": true, + "name": "Ingrid Bergman", + "outcome": "Won Best Actress for Gaslight", + "result_class": "strong_hit", + "rule_version": "v2_1", + "score": 8, + "signals": [ + "Sun_domain_karaka", + "Mercury_owns_event_houses:[11]", + "Mercury_domain_karaka", + "active_dasha_matches_D10_Lagna_lord:Sun", + "narayana_lord_owns_event_house:Venus", + "double_transit_pac_strong" + ] + }, + { + "actual_label": "domain_activation", + "birth_time_rating": "A", + "blocked": false, + "case_id": "morissette_grammy_1996", + "domain": "career", + "event_date": "1996-02-28", + "evidence": { + "arudha_lord": "Saturn", + "ascendant": { + "degree": 15.1562, + "degree_in_sign": 15.1562, + "degree_in_sign_raw": 105.15618971934725, + "degree_raw": 105.1562, + "lon": 105.1562, + "lord": "Moon", + "sign": "Cancer", + "sign_cn": "巨蟹座" + }, + "ashtakavarga_audit": { + "all_bav_valid": true, + "boundary": "Audit evidence only. SAV/BAV does not change V2.1 event scores until a fresh holdout validates it.", + "event_house_sav": { + "10": { + "level": "极吉", + "sav_score": 32, + "sign": "Aries" + }, + "11": { + "level": "吉利", + "sav_score": 28, + "sign": "Taurus" + }, + "6": { + "level": "极吉", + "sav_score": 34, + "sign": "Sagittarius" + }, + "9": { + "level": "极吉", + "sav_score": 31, + "sign": "Pisces" + } + }, + "method": "Ashtakavarga八分法(BPHS/PVR书例校准v2.1)", + "sav_total": 337, + "sav_valid": true, + "scoring_effect": 0, + "settings": { + "ayanamsa": 23.8034, + "node_mode": "mean" + }, + "status": "used_non_scoring", + "transit_support": { + "Jupiter": { + "bav": 4, + "sav": 34, + "sign": "Sagittarius" + }, + "Saturn": { + "bav": 4, + "sav": 31, + "sign": "Pisces" + } + }, + "version": "2.1" + }, + "birth_source": { + "evidence_basis": "from memory", + "source_grade": "verified_secondary", + "time_accuracy_rating": "A", + "url": "https://www.astro.com/adbvip/adbvip_06_01.htm" + }, + "domain_varga": { + "birth_info": "1974-06-01 09:51", + "divisional_charts": { + "D10_Dasamsa": { + "Jupiter": { + "sign": "Virgo", + "sign_cn": "处女座" + }, + "Ketu": { + "sign": "Virgo", + "sign_cn": "处女座" + }, + "Mars": { + "sign": "Pisces", + "sign_cn": "双鱼座" + }, + "Mercury": { + "sign": "Virgo", + "sign_cn": "处女座" + }, + "Moon": { + "sign": "Sagittarius", + "sign_cn": "射手座" + }, + "Rahu": { + "sign": "Pisces", + "sign_cn": "双鱼座" + }, + "Saturn": { + "sign": "Virgo", + "sign_cn": "处女座" + }, + "Sun": { + "sign": "Gemini", + "sign_cn": "双子座" + }, + "Venus": { + "sign": "Gemini", + "sign_cn": "双子座" + }, + "ascendant": "Leo" + } + } + }, + "double_transit_pac": { + "cl": { + "jupiter": { + "CL_10宫(Cancer)": [ + { + "desc": "7宫相位", + "offset": 7, + "type": "Aspect" + } + ] + }, + "saturn": { + "CL_Moon(宫主)": [ + { + "desc": "7宫相位", + "offset": 7, + "type": "Aspect" + } + ] + } + }, + "d1": { + "jupiter": { + "Mars(宫主)": [ + { + "desc": "7宫相位", + "offset": 7, + "type": "Aspect" + } + ] + }, + "saturn": { + "Moon(LL)": [ + { + "desc": "7宫相位", + "offset": 7, + "type": "Aspect" + } + ], + "Saturn(对宫主)": [ + { + "desc": "3宫相位", + "offset": 3, + "type": "Aspect" + } + ] + } + }, + "d9": { + "jupiter": { + "D9_Sun(宫主)": [ + { + "desc": "5宫相位", + "offset": 5, + "type": "Aspect" + } + ], + "Mars_D9(Cancer)": [ + { + "desc": "7宫相位", + "offset": 7, + "type": "Aspect" + } + ], + "Moon_D9(Sagittarius)": [ + { + "desc": "同宫(2宫)", + "type": "Position" + }, + { + "desc": "合相(2.71°)", + "type": "Conjunction" + } + ] + }, + "saturn": {} + }, + "double_transit": [], + "event_house": 10, + "stats": { + "chandra_lagna": "Libra", + "cl_jupiter_targets": [ + "CL_10宫(Cancer)" + ], + "cl_overlap": [], + "cl_saturn_targets": [ + "CL_Moon(宫主)" + ], + "d1_jupiter_targets": [ + "Mars(宫主)" + ], + "d1_overlap": [], + "d1_saturn_targets": [ + "Moon(LL)", + "Saturn(对宫主)" + ], + "d9_ascendant": "Scorpio", + "d9_jupiter_targets": [ + "D9_Sun(宫主)", + "Mars_D9(Cancer)", + "Moon_D9(Sagittarius)" + ], + "d9_overlap": [], + "d9_saturn_targets": [], + "event_lord_d9_sign": "Cancer" + }, + "summary": "❌ 无 Double Transit PAC 激活", + "transit_date": "1996-02-28" + }, + "event_source": { + "source_grade": "verified_secondary", + "url": "https://en.wikipedia.org/wiki/38th_Annual_Grammy_Awards" + }, + "functional_benefic_malefic": { + "ascendant": "Cancer", + "effect_on_confidence": "高严谨模式下必须叠加功能性宫主吉凶与自然吉凶;若功能属性与自然属性冲突,应降低置信度或显式标记冲突。", + "functional_benefics": [ + "Jupiter", + "Mars", + "Moon" + ], + "functional_malefics": [ + "Mercury", + "Saturn", + "Venus" + ], + "functional_neutrals": [ + "Sun" + ], + "owned_houses": { + "Jupiter": [ + 6, + 9 + ], + "Mars": [ + 5, + 10 + ], + "Mercury": [ + 3, + 12 + ], + "Moon": [ + 1 + ], + "Saturn": [ + 7, + 8 + ], + "Sun": [ + 2 + ], + "Venus": [ + 4, + 11 + ] + }, + "source": "strict_functional_benefic_malefic_v1", + "status": "used", + "yogakarakas": [ + "Mars" + ] + }, + "narayana": { + "ad": { + "end_age": 22.012, + "lord": "Mars", + "sign": "Scorpio", + "sign_idx": 7, + "start_age": 21.4337, + "years": 0.5783 + }, + "md": { + "end_age": 27.0, + "lord": "Venus", + "sign": "Libra", + "sign_idx": 6, + "start_age": 21.0, + "years": 6 + }, + "pd": { + "end_age": 21.8239, + "lord": "Mercury", + "sign": "Gemini", + "sign_idx": 2, + "start_age": 21.7403, + "years": 0.0836 + }, + "remaining_years": 5.26 + }, + "vimshottari": { + "antardasha": "Mercury", + "mahadasha": "Jupiter" + } + }, + "expected_label": "career_status", + "matched_expected_label": false, + "name": "Alanis Morissette", + "outcome": "Won four Grammy awards", + "result_class": "weak_hit", + "rule_version": "v2_1", + "score": 4, + "signals": [ + "Jupiter_owns_event_houses:[6, 9]", + "Mercury_domain_karaka", + "narayana_lord_owns_event_house:Venus" + ] + }, + { + "actual_label": "domain_activation", + "birth_time_rating": "A", + "blocked": false, + "case_id": "dion_eurovision_1988", + "domain": "career", + "event_date": "1988-04-30", + "evidence": { + "arudha_lord": "Saturn", + "ascendant": { + "degree": 9.1356, + "degree_in_sign": 9.1356, + "degree_in_sign_raw": 99.13557485171225, + "degree_raw": 99.1356, + "lon": 99.1356, + "lord": "Moon", + "sign": "Cancer", + "sign_cn": "巨蟹座" + }, + "ashtakavarga_audit": { + "all_bav_valid": true, + "boundary": "Audit evidence only. SAV/BAV does not change V2.1 event scores until a fresh holdout validates it.", + "event_house_sav": { + "10": { + "level": "中等", + "sav_score": 27, + "sign": "Aries" + }, + "11": { + "level": "极吉", + "sav_score": 31, + "sign": "Taurus" + }, + "6": { + "level": "极吉", + "sav_score": 37, + "sign": "Sagittarius" + }, + "9": { + "level": "挑战", + "sav_score": 19, + "sign": "Pisces" + } + }, + "method": "Ashtakavarga八分法(BPHS/PVR书例校准v2.1)", + "sav_total": 337, + "sav_valid": true, + "scoring_effect": 0, + "settings": { + "ayanamsa": 23.6941, + "node_mode": "mean" + }, + "status": "used_non_scoring", + "transit_support": { + "Jupiter": { + "bav": 3, + "sav": 27, + "sign": "Aries" + }, + "Saturn": { + "bav": 5, + "sav": 37, + "sign": "Sagittarius" + } + }, + "version": "2.1" + }, + "birth_source": { + "evidence_basis": "from memory", + "source_grade": "verified_secondary", + "time_accuracy_rating": "A", + "url": "https://www.astro.com/adbvip/adbvip_03_30.htm" + }, + "domain_varga": { + "birth_info": "1968-03-30 12:15", + "divisional_charts": { + "D10_Dasamsa": { + "Jupiter": { + "sign": "Virgo", + "sign_cn": "处女座" + }, + "Ketu": { + "sign": "Capricorn", + "sign_cn": "摩羯座" + }, + "Mars": { + "sign": "Gemini", + "sign_cn": "双子座" + }, + "Mercury": { + "sign": "Libra", + "sign_cn": "天秤座" + }, + "Moon": { + "sign": "Gemini", + "sign_cn": "双子座" + }, + "Rahu": { + "sign": "Cancer", + "sign_cn": "巨蟹座" + }, + "Saturn": { + "sign": "Gemini", + "sign_cn": "双子座" + }, + "Sun": { + "sign": "Aries", + "sign_cn": "白羊座" + }, + "Venus": { + "sign": "Libra", + "sign_cn": "天秤座" + }, + "ascendant": "Gemini" + } + } + }, + "double_transit_pac": { + "cl": { + "jupiter": { + "CL_10宫(Capricorn)": [ + { + "desc": "9宫相位", + "offset": 9, + "type": "Aspect" + } + ] + }, + "saturn": { + "CL_Saturn(宫主)": [ + { + "desc": "3宫相位", + "offset": 3, + "type": "Aspect" + } + ] + } + }, + "d1": { + "jupiter": { + "10宫(Aries)": [ + { + "desc": "同宫(10宫)", + "type": "Position" + }, + { + "desc": "合相(3.45°)", + "type": "Conjunction" + } + ], + "Mars(宫主)": [ + { + "desc": "同宫(10宫)", + "type": "Position" + }, + { + "desc": "合相(9.85°)", + "type": "Conjunction" + } + ], + "Moon(LL)": [ + { + "desc": "同宫(10宫)", + "type": "Position" + } + ] + }, + "saturn": { + "Saturn(对宫主)": [ + { + "desc": "3宫相位", + "offset": 3, + "type": "Aspect" + } + ] + } + }, + "d9": { + "jupiter": { + "D9_10宫(Gemini)": [ + { + "desc": "5宫相位", + "offset": 5, + "type": "Aspect" + } + ] + }, + "saturn": {} + }, + "double_transit": [], + "event_house": 10, + "stats": { + "chandra_lagna": "Aries", + "cl_jupiter_targets": [ + "CL_10宫(Capricorn)" + ], + "cl_overlap": [], + "cl_saturn_targets": [ + "CL_Saturn(宫主)" + ], + "d1_jupiter_targets": [ + "10宫(Aries)", + "Mars(宫主)", + "Moon(LL)" + ], + "d1_overlap": [], + "d1_saturn_targets": [ + "Saturn(对宫主)" + ], + "d9_ascendant": "Virgo", + "d9_jupiter_targets": [ + "D9_10宫(Gemini)" + ], + "d9_overlap": [], + "d9_saturn_targets": [], + "event_lord_d9_sign": "Gemini" + }, + "summary": "❌ 无 Double Transit PAC 激活", + "transit_date": "1988-04-30" + }, + "event_source": { + "source_grade": "primary", + "url": "https://eurovision.tv/event/dublin-1988" + }, + "functional_benefic_malefic": { + "ascendant": "Cancer", + "effect_on_confidence": "高严谨模式下必须叠加功能性宫主吉凶与自然吉凶;若功能属性与自然属性冲突,应降低置信度或显式标记冲突。", + "functional_benefics": [ + "Jupiter", + "Mars", + "Moon" + ], + "functional_malefics": [ + "Mercury", + "Saturn", + "Venus" + ], + "functional_neutrals": [ + "Sun" + ], + "owned_houses": { + "Jupiter": [ + 6, + 9 + ], + "Mars": [ + 5, + 10 + ], + "Mercury": [ + 3, + 12 + ], + "Moon": [ + 1 + ], + "Saturn": [ + 7, + 8 + ], + "Sun": [ + 2 + ], + "Venus": [ + 4, + 11 + ] + }, + "source": "strict_functional_benefic_malefic_v1", + "status": "used", + "yogakarakas": [ + "Mars" + ] + }, + "narayana": { + "ad": { + "end_age": 20.5333, + "lord": "Moon", + "sign": "Cancer", + "sign_idx": 3, + "start_age": 19.9333, + "years": 0.6 + }, + "md": { + "end_age": 21.0, + "lord": "Mercury", + "sign": "Virgo", + "sign_idx": 5, + "start_age": 16.0, + "years": 5 + }, + "pd": { + "end_age": 20.1013, + "lord": "Mercury", + "sign": "Virgo", + "sign_idx": 5, + "start_age": 20.0613, + "years": 0.04 + }, + "remaining_years": 0.91 + }, + "vimshottari": { + "antardasha": "Mercury", + "mahadasha": "Venus" + } + }, + "expected_label": "career_status", + "matched_expected_label": false, + "name": "Celine Dion", + "outcome": "Won the Eurovision Song Contest for Switzerland", + "result_class": "weak_hit", + "rule_version": "v2_1", + "score": 5, + "signals": [ + "Venus_owns_event_houses:[11]", + "Mercury_domain_karaka", + "active_dasha_matches_D10_Lagna_lord:Mercury", + "active_dasha_matches_Amatyakaraka:Mercury" + ] + }, + { + "actual_label": "legal_marriage", + "birth_time_rating": "A", + "blocked": false, + "case_id": "mccartney_marriage_1969", + "domain": "marriage", + "event_date": "1969-03-12", + "evidence": { + "arudha_lord": "Mars", + "ascendant": { + "degree": 2.2532, + "degree_in_sign": 2.2532, + "degree_in_sign_raw": 152.25316730126025, + "degree_raw": 152.2532, + "lon": 152.2532, + "lord": "Mercury", + "sign": "Virgo", + "sign_cn": "处女座" + }, + "ashtakavarga_audit": { + "all_bav_valid": true, + "boundary": "Audit evidence only. SAV/BAV does not change V2.1 event scores until a fresh holdout validates it.", + "event_house_sav": { + "11": { + "level": "极吉", + "sav_score": 30, + "sign": "Cancer" + }, + "2": { + "level": "中等", + "sav_score": 26, + "sign": "Libra" + }, + "5": { + "level": "吉利", + "sav_score": 29, + "sign": "Capricorn" + }, + "7": { + "level": "极吉", + "sav_score": 32, + "sign": "Pisces" + } + }, + "method": "Ashtakavarga八分法(BPHS/PVR书例校准v2.1)", + "sav_total": 337, + "sav_valid": true, + "scoring_effect": 0, + "settings": { + "ayanamsa": 23.4268, + "node_mode": "mean" + }, + "status": "used_non_scoring", + "transit_support": { + "Jupiter": { + "bav": 6, + "sav": 32, + "sign": "Virgo" + }, + "Saturn": { + "bav": 4, + "sav": 31, + "sign": "Aries" + } + }, + "version": "2.1" + }, + "birth_source": { + "evidence_basis": "from memory", + "source_grade": "verified_secondary", + "time_accuracy_rating": "A", + "url": "https://www.astro.com/adbvip/adbvip_06_18.htm" + }, + "domain_varga": { + "birth_info": "1942-06-18 14:00", + "divisional_charts": { + "D9_Navamsa": { + "Jupiter": { + "sign": "Sagittarius", + "sign_cn": "射手座" + }, + "Ketu": { + "sign": "Aquarius", + "sign_cn": "水瓶座" + }, + "Mars": { + "sign": "Virgo", + "sign_cn": "处女座" + }, + "Mercury": { + "sign": "Leo", + "sign_cn": "狮子座" + }, + "Moon": { + "sign": "Aquarius", + "sign_cn": "水瓶座" + }, + "Rahu": { + "sign": "Leo", + "sign_cn": "狮子座" + }, + "Saturn": { + "sign": "Aries", + "sign_cn": "白羊座" + }, + "Sun": { + "sign": "Scorpio", + "sign_cn": "天蝎座" + }, + "Venus": { + "sign": "Scorpio", + "sign_cn": "天蝎座" + }, + "ascendant": "Capricorn" + } + } + }, + "double_transit_pac": { + "cl": { + "jupiter": {}, + "saturn": {} + }, + "d1": { + "jupiter": { + "Jupiter(宫主)": [ + { + "desc": "9宫相位", + "offset": 9, + "type": "Aspect" + } + ], + "Jupiter(对宫主)": [ + { + "desc": "9宫相位", + "offset": 9, + "type": "Aspect" + } + ] + }, + "saturn": {} + }, + "d9": { + "jupiter": {}, + "saturn": { + "D9_Moon(宫主)": [ + { + "desc": "3宫相位", + "offset": 3, + "type": "Aspect" + } + ] + } + }, + "double_transit": [], + "event_house": 7, + "stats": { + "chandra_lagna": "Cancer", + "cl_jupiter_targets": [], + "cl_overlap": [], + "cl_saturn_targets": [], + "d1_jupiter_targets": [ + "Jupiter(宫主)", + "Jupiter(对宫主)" + ], + "d1_overlap": [], + "d1_saturn_targets": [], + "d9_ascendant": "Capricorn", + "d9_jupiter_targets": [], + "d9_overlap": [], + "d9_saturn_targets": [ + "D9_Moon(宫主)" + ], + "event_lord_d9_sign": "Sagittarius" + }, + "summary": "❌ 无 Double Transit PAC 激活", + "transit_date": "1969-03-12" + }, + "event_source": { + "source_grade": "verified_secondary", + "url": "https://en.wikipedia.org/wiki/Personal_relationships_of_Paul_McCartney" + }, + "functional_benefic_malefic": { + "ascendant": "Virgo", + "effect_on_confidence": "高严谨模式下必须叠加功能性宫主吉凶与自然吉凶;若功能属性与自然属性冲突,应降低置信度或显式标记冲突。", + "functional_benefics": [ + "Mercury", + "Saturn", + "Venus" + ], + "functional_malefics": [ + "Mars", + "Moon", + "Sun" + ], + "functional_neutrals": [ + "Jupiter" + ], + "owned_houses": { + "Jupiter": [ + 4, + 7 + ], + "Mars": [ + 3, + 8 + ], + "Mercury": [ + 1, + 10 + ], + "Moon": [ + 11 + ], + "Saturn": [ + 5, + 6 + ], + "Sun": [ + 12 + ], + "Venus": [ + 2, + 9 + ] + }, + "source": "strict_functional_benefic_malefic_v1", + "status": "used", + "yogakarakas": [ + "Mercury" + ] + }, + "narayana": { + "ad": { + "end_age": 27.0118, + "lord": "Mercury", + "sign": "Virgo", + "sign_idx": 5, + "start_age": 26.4471, + "years": 0.5647 + }, + "md": { + "end_age": 28.0, + "lord": "Jupiter", + "sign": "Sagittarius", + "sign_idx": 8, + "start_age": 22.0, + "years": 6 + }, + "pd": { + "end_age": 26.7926, + "lord": "Venus", + "sign": "Taurus", + "sign_idx": 1, + "start_age": 26.7195, + "years": 0.0731 + }, + "remaining_years": 1.27 + }, + "vimshottari": { + "antardasha": "Jupiter", + "mahadasha": "Venus" + } + }, + "expected_label": "legal_marriage", + "matched_expected_label": true, + "name": "Paul McCartney", + "outcome": "Married Linda Eastman", + "result_class": "strong_hit", + "rule_version": "v2_1", + "score": 7, + "signals": [ + "Venus_owns_event_houses:[2]", + "Venus_domain_karaka", + "Jupiter_owns_event_houses:[7]", + "Jupiter_domain_karaka", + "narayana_lord_owns_event_house:Jupiter" + ] + }, + { + "actual_label": "domain_activation", + "birth_time_rating": "AA", + "blocked": false, + "case_id": "depp_marriage_2015", + "domain": "marriage", + "event_date": "2015-02-03", + "evidence": { + "arudha_lord": "Mars", + "ascendant": { + "degree": 6.9997, + "degree_in_sign": 6.9997, + "degree_in_sign_raw": 96.99973733317259, + "degree_raw": 96.9997, + "lon": 96.9997, + "lord": "Moon", + "sign": "Cancer", + "sign_cn": "巨蟹座" + }, + "ashtakavarga_audit": { + "all_bav_valid": true, + "boundary": "Audit evidence only. SAV/BAV does not change V2.1 event scores until a fresh holdout validates it.", + "event_house_sav": { + "11": { + "level": "极吉", + "sav_score": 31, + "sign": "Taurus" + }, + "2": { + "level": "中等", + "sav_score": 26, + "sign": "Leo" + }, + "5": { + "level": "挑战", + "sav_score": 23, + "sign": "Scorpio" + }, + "7": { + "level": "吉利", + "sav_score": 29, + "sign": "Capricorn" + } + }, + "method": "Ashtakavarga八分法(BPHS/PVR书例校准v2.1)", + "sav_total": 337, + "sav_valid": true, + "scoring_effect": 0, + "settings": { + "ayanamsa": 24.0679, + "node_mode": "mean" + }, + "status": "used_non_scoring", + "transit_support": { + "Jupiter": { + "bav": 2, + "sav": 26, + "sign": "Cancer" + }, + "Saturn": { + "bav": 2, + "sav": 23, + "sign": "Scorpio" + } + }, + "version": "2.1" + }, + "birth_source": { + "evidence_basis": "BC/BR in hand", + "source_grade": "primary", + "time_accuracy_rating": "AA", + "url": "https://www.astro.com/adbvip/adbvip_06_09.htm" + }, + "domain_varga": { + "birth_info": "1963-06-09 08:44", + "divisional_charts": { + "D9_Navamsa": { + "Jupiter": { + "sign": "Capricorn", + "sign_cn": "摩羯座" + }, + "Ketu": { + "sign": "Sagittarius", + "sign_cn": "射手座" + }, + "Mars": { + "sign": "Gemini", + "sign_cn": "双子座" + }, + "Mercury": { + "sign": "Capricorn", + "sign_cn": "摩羯座" + }, + "Moon": { + "sign": "Virgo", + "sign_cn": "处女座" + }, + "Rahu": { + "sign": "Gemini", + "sign_cn": "双子座" + }, + "Saturn": { + "sign": "Virgo", + "sign_cn": "处女座" + }, + "Sun": { + "sign": "Leo", + "sign_cn": "狮子座" + }, + "Venus": { + "sign": "Capricorn", + "sign_cn": "摩羯座" + }, + "ascendant": "Virgo" + } + } + }, + "double_transit_pac": { + "cl": { + "jupiter": {}, + "saturn": { + "CL_7宫(Gemini)": [ + { + "desc": "7宫相位", + "offset": 7, + "type": "Aspect" + } + ] + } + }, + "d1": { + "jupiter": { + "Moon(LL)": [ + { + "desc": "5宫相位", + "offset": 5, + "type": "Aspect" + } + ] + }, + "saturn": {} + }, + "d9": { + "jupiter": {}, + "saturn": { + "D9_7宫(Pisces)": [ + { + "desc": "10宫相位", + "offset": 10, + "type": "Aspect" + } + ], + "Moon_D9(Virgo)": [ + { + "desc": "10宫相位", + "offset": 10, + "type": "Aspect" + } + ], + "Saturn_D9(Virgo)": [ + { + "desc": "10宫相位", + "offset": 10, + "type": "Aspect" + } + ] + } + }, + "double_transit": [], + "event_house": 7, + "stats": { + "chandra_lagna": "Sagittarius", + "cl_jupiter_targets": [], + "cl_overlap": [], + "cl_saturn_targets": [ + "CL_7宫(Gemini)" + ], + "d1_jupiter_targets": [ + "Moon(LL)" + ], + "d1_overlap": [], + "d1_saturn_targets": [], + "d9_ascendant": "Virgo", + "d9_jupiter_targets": [], + "d9_overlap": [], + "d9_saturn_targets": [ + "D9_7宫(Pisces)", + "Moon_D9(Virgo)", + "Saturn_D9(Virgo)" + ], + "event_lord_d9_sign": "Virgo" + }, + "summary": "❌ 无 Double Transit PAC 激活", + "transit_date": "2015-02-03" + }, + "event_source": { + "source_grade": "verified_secondary", + "url": "https://variety.com/2016/biz/news/johnny-depp-amber-heard-divorce-settlement-1201837685/" + }, + "functional_benefic_malefic": { + "ascendant": "Cancer", + "effect_on_confidence": "高严谨模式下必须叠加功能性宫主吉凶与自然吉凶;若功能属性与自然属性冲突,应降低置信度或显式标记冲突。", + "functional_benefics": [ + "Jupiter", + "Mars", + "Moon" + ], + "functional_malefics": [ + "Mercury", + "Saturn", + "Venus" + ], + "functional_neutrals": [ + "Sun" + ], + "owned_houses": { + "Jupiter": [ + 6, + 9 + ], + "Mars": [ + 5, + 10 + ], + "Mercury": [ + 3, + 12 + ], + "Moon": [ + 1 + ], + "Saturn": [ + 7, + 8 + ], + "Sun": [ + 2 + ], + "Venus": [ + 4, + 11 + ] + }, + "source": "strict_functional_benefic_malefic_v1", + "status": "used", + "yogakarakas": [ + "Mars" + ] + }, + "narayana": { + "ad": { + "end_age": 52.6505, + "lord": "Mars", + "sign": "Scorpio", + "sign_idx": 7, + "start_age": 51.6019, + "years": 1.0485 + }, + "md": { + "end_age": 53.0, + "lord": "Saturn", + "sign": "Capricorn", + "sign_idx": 9, + "start_age": 41.0, + "years": 12 + }, + "pd": { + "end_age": 51.6935, + "lord": "Mars", + "sign": "Scorpio", + "sign_idx": 7, + "start_age": 51.6019, + "years": 0.0916 + }, + "remaining_years": 1.34 + }, + "vimshottari": { + "antardasha": "Jupiter", + "mahadasha": "Jupiter" + } + }, + "expected_label": "legal_marriage", + "matched_expected_label": false, + "name": "Johnny Depp", + "outcome": "Married Amber Heard in a civil ceremony", + "result_class": "weak_hit", + "rule_version": "v2_1", + "score": 5, + "signals": [ + "Jupiter_domain_karaka", + "active_dasha_matches_D9_7L:Jupiter", + "narayana_activates_primary_event_sign:Capricorn", + "narayana_lord_owns_event_house:Saturn" + ] + }, + { + "actual_label": null, + "birth_time_rating": "A", + "blocked": false, + "case_id": "kidman_marriage_2006", + "domain": "marriage", + "event_date": "2006-06-25", + "evidence": { + "arudha_lord": "Mercury", + "ascendant": { + "degree": 12.9323, + "degree_in_sign": 12.9323, + "degree_in_sign_raw": 192.9322623333046, + "degree_raw": 192.9323, + "lon": 192.9323, + "lord": "Venus", + "sign": "Libra", + "sign_cn": "天秤座" + }, + "ashtakavarga_audit": { + "all_bav_valid": true, + "boundary": "Audit evidence only. SAV/BAV does not change V2.1 event scores until a fresh holdout validates it.", + "event_house_sav": { + "11": { + "level": "吉利", + "sav_score": 28, + "sign": "Leo" + }, + "2": { + "level": "吉利", + "sav_score": 28, + "sign": "Scorpio" + }, + "5": { + "level": "挑战", + "sav_score": 23, + "sign": "Aquarius" + }, + "7": { + "level": "极吉", + "sav_score": 33, + "sign": "Aries" + } + }, + "method": "Ashtakavarga八分法(BPHS/PVR书例校准v2.1)", + "sav_total": 337, + "sav_valid": true, + "scoring_effect": 0, + "settings": { + "ayanamsa": 23.9476, + "node_mode": "mean" + }, + "status": "used_non_scoring", + "transit_support": { + "Jupiter": { + "bav": 4, + "sav": 28, + "sign": "Libra" + }, + "Saturn": { + "bav": 4, + "sav": 28, + "sign": "Cancer" + } + }, + "version": "2.1" + }, + "birth_source": { + "evidence_basis": "from memory", + "source_grade": "verified_secondary", + "time_accuracy_rating": "A", + "url": "https://www.astro.com/adbvip/adbvip_06_20.htm" + }, + "domain_varga": { + "birth_info": "1967-06-20 15:15", + "divisional_charts": { + "D9_Navamsa": { + "Jupiter": { + "sign": "Libra", + "sign_cn": "天秤座" + }, + "Ketu": { + "sign": "Capricorn", + "sign_cn": "摩羯座" + }, + "Mars": { + "sign": "Leo", + "sign_cn": "狮子座" + }, + "Mercury": { + "sign": "Gemini", + "sign_cn": "双子座" + }, + "Moon": { + "sign": "Capricorn", + "sign_cn": "摩羯座" + }, + "Rahu": { + "sign": "Cancer", + "sign_cn": "巨蟹座" + }, + "Saturn": { + "sign": "Sagittarius", + "sign_cn": "射手座" + }, + "Sun": { + "sign": "Scorpio", + "sign_cn": "天蝎座" + }, + "Venus": { + "sign": "Capricorn", + "sign_cn": "摩羯座" + }, + "ascendant": "Capricorn" + } + } + }, + "double_transit_pac": { + "cl": { + "jupiter": { + "CL_7宫(Taurus)": [ + { + "desc": "7宫相位", + "offset": 7, + "type": "Aspect" + } + ], + "CL_Venus(宫主)": [ + { + "desc": "9宫相位", + "offset": 9, + "type": "Aspect" + } + ] + }, + "saturn": { + "CL_7宫(Taurus)": [ + { + "desc": "10宫相位", + "offset": 10, + "type": "Aspect" + } + ], + "CL_Venus(宫主)": [ + { + "desc": "同宫(9宫)", + "type": "Position" + }, + { + "desc": "合相(5.32°)", + "type": "Conjunction" + } + ] + } + }, + "d1": { + "jupiter": { + "Venus(LL)": [ + { + "desc": "9宫相位", + "offset": 9, + "type": "Aspect" + } + ] + }, + "saturn": { + "Venus(LL)": [ + { + "desc": "同宫(10宫)", + "type": "Position" + }, + { + "desc": "合相(5.32°)", + "type": "Conjunction" + } + ] + } + }, + "d9": { + "jupiter": {}, + "saturn": {} + }, + "double_transit": [ + { + "jupiter_pac": [ + { + "desc": "9宫相位", + "offset": 9, + "type": "Aspect" + } + ], + "layer": "D1", + "saturn_pac": [ + { + "desc": "同宫(10宫)", + "type": "Position" + }, + { + "desc": "合相(5.32°)", + "type": "Conjunction" + } + ], + "strength": "strong", + "target": "Venus(LL)" + }, + { + "jupiter_pac": [ + { + "desc": "9宫相位", + "offset": 9, + "type": "Aspect" + } + ], + "layer": "CL", + "saturn_pac": [ + { + "desc": "同宫(9宫)", + "type": "Position" + }, + { + "desc": "合相(5.32°)", + "type": "Conjunction" + } + ], + "strength": "strong", + "target": "CL_Venus(宫主)" + }, + { + "jupiter_pac": [ + { + "desc": "7宫相位", + "offset": 7, + "type": "Aspect" + } + ], + "layer": "CL", + "saturn_pac": [ + { + "desc": "10宫相位", + "offset": 10, + "type": "Aspect" + } + ], + "strength": "strong", + "target": "CL_7宫(Taurus)" + } + ], + "event_house": 7, + "stats": { + "chandra_lagna": "Scorpio", + "cl_jupiter_targets": [ + "CL_7宫(Taurus)", + "CL_Venus(宫主)" + ], + "cl_overlap": [ + "CL_7宫(Taurus)", + "CL_Venus(宫主)" + ], + "cl_saturn_targets": [ + "CL_7宫(Taurus)", + "CL_Venus(宫主)" + ], + "d1_jupiter_targets": [ + "Venus(LL)" + ], + "d1_overlap": [ + "Venus(LL)" + ], + "d1_saturn_targets": [ + "Venus(LL)" + ], + "d9_ascendant": "Capricorn", + "d9_jupiter_targets": [], + "d9_overlap": [], + "d9_saturn_targets": [], + "event_lord_d9_sign": "Leo" + }, + "summary": "✅ Double Transit PAC 确认: D1+CL 多层激活7宫主题", + "transit_date": "2006-06-25" + }, + "event_source": { + "source_grade": "verified_secondary", + "url": "https://en.wikipedia.org/wiki/Nicole_Kidman" + }, + "functional_benefic_malefic": { + "ascendant": "Libra", + "effect_on_confidence": "高严谨模式下必须叠加功能性宫主吉凶与自然吉凶;若功能属性与自然属性冲突,应降低置信度或显式标记冲突。", + "functional_benefics": [ + "Mercury", + "Saturn", + "Venus" + ], + "functional_malefics": [ + "Jupiter", + "Sun" + ], + "functional_neutrals": [ + "Mars", + "Moon" + ], + "owned_houses": { + "Jupiter": [ + 3, + 6 + ], + "Mars": [ + 2, + 7 + ], + "Mercury": [ + 9, + 12 + ], + "Moon": [ + 10 + ], + "Saturn": [ + 4, + 5 + ], + "Sun": [ + 11 + ], + "Venus": [ + 1, + 8 + ] + }, + "source": "strict_functional_benefic_malefic_v1", + "status": "used", + "yogakarakas": [ + "Saturn", + "Venus" + ] + }, + "narayana": { + "ad": { + "end_age": 39.2267, + "lord": "Venus", + "sign": "Libra", + "sign_idx": 6, + "start_age": 38.9867, + "years": 0.24 + }, + "md": { + "end_age": 40.0, + "lord": "Venus", + "sign": "Taurus", + "sign_idx": 1, + "start_age": 38.0, + "years": 2 + }, + "pd": { + "end_age": 39.0155, + "lord": "Venus", + "sign": "Libra", + "sign_idx": 6, + "start_age": 38.9867, + "years": 0.0288 + }, + "remaining_years": 0.98 + }, + "vimshottari": { + "antardasha": "Ketu", + "mahadasha": "Venus" + } + }, + "expected_label": "legal_marriage", + "matched_expected_label": false, + "name": "Nicole Kidman", + "outcome": "Married Keith Urban", + "result_class": "miss", + "rule_version": "v2_1", + "score": 3, + "signals": [ + "Venus_domain_karaka", + "double_transit_pac_strong" + ] + }, + { + "actual_label": null, + "birth_time_rating": "A", + "blocked": false, + "case_id": "demi_moore_marriage_1987", + "domain": "marriage", + "event_date": "1987-11-21", + "evidence": { + "arudha_lord": "Venus", + "ascendant": { + "degree": 29.0648, + "degree_in_sign": 29.0648, + "degree_in_sign_raw": 329.06483110736514, + "degree_raw": 329.0648, + "lon": 329.0648, + "lord": "Saturn", + "sign": "Aquarius", + "sign_cn": "水瓶座" + }, + "ashtakavarga_audit": { + "all_bav_valid": true, + "boundary": "Audit evidence only. SAV/BAV does not change V2.1 event scores until a fresh holdout validates it.", + "event_house_sav": { + "11": { + "level": "极吉", + "sav_score": 32, + "sign": "Sagittarius" + }, + "2": { + "level": "中等", + "sav_score": 26, + "sign": "Pisces" + }, + "5": { + "level": "中等", + "sav_score": 27, + "sign": "Gemini" + }, + "7": { + "level": "极吉", + "sav_score": 34, + "sign": "Leo" + } + }, + "method": "Ashtakavarga八分法(BPHS/PVR书例校准v2.1)", + "sav_total": 337, + "sav_valid": true, + "scoring_effect": 0, + "settings": { + "ayanamsa": 23.6879, + "node_mode": "mean" + }, + "status": "used_non_scoring", + "transit_support": { + "Jupiter": { + "bav": 5, + "sav": 26, + "sign": "Pisces" + }, + "Saturn": { + "bav": 4, + "sav": 27, + "sign": "Scorpio" + } + }, + "version": "2.1" + }, + "birth_source": { + "evidence_basis": "from memory", + "source_grade": "verified_secondary", + "time_accuracy_rating": "A", + "url": "https://www.astro.com/adbvip/adbvip_11_11.htm" + }, + "domain_varga": { + "birth_info": "1962-11-11 14:16", + "divisional_charts": { + "D9_Navamsa": { + "Jupiter": { + "sign": "Sagittarius", + "sign_cn": "射手座" + }, + "Ketu": { + "sign": "Pisces", + "sign_cn": "双鱼座" + }, + "Mars": { + "sign": "Capricorn", + "sign_cn": "摩羯座" + }, + "Mercury": { + "sign": "Pisces", + "sign_cn": "双鱼座" + }, + "Moon": { + "sign": "Scorpio", + "sign_cn": "天蝎座" + }, + "Rahu": { + "sign": "Virgo", + "sign_cn": "处女座" + }, + "Saturn": { + "sign": "Aries", + "sign_cn": "白羊座" + }, + "Sun": { + "sign": "Taurus", + "sign_cn": "金牛座" + }, + "Venus": { + "sign": "Gemini", + "sign_cn": "双子座" + }, + "ascendant": "Gemini" + } + } + }, + "double_transit_pac": { + "cl": { + "jupiter": { + "CL_7宫(Libra)": [ + { + "desc": "7宫相位", + "offset": 7, + "type": "Aspect" + } + ], + "CL_Venus(宫主)": [ + { + "desc": "7宫相位", + "offset": 7, + "type": "Aspect" + } + ] + }, + "saturn": {} + }, + "d1": { + "jupiter": { + "7宫(Leo)": [ + { + "desc": "5宫相位", + "offset": 5, + "type": "Aspect" + } + ], + "Sun(宫主)": [ + { + "desc": "7宫相位", + "offset": 7, + "type": "Aspect" + } + ], + "Sun(对宫主)": [ + { + "desc": "7宫相位", + "offset": 7, + "type": "Aspect" + } + ] + }, + "saturn": {} + }, + "d9": { + "jupiter": {}, + "saturn": { + "D9_7宫(Sagittarius)": [ + { + "desc": "7宫相位", + "offset": 7, + "type": "Aspect" + } + ], + "D9_Jupiter(宫主)": [ + { + "desc": "3宫相位", + "offset": 3, + "type": "Aspect" + } + ] + } + }, + "double_transit": [], + "event_house": 7, + "stats": { + "chandra_lagna": "Aries", + "cl_jupiter_targets": [ + "CL_7宫(Libra)", + "CL_Venus(宫主)" + ], + "cl_overlap": [], + "cl_saturn_targets": [], + "d1_jupiter_targets": [ + "7宫(Leo)", + "Sun(宫主)", + "Sun(对宫主)" + ], + "d1_overlap": [], + "d1_saturn_targets": [], + "d9_ascendant": "Gemini", + "d9_jupiter_targets": [], + "d9_overlap": [], + "d9_saturn_targets": [ + "D9_7宫(Sagittarius)", + "D9_Jupiter(宫主)" + ], + "event_lord_d9_sign": "Taurus" + }, + "summary": "❌ 无 Double Transit PAC 激活", + "transit_date": "1987-11-21" + }, + "event_source": { + "source_grade": "verified_secondary", + "url": "https://en.wikipedia.org/wiki/Demi_Moore" + }, + "functional_benefic_malefic": { + "ascendant": "Aquarius", + "effect_on_confidence": "高严谨模式下必须叠加功能性宫主吉凶与自然吉凶;若功能属性与自然属性冲突,应降低置信度或显式标记冲突。", + "functional_benefics": [ + "Mercury", + "Saturn", + "Venus" + ], + "functional_malefics": [ + "Jupiter", + "Mars", + "Moon" + ], + "functional_neutrals": [ + "Sun" + ], + "owned_houses": { + "Jupiter": [ + 2, + 11 + ], + "Mars": [ + 3, + 10 + ], + "Mercury": [ + 5, + 8 + ], + "Moon": [ + 6 + ], + "Saturn": [ + 1, + 12 + ], + "Sun": [ + 7 + ], + "Venus": [ + 4, + 9 + ] + }, + "source": "strict_functional_benefic_malefic_v1", + "status": "used", + "yogakarakas": [ + "Saturn", + "Venus" + ] + }, + "narayana": { + "ad": { + "end_age": 25.3125, + "lord": "Venus", + "sign": "Taurus", + "sign_idx": 1, + "start_age": 25.0, + "years": 0.3125 + }, + "md": { + "end_age": 30.0, + "lord": "Venus", + "sign": "Taurus", + "sign_idx": 1, + "start_age": 25.0, + "years": 5 + }, + "pd": { + "end_age": 25.0352, + "lord": "Mercury", + "sign": "Gemini", + "sign_idx": 2, + "start_age": 25.0195, + "years": 0.0156 + }, + "remaining_years": 4.97 + }, + "vimshottari": { + "antardasha": "Moon", + "mahadasha": "Mars" + } + }, + "expected_label": "legal_marriage", + "matched_expected_label": false, + "name": "Demi Moore", + "outcome": "Married Bruce Willis", + "result_class": "miss", + "rule_version": "v2_1", + "score": 0, + "signals": [] + }, + { + "actual_label": "domain_activation", + "birth_time_rating": "AA", + "blocked": false, + "case_id": "chelsea_clinton_marriage_2010", + "domain": "marriage", + "event_date": "2010-07-31", + "evidence": { + "arudha_lord": "Moon", + "ascendant": { + "degree": 22.7703, + "degree_in_sign": 22.7703, + "degree_in_sign_raw": 202.7702714237087, + "degree_raw": 202.7703, + "lon": 202.7703, + "lord": "Venus", + "sign": "Libra", + "sign_cn": "天秤座" + }, + "ashtakavarga_audit": { + "all_bav_valid": true, + "boundary": "Audit evidence only. SAV/BAV does not change V2.1 event scores until a fresh holdout validates it.", + "event_house_sav": { + "11": { + "level": "中等", + "sav_score": 26, + "sign": "Leo" + }, + "2": { + "level": "极吉", + "sav_score": 30, + "sign": "Scorpio" + }, + "5": { + "level": "挑战", + "sav_score": 22, + "sign": "Aquarius" + }, + "7": { + "level": "挑战", + "sav_score": 24, + "sign": "Aries" + } + }, + "method": "Ashtakavarga八分法(BPHS/PVR书例校准v2.1)", + "sav_total": 337, + "sav_valid": true, + "scoring_effect": 0, + "settings": { + "ayanamsa": 24.0049, + "node_mode": "mean" + }, + "status": "used_non_scoring", + "transit_support": { + "Jupiter": { + "bav": 6, + "sav": 26, + "sign": "Pisces" + }, + "Saturn": { + "bav": 3, + "sav": 25, + "sign": "Virgo" + } + }, + "version": "2.1" + }, + "birth_source": { + "evidence_basis": "BC/BR in hand", + "source_grade": "primary", + "time_accuracy_rating": "AA", + "url": "https://www.astro.com/adbvip/adbvip_02_27.htm" + }, + "domain_varga": { + "birth_info": "1980-02-27 23:24", + "divisional_charts": { + "D9_Navamsa": { + "Jupiter": { + "sign": "Cancer", + "sign_cn": "巨蟹座" + }, + "Ketu": { + "sign": "Scorpio", + "sign_cn": "天蝎座" + }, + "Mars": { + "sign": "Cancer", + "sign_cn": "巨蟹座" + }, + "Mercury": { + "sign": "Gemini", + "sign_cn": "双子座" + }, + "Moon": { + "sign": "Scorpio", + "sign_cn": "天蝎座" + }, + "Rahu": { + "sign": "Taurus", + "sign_cn": "金牛座" + }, + "Saturn": { + "sign": "Capricorn", + "sign_cn": "摩羯座" + }, + "Sun": { + "sign": "Aquarius", + "sign_cn": "水瓶座" + }, + "Venus": { + "sign": "Pisces", + "sign_cn": "双鱼座" + }, + "ascendant": "Aries" + } + } + }, + "double_transit_pac": { + "cl": { + "jupiter": {}, + "saturn": { + "CL_Saturn(宫主)": [ + { + "desc": "同宫(3宫)", + "type": "Position" + }, + { + "desc": "合相(5.63°)", + "type": "Conjunction" + } + ] + } + }, + "d1": { + "jupiter": { + "Mars(宫主)": [ + { + "desc": "5宫相位", + "offset": 5, + "type": "Aspect" + } + ], + "Mars(对宫主)": [ + { + "desc": "5宫相位", + "offset": 5, + "type": "Aspect" + } + ], + "Venus(LL)": [ + { + "desc": "同宫(6宫)", + "type": "Position" + } + ] + }, + "saturn": { + "7宫(Aries)": [ + { + "desc": "7宫相位", + "offset": 7, + "type": "Aspect" + } + ] + } + }, + "d9": { + "jupiter": { + "D9_Venus(宫主)": [ + { + "desc": "同宫(12宫)", + "type": "Position" + } + ], + "Venus_D9(Pisces)": [ + { + "desc": "同宫(12宫)", + "type": "Position" + }, + { + "desc": "合相(5.71°)", + "type": "Conjunction" + } + ] + }, + "saturn": { + "D9_7宫(Libra)": [ + { + "desc": "7宫相位", + "offset": 7, + "type": "Aspect" + } + ], + "Mars_D9(Cancer)": [ + { + "desc": "10宫相位", + "offset": 10, + "type": "Aspect" + } + ] + } + }, + "double_transit": [ + { + "jupiter_pac": [ + { + "desc": "5宫相位", + "offset": 5, + "type": "Aspect" + } + ], + "layer": "D1+D9", + "saturn_pac": [ + { + "desc": "10宫相位", + "offset": 10, + "type": "Aspect" + } + ], + "strength": "moderate", + "target": "Jupiter(D1)Mars(宫主) + Saturn(D9)Mars_D9(Cancer)" + }, + { + "jupiter_pac": [ + { + "desc": "5宫相位", + "offset": 5, + "type": "Aspect" + } + ], + "layer": "D1+D9", + "saturn_pac": [ + { + "desc": "10宫相位", + "offset": 10, + "type": "Aspect" + } + ], + "strength": "moderate", + "target": "Jupiter(D1)Mars(对宫主) + Saturn(D9)Mars_D9(Cancer)" + } + ], + "event_house": 7, + "stats": { + "chandra_lagna": "Cancer", + "cl_jupiter_targets": [], + "cl_overlap": [], + "cl_saturn_targets": [ + "CL_Saturn(宫主)" + ], + "d1_jupiter_targets": [ + "Mars(宫主)", + "Mars(对宫主)", + "Venus(LL)" + ], + "d1_overlap": [], + "d1_saturn_targets": [ + "7宫(Aries)" + ], + "d9_ascendant": "Aries", + "d9_jupiter_targets": [ + "D9_Venus(宫主)", + "Venus_D9(Pisces)" + ], + "d9_overlap": [], + "d9_saturn_targets": [ + "D9_7宫(Libra)", + "Mars_D9(Cancer)" + ], + "event_lord_d9_sign": "Cancer" + }, + "summary": "⚠️ 跨层间接 Double Transit (D1+D9),需结合 Dasha 确认", + "transit_date": "2010-07-31" + }, + "event_source": { + "source_grade": "verified_secondary", + "url": "https://en.wikipedia.org/wiki/Chelsea_Clinton" + }, + "functional_benefic_malefic": { + "ascendant": "Libra", + "effect_on_confidence": "高严谨模式下必须叠加功能性宫主吉凶与自然吉凶;若功能属性与自然属性冲突,应降低置信度或显式标记冲突。", + "functional_benefics": [ + "Mercury", + "Saturn", + "Venus" + ], + "functional_malefics": [ + "Jupiter", + "Sun" + ], + "functional_neutrals": [ + "Mars", + "Moon" + ], + "owned_houses": { + "Jupiter": [ + 3, + 6 + ], + "Mars": [ + 2, + 7 + ], + "Mercury": [ + 9, + 12 + ], + "Moon": [ + 10 + ], + "Saturn": [ + 4, + 5 + ], + "Sun": [ + 11 + ], + "Venus": [ + 1, + 8 + ] + }, + "source": "strict_functional_benefic_malefic_v1", + "status": "used", + "yogakarakas": [ + "Saturn", + "Venus" + ] + }, + "narayana": { + "ad": { + "end_age": 30.5632, + "lord": "Saturn", + "sign": "Aquarius", + "sign_idx": 10, + "start_age": 30.0, + "years": 0.5632 + }, + "md": { + "end_age": 37.0, + "lord": "Saturn", + "sign": "Aquarius", + "sign_idx": 10, + "start_age": 30.0, + "years": 7 + }, + "pd": { + "end_age": 30.4596, + "lord": "Mars", + "sign": "Scorpio", + "sign_idx": 7, + "start_age": 30.4014, + "years": 0.0583 + }, + "remaining_years": 6.58 + }, + "vimshottari": { + "antardasha": "Moon", + "mahadasha": "Venus" + } + }, + "expected_label": "legal_marriage", + "matched_expected_label": false, + "name": "Chelsea Clinton", + "outcome": "Married Marc Mezvinsky", + "result_class": "weak_hit", + "rule_version": "v2_1", + "score": 5, + "signals": [ + "Venus_domain_karaka", + "active_dasha_matches_D9_7L:Venus", + "active_dasha_matches_UL_lord:Moon", + "narayana_lord_owns_event_house:Saturn", + "double_transit_pac_present" + ] + }, + { + "actual_label": null, + "birth_time_rating": "AA", + "blocked": false, + "case_id": "trump_inauguration_2017", + "domain": "career", + "event_date": "2017-01-20", + "evidence": { + "arudha_lord": "Mercury", + "ascendant": { + "degree": 6.8497, + "degree_in_sign": 6.8497, + "degree_in_sign_raw": 126.84968959703062, + "degree_raw": 126.8497, + "lon": 126.8497, + "lord": "Sun", + "sign": "Leo", + "sign_cn": "狮子座" + }, + "ashtakavarga_audit": { + "all_bav_valid": true, + "boundary": "Audit evidence only. SAV/BAV does not change V2.1 event scores until a fresh holdout validates it.", + "event_house_sav": { + "10": { + "level": "极吉", + "sav_score": 34, + "sign": "Taurus" + }, + "11": { + "level": "极吉", + "sav_score": 30, + "sign": "Gemini" + }, + "6": { + "level": "中等", + "sav_score": 27, + "sign": "Capricorn" + }, + "9": { + "level": "极吉", + "sav_score": 30, + "sign": "Aries" + } + }, + "method": "Ashtakavarga八分法(BPHS/PVR书例校准v2.1)", + "sav_total": 337, + "sav_valid": true, + "scoring_effect": 0, + "settings": { + "ayanamsa": 24.0953, + "node_mode": "mean" + }, + "status": "used_non_scoring", + "transit_support": { + "Jupiter": { + "bav": 6, + "sav": 28, + "sign": "Virgo" + }, + "Saturn": { + "bav": 4, + "sav": 31, + "sign": "Scorpio" + } + }, + "version": "2.1" + }, + "birth_source": { + "evidence_basis": "BC/BR in hand", + "source_grade": "primary", + "time_accuracy_rating": "AA", + "url": "https://www.astro.com/astro-databank/Trump,_Donald" + }, + "domain_varga": { + "birth_info": "1946-06-14 10:54", + "divisional_charts": { + "D10_Dasamsa": { + "Jupiter": { + "sign": "Capricorn", + "sign_cn": "摩羯座" + }, + "Ketu": { + "sign": "Aries", + "sign_cn": "白羊座" + }, + "Mars": { + "sign": "Virgo", + "sign_cn": "处女座" + }, + "Mercury": { + "sign": "Scorpio", + "sign_cn": "天蝎座" + }, + "Moon": { + "sign": "Aries", + "sign_cn": "白羊座" + }, + "Rahu": { + "sign": "Libra", + "sign_cn": "天秤座" + }, + "Saturn": { + "sign": "Pisces", + "sign_cn": "双鱼座" + }, + "Sun": { + "sign": "Libra", + "sign_cn": "天秤座" + }, + "Venus": { + "sign": "Pisces", + "sign_cn": "双鱼座" + }, + "ascendant": "Libra" + } + } + }, + "double_transit_pac": { + "cl": { + "jupiter": {}, + "saturn": {} + }, + "d1": { + "jupiter": {}, + "saturn": {} + }, + "d9": { + "jupiter": { + "D9_10宫(Pisces)": [ + { + "desc": "9宫相位", + "offset": 9, + "type": "Aspect" + } + ], + "D9_Jupiter(宫主)": [ + { + "desc": "同宫(4宫)", + "type": "Position" + }, + { + "desc": "合相(4.27°)", + "type": "Conjunction" + } + ], + "Sun_D9(Virgo)": [ + { + "desc": "同宫(4宫)", + "type": "Position" + } + ] + }, + "saturn": { + "D9_10宫(Pisces)": [ + { + "desc": "7宫相位", + "offset": 7, + "type": "Aspect" + } + ], + "D9_Jupiter(宫主)": [ + { + "desc": "10宫相位", + "offset": 10, + "type": "Aspect" + } + ], + "Sun_D9(Virgo)": [ + { + "desc": "10宫相位", + "offset": 10, + "type": "Aspect" + } + ] + } + }, + "double_transit": [ + { + "jupiter_pac": [ + { + "desc": "同宫(4宫)", + "type": "Position" + } + ], + "layer": "D9", + "saturn_pac": [ + { + "desc": "10宫相位", + "offset": 10, + "type": "Aspect" + } + ], + "strength": "strong", + "target": "Sun_D9(Virgo)" + }, + { + "jupiter_pac": [ + { + "desc": "同宫(4宫)", + "type": "Position" + }, + { + "desc": "合相(4.27°)", + "type": "Conjunction" + } + ], + "layer": "D9", + "saturn_pac": [ + { + "desc": "10宫相位", + "offset": 10, + "type": "Aspect" + } + ], + "strength": "strong", + "target": "D9_Jupiter(宫主)" + }, + { + "jupiter_pac": [ + { + "desc": "9宫相位", + "offset": 9, + "type": "Aspect" + } + ], + "layer": "D9", + "saturn_pac": [ + { + "desc": "7宫相位", + "offset": 7, + "type": "Aspect" + } + ], + "strength": "strong", + "target": "D9_10宫(Pisces)" + } + ], + "event_house": 10, + "stats": { + "chandra_lagna": "Scorpio", + "cl_jupiter_targets": [], + "cl_overlap": [], + "cl_saturn_targets": [], + "d1_jupiter_targets": [], + "d1_overlap": [], + "d1_saturn_targets": [], + "d9_ascendant": "Gemini", + "d9_jupiter_targets": [ + "D9_10宫(Pisces)", + "D9_Jupiter(宫主)", + "Sun_D9(Virgo)" + ], + "d9_overlap": [ + "D9_10宫(Pisces)", + "D9_Jupiter(宫主)", + "Sun_D9(Virgo)" + ], + "d9_saturn_targets": [ + "D9_10宫(Pisces)", + "D9_Jupiter(宫主)", + "Sun_D9(Virgo)" + ], + "event_lord_d9_sign": "Cancer" + }, + "summary": "⚠️ D9 层 Double Transit 激活,D1/CL 层未确认", + "transit_date": "2017-01-20" + }, + "event_source": { + "source_grade": "primary", + "url": "https://www.inaugural.senate.gov/58th-inaugural-ceremonies/" + }, + "functional_benefic_malefic": { + "ascendant": "Leo", + "effect_on_confidence": "高严谨模式下必须叠加功能性宫主吉凶与自然吉凶;若功能属性与自然属性冲突,应降低置信度或显式标记冲突。", + "functional_benefics": [ + "Jupiter", + "Mars", + "Sun" + ], + "functional_malefics": [ + "Mercury", + "Moon", + "Saturn", + "Venus" + ], + "functional_neutrals": [], + "owned_houses": { + "Jupiter": [ + 5, + 8 + ], + "Mars": [ + 4, + 9 + ], + "Mercury": [ + 2, + 11 + ], + "Moon": [ + 12 + ], + "Saturn": [ + 6, + 7 + ], + "Sun": [ + 1 + ], + "Venus": [ + 3, + 10 + ] + }, + "source": "strict_functional_benefic_malefic_v1", + "status": "used", + "yogakarakas": [ + "Mars" + ] + }, + "narayana": { + "ad": { + "end_age": 71.5714, + "lord": "Sun", + "sign": "Leo", + "sign_idx": 4, + "start_age": 70.2857, + "years": 1.2857 + }, + "md": { + "end_age": 80.0, + "lord": "Mercury", + "sign": "Gemini", + "sign_idx": 2, + "start_age": 68.0, + "years": 12 + }, + "pd": { + "end_age": 70.699, + "lord": "Venus", + "sign": "Libra", + "sign_idx": 6, + "start_age": 70.5612, + "years": 0.1378 + }, + "remaining_years": 9.39 + }, + "vimshottari": { + "antardasha": "Jupiter", + "mahadasha": "Jupiter" + } + }, + "expected_label": "career_status", + "matched_expected_label": false, + "name": "Donald Trump", + "outcome": "Sworn in as President of the United States", + "result_class": "miss", + "rule_version": "v2_1", + "score": 3, + "signals": [ + "narayana_lord_owns_event_house:Mercury", + "double_transit_pac_strong" + ] + }, + { + "actual_label": null, + "birth_time_rating": "AA", + "blocked": false, + "case_id": "dicaprio_oscar_2016", + "domain": "career", + "event_date": "2016-02-28", + "evidence": { + "arudha_lord": "Saturn", + "ascendant": { + "degree": 9.6131, + "degree_in_sign": 9.6131, + "degree_in_sign_raw": 159.61308341512213, + "degree_raw": 159.6131, + "lon": 159.6131, + "lord": "Mercury", + "sign": "Virgo", + "sign_cn": "处女座" + }, + "ashtakavarga_audit": { + "all_bav_valid": true, + "boundary": "Audit evidence only. SAV/BAV does not change V2.1 event scores until a fresh holdout validates it.", + "event_house_sav": { + "10": { + "level": "极吉", + "sav_score": 30, + "sign": "Gemini" + }, + "11": { + "level": "极吉", + "sav_score": 39, + "sign": "Cancer" + }, + "6": { + "level": "极吉", + "sav_score": 31, + "sign": "Aquarius" + }, + "9": { + "level": "挑战", + "sav_score": 22, + "sign": "Taurus" + } + }, + "method": "Ashtakavarga八分法(BPHS/PVR书例校准v2.1)", + "sav_total": 337, + "sav_valid": true, + "scoring_effect": 0, + "settings": { + "ayanamsa": 24.0828, + "node_mode": "mean" + }, + "status": "used_non_scoring", + "transit_support": { + "Jupiter": { + "bav": 6, + "sav": 35, + "sign": "Leo" + }, + "Saturn": { + "bav": 4, + "sav": 29, + "sign": "Scorpio" + } + }, + "version": "2.1" + }, + "birth_source": { + "evidence_basis": "BC/BR in hand", + "source_grade": "primary", + "time_accuracy_rating": "AA", + "url": "https://www.astro.com/astro-databank/DiCaprio,_Leonardo" + }, + "domain_varga": { + "birth_info": "1974-11-11 02:47", + "divisional_charts": { + "D10_Dasamsa": { + "Jupiter": { + "sign": "Gemini", + "sign_cn": "双子座" + }, + "Ketu": { + "sign": "Gemini", + "sign_cn": "双子座" + }, + "Mars": { + "sign": "Pisces", + "sign_cn": "双鱼座" + }, + "Mercury": { + "sign": "Sagittarius", + "sign_cn": "射手座" + }, + "Moon": { + "sign": "Sagittarius", + "sign_cn": "射手座" + }, + "Rahu": { + "sign": "Sagittarius", + "sign_cn": "射手座" + }, + "Saturn": { + "sign": "Aquarius", + "sign_cn": "水瓶座" + }, + "Sun": { + "sign": "Gemini", + "sign_cn": "双子座" + }, + "Venus": { + "sign": "Gemini", + "sign_cn": "双子座" + }, + "ascendant": "Leo" + } + } + }, + "double_transit_pac": { + "cl": { + "jupiter": {}, + "saturn": { + "CL_10宫(Gemini)": [ + { + "desc": "7宫相位", + "offset": 7, + "type": "Aspect" + } + ] + } + }, + "d1": { + "jupiter": {}, + "saturn": { + "10宫(Gemini)": [ + { + "desc": "7宫相位", + "offset": 7, + "type": "Aspect" + } + ], + "Jupiter(对宫主)": [ + { + "desc": "3宫相位", + "offset": 3, + "type": "Aspect" + } + ] + } + }, + "d9": { + "jupiter": { + "D9_10宫(Sagittarius)": [ + { + "desc": "7宫相位", + "offset": 7, + "type": "Aspect" + } + ] + }, + "saturn": { + "D9_Jupiter(宫主)": [ + { + "desc": "3宫相位", + "offset": 3, + "type": "Aspect" + } + ], + "Mercury_D9(Scorpio)": [ + { + "desc": "同宫(9宫)", + "type": "Position" + }, + { + "desc": "合相(6.78°)", + "type": "Conjunction" + } + ] + } + }, + "double_transit": [], + "event_house": 10, + "stats": { + "chandra_lagna": "Virgo", + "cl_jupiter_targets": [], + "cl_overlap": [], + "cl_saturn_targets": [ + "CL_10宫(Gemini)" + ], + "d1_jupiter_targets": [], + "d1_overlap": [], + "d1_saturn_targets": [ + "10宫(Gemini)", + "Jupiter(对宫主)" + ], + "d9_ascendant": "Pisces", + "d9_jupiter_targets": [ + "D9_10宫(Sagittarius)" + ], + "d9_overlap": [], + "d9_saturn_targets": [ + "D9_Jupiter(宫主)", + "Mercury_D9(Scorpio)" + ], + "event_lord_d9_sign": "Scorpio" + }, + "summary": "❌ 无 Double Transit PAC 激活", + "transit_date": "2016-02-28" + }, + "event_source": { + "source_grade": "primary", + "url": "https://www.oscars.org/oscars/ceremonies/2016" + }, + "functional_benefic_malefic": { + "ascendant": "Virgo", + "effect_on_confidence": "高严谨模式下必须叠加功能性宫主吉凶与自然吉凶;若功能属性与自然属性冲突,应降低置信度或显式标记冲突。", + "functional_benefics": [ + "Mercury", + "Saturn", + "Venus" + ], + "functional_malefics": [ + "Mars", + "Moon", + "Sun" + ], + "functional_neutrals": [ + "Jupiter" + ], + "owned_houses": { + "Jupiter": [ + 4, + 7 + ], + "Mars": [ + 3, + 8 + ], + "Mercury": [ + 1, + 10 + ], + "Moon": [ + 11 + ], + "Saturn": [ + 5, + 6 + ], + "Sun": [ + 12 + ], + "Venus": [ + 2, + 9 + ] + }, + "source": "strict_functional_benefic_malefic_v1", + "status": "used", + "yogakarakas": [ + "Mercury" + ] + }, + "narayana": { + "ad": { + "end_age": 42.2769, + "lord": "Venus", + "sign": "Libra", + "sign_idx": 6, + "start_age": 40.2462, + "years": 2.0308 + }, + "md": { + "end_age": 46.0, + "lord": "Jupiter", + "sign": "Pisces", + "sign_idx": 11, + "start_age": 35.0, + "years": 11 + }, + "pd": { + "end_age": 41.3085, + "lord": "Saturn", + "sign": "Aquarius", + "sign_idx": 10, + "start_age": 41.1835, + "years": 0.125 + }, + "remaining_years": 4.7 + }, + "vimshottari": { + "antardasha": "Rahu", + "mahadasha": "Jupiter" + } + }, + "expected_label": "career_status", + "matched_expected_label": false, + "name": "Leonardo DiCaprio", + "outcome": "Won the Academy Award for Actor in a Leading Role", + "result_class": "miss", + "rule_version": "v2_1", + "score": 1, + "signals": [ + "Jupiter_occupies_event_house:6" + ] + }, + { + "actual_label": "legal_marriage", + "birth_time_rating": "AA", + "blocked": false, + "case_id": "markle_marriage_2018", + "domain": "marriage", + "event_date": "2018-05-19", + "evidence": { + "arudha_lord": "Sun", + "ascendant": { + "degree": 0.6845, + "degree_in_sign": 0.6845, + "degree_in_sign_raw": 90.68454836083515, + "degree_raw": 90.6845, + "lon": 90.6845, + "lord": "Moon", + "sign": "Cancer", + "sign_cn": "巨蟹座" + }, + "ashtakavarga_audit": { + "all_bav_valid": true, + "boundary": "Audit evidence only. SAV/BAV does not change V2.1 event scores until a fresh holdout validates it.", + "event_house_sav": { + "11": { + "level": "极吉", + "sav_score": 35, + "sign": "Taurus" + }, + "2": { + "level": "挑战", + "sav_score": 17, + "sign": "Leo" + }, + "5": { + "level": "中等", + "sav_score": 26, + "sign": "Scorpio" + }, + "7": { + "level": "挑战", + "sav_score": 23, + "sign": "Capricorn" + } + }, + "method": "Ashtakavarga八分法(BPHS/PVR书例校准v2.1)", + "sav_total": 337, + "sav_valid": true, + "scoring_effect": 0, + "settings": { + "ayanamsa": 24.1139, + "node_mode": "mean" + }, + "status": "used_non_scoring", + "transit_support": { + "Jupiter": { + "bav": 5, + "sav": 22, + "sign": "Libra" + }, + "Saturn": { + "bav": 2, + "sav": 31, + "sign": "Sagittarius" + } + }, + "version": "2.1" + }, + "birth_source": { + "evidence_basis": "BC/BR in hand", + "source_grade": "primary", + "time_accuracy_rating": "AA", + "url": "https://www.astro.com/astro-databank/Markle,_Meghan" + }, + "domain_varga": { + "birth_info": "1981-08-04 04:46", + "divisional_charts": { + "D9_Navamsa": { + "Jupiter": { + "sign": "Aries", + "sign_cn": "白羊座" + }, + "Ketu": { + "sign": "Pisces", + "sign_cn": "双鱼座" + }, + "Mars": { + "sign": "Pisces", + "sign_cn": "双鱼座" + }, + "Mercury": { + "sign": "Libra", + "sign_cn": "天秤座" + }, + "Moon": { + "sign": "Aries", + "sign_cn": "白羊座" + }, + "Rahu": { + "sign": "Virgo", + "sign_cn": "处女座" + }, + "Saturn": { + "sign": "Aries", + "sign_cn": "白羊座" + }, + "Sun": { + "sign": "Sagittarius", + "sign_cn": "射手座" + }, + "Venus": { + "sign": "Virgo", + "sign_cn": "处女座" + }, + "ascendant": "Cancer" + } + } + }, + "double_transit_pac": { + "cl": { + "jupiter": { + "CL_7宫(Pisces)": [ + { + "desc": "5宫相位", + "offset": 5, + "type": "Aspect" + } + ] + }, + "saturn": { + "CL_7宫(Pisces)": [ + { + "desc": "3宫相位", + "offset": 3, + "type": "Aspect" + } + ] + } + }, + "d1": { + "jupiter": {}, + "saturn": {} + }, + "d9": { + "jupiter": { + "D9_7宫(Capricorn)": [ + { + "desc": "9宫相位", + "offset": 9, + "type": "Aspect" + } + ] + }, + "saturn": { + "D9_7宫(Capricorn)": [ + { + "desc": "7宫相位", + "offset": 7, + "type": "Aspect" + } + ] + } + }, + "double_transit": [ + { + "jupiter_pac": [ + { + "desc": "9宫相位", + "offset": 9, + "type": "Aspect" + } + ], + "layer": "D9", + "saturn_pac": [ + { + "desc": "7宫相位", + "offset": 7, + "type": "Aspect" + } + ], + "strength": "strong", + "target": "D9_7宫(Capricorn)" + }, + { + "jupiter_pac": [ + { + "desc": "5宫相位", + "offset": 5, + "type": "Aspect" + } + ], + "layer": "CL", + "saturn_pac": [ + { + "desc": "3宫相位", + "offset": 3, + "type": "Aspect" + } + ], + "strength": "strong", + "target": "CL_7宫(Pisces)" + } + ], + "event_house": 7, + "stats": { + "chandra_lagna": "Virgo", + "cl_jupiter_targets": [ + "CL_7宫(Pisces)" + ], + "cl_overlap": [ + "CL_7宫(Pisces)" + ], + "cl_saturn_targets": [ + "CL_7宫(Pisces)" + ], + "d1_jupiter_targets": [], + "d1_overlap": [], + "d1_saturn_targets": [], + "d9_ascendant": "Cancer", + "d9_jupiter_targets": [ + "D9_7宫(Capricorn)" + ], + "d9_overlap": [ + "D9_7宫(Capricorn)" + ], + "d9_saturn_targets": [ + "D9_7宫(Capricorn)" + ], + "event_lord_d9_sign": "Aries" + }, + "summary": "✅ Double Transit PAC 确认: D9+CL 多层激活7宫主题", + "transit_date": "2018-05-19" + }, + "event_source": { + "source_grade": "primary", + "url": "https://www.royal.uk/wedding-duke-and-duchess-sussex" + }, + "functional_benefic_malefic": { + "ascendant": "Cancer", + "effect_on_confidence": "高严谨模式下必须叠加功能性宫主吉凶与自然吉凶;若功能属性与自然属性冲突,应降低置信度或显式标记冲突。", + "functional_benefics": [ + "Jupiter", + "Mars", + "Moon" + ], + "functional_malefics": [ + "Mercury", + "Saturn", + "Venus" + ], + "functional_neutrals": [ + "Sun" + ], + "owned_houses": { + "Jupiter": [ + 6, + 9 + ], + "Mars": [ + 5, + 10 + ], + "Mercury": [ + 3, + 12 + ], + "Moon": [ + 1 + ], + "Saturn": [ + 7, + 8 + ], + "Sun": [ + 2 + ], + "Venus": [ + 4, + 11 + ] + }, + "source": "strict_functional_benefic_malefic_v1", + "status": "used", + "yogakarakas": [ + "Mars" + ] + }, + "narayana": { + "ad": { + "end_age": 36.8684, + "lord": "Venus", + "sign": "Taurus", + "sign_idx": 1, + "start_age": 36.5921, + "years": 0.2763 + }, + "md": { + "end_age": 40.0, + "lord": "Mars", + "sign": "Scorpio", + "sign_idx": 7, + "start_age": 33.0, + "years": 7 + }, + "pd": { + "end_age": 36.8139, + "lord": "Saturn", + "sign": "Capricorn", + "sign_idx": 9, + "start_age": 36.7848, + "years": 0.0291 + }, + "remaining_years": 3.21 + }, + "vimshottari": { + "antardasha": "Saturn", + "mahadasha": "Jupiter" + } + }, + "expected_label": "legal_marriage", + "matched_expected_label": true, + "name": "Meghan Markle", + "outcome": "Married Prince Harry at St George's Chapel", + "result_class": "strong_hit", + "rule_version": "v2_1", + "score": 7, + "signals": [ + "Jupiter_domain_karaka", + "Saturn_owns_event_houses:[7]", + "active_dasha_matches_D9_7L:Saturn", + "narayana_lord_owns_event_house:Mars", + "double_transit_pac_strong" + ] + } + ], + "corrections": [ + "deduplicated_same_MD_AD_lord_scoring", + "SAV_BAV_non_scoring_audit", + "truthful_metric_aliases" + ], + "domain_summaries": { + "career": { + "balanced_accuracy": null, + "balanced_accuracy_blocked_reason": "no_verified_negative_control_dates", + "blocked_events": 0, + "blocked_rate": 0.0, + "evaluated_events": 12, + "exact_label_rate": 0.16666666666666666, + "exact_label_rate_deprecated": true, + "known_event_activation_rate": 0.6666666666666666, + "misses": 4, + "positive_event_recall": 0.6666666666666666, + "positive_event_recall_deprecated": true, + "strong_activation_rate": 0.16666666666666666, + "strong_hits": 2, + "total_events": 12, + "weak_hits": 6 + }, + "marriage": { + "balanced_accuracy": null, + "balanced_accuracy_blocked_reason": "no_verified_negative_control_dates", + "blocked_events": 0, + "blocked_rate": 0.0, + "evaluated_events": 11, + "exact_label_rate": 0.45454545454545453, + "exact_label_rate_deprecated": true, + "known_event_activation_rate": 0.8181818181818182, + "misses": 2, + "positive_event_recall": 0.8181818181818182, + "positive_event_recall_deprecated": true, + "strong_activation_rate": 0.45454545454545453, + "strong_hits": 5, + "total_events": 11, + "weak_hits": 4 + } + }, + "rule_version": "v2_1", + "summary": { + "balanced_accuracy": null, + "balanced_accuracy_blocked_reason": "no_verified_negative_control_dates", + "blocked_events": 0, + "blocked_rate": 0.0, + "evaluated_events": 23, + "exact_label_rate": 0.30434782608695654, + "exact_label_rate_deprecated": true, + "known_event_activation_rate": 0.7391304347826086, + "misses": 6, + "positive_event_recall": 0.7391304347826086, + "positive_event_recall_deprecated": true, + "strong_activation_rate": 0.30434782608695654, + "strong_hits": 7, + "total_events": 23, + "weak_hits": 10 + } +} diff --git a/docs/benchmark/public_real_case_annual_control_pilot_2026_07_11.json b/docs/benchmark/public_real_case_annual_control_pilot_2026_07_11.json new file mode 100644 index 00000000..285ec930 --- /dev/null +++ b/docs/benchmark/public_real_case_annual_control_pilot_2026_07_11.json @@ -0,0 +1,1167 @@ +{ + "benchmark_id": "public_real_case_annual_control_pilot_2026_07_11", + "boundary": "Controls are dates without the exact recorded target outcome. They may contain other life events. This pilot measures date ranking and false domain activation, not scientific causal validity.", + "cases": [ + { + "case_id": "trump_inauguration_2017", + "controls": [ + { + "blocked": false, + "blocked_reason": null, + "date": "2015-01-21", + "result_class": "weak_hit", + "score": 5 + }, + { + "blocked": false, + "blocked_reason": null, + "date": "2016-01-21", + "result_class": "weak_hit", + "score": 5 + }, + { + "blocked": false, + "blocked_reason": null, + "date": "2018-01-20", + "result_class": "miss", + "score": 3 + }, + { + "blocked": false, + "blocked_reason": null, + "date": "2019-01-20", + "result_class": "weak_hit", + "score": 4 + } + ], + "domain": "career", + "name": "Donald Trump", + "positive": { + "actual_label": null, + "birth_time_rating": "AA", + "blocked": false, + "case_id": "trump_inauguration_2017", + "domain": "career", + "event_date": "2017-01-20", + "evidence": { + "arudha_lord": "Mercury", + "ascendant": { + "degree": 6.8497, + "degree_in_sign": 6.8497, + "degree_in_sign_raw": 126.84968959703062, + "degree_raw": 126.8497, + "lon": 126.8497, + "lord": "Sun", + "sign": "Leo", + "sign_cn": "狮子座" + }, + "ashtakavarga_audit": { + "all_bav_valid": true, + "boundary": "Audit evidence only. SAV/BAV does not change V2.1 event scores until a fresh holdout validates it.", + "event_house_sav": { + "10": { + "level": "极吉", + "sav_score": 34, + "sign": "Taurus" + }, + "11": { + "level": "极吉", + "sav_score": 30, + "sign": "Gemini" + }, + "6": { + "level": "中等", + "sav_score": 27, + "sign": "Capricorn" + }, + "9": { + "level": "极吉", + "sav_score": 30, + "sign": "Aries" + } + }, + "method": "Ashtakavarga八分法(BPHS/PVR书例校准v2.1)", + "sav_total": 337, + "sav_valid": true, + "scoring_effect": 0, + "settings": { + "ayanamsa": 24.0953, + "node_mode": "mean" + }, + "status": "used_non_scoring", + "transit_support": { + "Jupiter": { + "bav": 6, + "sav": 28, + "sign": "Virgo" + }, + "Saturn": { + "bav": 4, + "sav": 31, + "sign": "Scorpio" + } + }, + "version": "2.1" + }, + "birth_source": { + "evidence_basis": "BC/BR in hand", + "source_grade": "primary", + "time_accuracy_rating": "AA", + "url": "https://www.astro.com/astro-databank/Trump,_Donald" + }, + "domain_varga": { + "birth_info": "1946-06-14 10:54", + "divisional_charts": { + "D10_Dasamsa": { + "Jupiter": { + "sign": "Capricorn", + "sign_cn": "摩羯座" + }, + "Ketu": { + "sign": "Aries", + "sign_cn": "白羊座" + }, + "Mars": { + "sign": "Virgo", + "sign_cn": "处女座" + }, + "Mercury": { + "sign": "Scorpio", + "sign_cn": "天蝎座" + }, + "Moon": { + "sign": "Aries", + "sign_cn": "白羊座" + }, + "Rahu": { + "sign": "Libra", + "sign_cn": "天秤座" + }, + "Saturn": { + "sign": "Pisces", + "sign_cn": "双鱼座" + }, + "Sun": { + "sign": "Libra", + "sign_cn": "天秤座" + }, + "Venus": { + "sign": "Pisces", + "sign_cn": "双鱼座" + }, + "ascendant": "Libra" + } + } + }, + "double_transit_pac": { + "cl": { + "jupiter": {}, + "saturn": {} + }, + "d1": { + "jupiter": {}, + "saturn": {} + }, + "d9": { + "jupiter": { + "D9_10宫(Pisces)": [ + { + "desc": "9宫相位", + "offset": 9, + "type": "Aspect" + } + ], + "D9_Jupiter(宫主)": [ + { + "desc": "同宫(4宫)", + "type": "Position" + }, + { + "desc": "合相(4.27°)", + "type": "Conjunction" + } + ], + "Sun_D9(Virgo)": [ + { + "desc": "同宫(4宫)", + "type": "Position" + } + ] + }, + "saturn": { + "D9_10宫(Pisces)": [ + { + "desc": "7宫相位", + "offset": 7, + "type": "Aspect" + } + ], + "D9_Jupiter(宫主)": [ + { + "desc": "10宫相位", + "offset": 10, + "type": "Aspect" + } + ], + "Sun_D9(Virgo)": [ + { + "desc": "10宫相位", + "offset": 10, + "type": "Aspect" + } + ] + } + }, + "double_transit": [ + { + "jupiter_pac": [ + { + "desc": "同宫(4宫)", + "type": "Position" + } + ], + "layer": "D9", + "saturn_pac": [ + { + "desc": "10宫相位", + "offset": 10, + "type": "Aspect" + } + ], + "strength": "strong", + "target": "Sun_D9(Virgo)" + }, + { + "jupiter_pac": [ + { + "desc": "9宫相位", + "offset": 9, + "type": "Aspect" + } + ], + "layer": "D9", + "saturn_pac": [ + { + "desc": "7宫相位", + "offset": 7, + "type": "Aspect" + } + ], + "strength": "strong", + "target": "D9_10宫(Pisces)" + }, + { + "jupiter_pac": [ + { + "desc": "同宫(4宫)", + "type": "Position" + }, + { + "desc": "合相(4.27°)", + "type": "Conjunction" + } + ], + "layer": "D9", + "saturn_pac": [ + { + "desc": "10宫相位", + "offset": 10, + "type": "Aspect" + } + ], + "strength": "strong", + "target": "D9_Jupiter(宫主)" + } + ], + "event_house": 10, + "stats": { + "chandra_lagna": "Scorpio", + "cl_jupiter_targets": [], + "cl_overlap": [], + "cl_saturn_targets": [], + "d1_jupiter_targets": [], + "d1_overlap": [], + "d1_saturn_targets": [], + "d9_ascendant": "Gemini", + "d9_jupiter_targets": [ + "D9_10宫(Pisces)", + "D9_Jupiter(宫主)", + "Sun_D9(Virgo)" + ], + "d9_overlap": [ + "D9_10宫(Pisces)", + "D9_Jupiter(宫主)", + "Sun_D9(Virgo)" + ], + "d9_saturn_targets": [ + "D9_10宫(Pisces)", + "D9_Jupiter(宫主)", + "Sun_D9(Virgo)" + ], + "event_lord_d9_sign": "Cancer" + }, + "summary": "⚠️ D9 层 Double Transit 激活,D1/CL 层未确认", + "transit_date": "2017-01-20" + }, + "event_source": { + "source_grade": "primary", + "url": "https://www.inaugural.senate.gov/58th-inaugural-ceremonies/" + }, + "functional_benefic_malefic": { + "ascendant": "Leo", + "effect_on_confidence": "高严谨模式下必须叠加功能性宫主吉凶与自然吉凶;若功能属性与自然属性冲突,应降低置信度或显式标记冲突。", + "functional_benefics": [ + "Jupiter", + "Mars", + "Sun" + ], + "functional_malefics": [ + "Mercury", + "Moon", + "Saturn", + "Venus" + ], + "functional_neutrals": [], + "owned_houses": { + "Jupiter": [ + 5, + 8 + ], + "Mars": [ + 4, + 9 + ], + "Mercury": [ + 2, + 11 + ], + "Moon": [ + 12 + ], + "Saturn": [ + 6, + 7 + ], + "Sun": [ + 1 + ], + "Venus": [ + 3, + 10 + ] + }, + "source": "strict_functional_benefic_malefic_v1", + "status": "used", + "yogakarakas": [ + "Mars" + ] + }, + "narayana": { + "ad": { + "end_age": 71.5714, + "lord": "Sun", + "sign": "Leo", + "sign_idx": 4, + "start_age": 70.2857, + "years": 1.2857 + }, + "md": { + "end_age": 80.0, + "lord": "Mercury", + "sign": "Gemini", + "sign_idx": 2, + "start_age": 68.0, + "years": 12 + }, + "pd": { + "end_age": 70.699, + "lord": "Venus", + "sign": "Libra", + "sign_idx": 6, + "start_age": 70.5612, + "years": 0.1378 + }, + "remaining_years": 9.39 + }, + "vimshottari": { + "antardasha": "Jupiter", + "mahadasha": "Jupiter" + } + }, + "expected_label": "career_status", + "matched_expected_label": false, + "name": "Donald Trump", + "outcome": "Sworn in as President of the United States", + "result_class": "miss", + "rule_version": "v2_1", + "score": 3, + "signals": [ + "narayana_lord_owns_event_house:Mercury", + "double_transit_pac_strong" + ] + }, + "positive_date": "2017-01-20", + "ranking": { + "candidate_count": 5, + "max_control_score": 5, + "positive_rank": 5, + "positive_score": 3, + "reciprocal_rank": 0.2, + "score_margin": -2, + "top_1": false, + "top_3": false + } + }, + { + "case_id": "dicaprio_oscar_2016", + "controls": [ + { + "blocked": false, + "blocked_reason": null, + "date": "2014-02-28", + "result_class": "miss", + "score": 2 + }, + { + "blocked": false, + "blocked_reason": null, + "date": "2015-02-28", + "result_class": "miss", + "score": 3 + }, + { + "blocked": false, + "blocked_reason": null, + "date": "2017-02-27", + "result_class": "strong_hit", + "score": 8 + }, + { + "blocked": false, + "blocked_reason": null, + "date": "2018-02-27", + "result_class": "strong_hit", + "score": 8 + } + ], + "domain": "career", + "name": "Leonardo DiCaprio", + "positive": { + "actual_label": null, + "birth_time_rating": "AA", + "blocked": false, + "case_id": "dicaprio_oscar_2016", + "domain": "career", + "event_date": "2016-02-28", + "evidence": { + "arudha_lord": "Saturn", + "ascendant": { + "degree": 9.6131, + "degree_in_sign": 9.6131, + "degree_in_sign_raw": 159.61308341512213, + "degree_raw": 159.6131, + "lon": 159.6131, + "lord": "Mercury", + "sign": "Virgo", + "sign_cn": "处女座" + }, + "ashtakavarga_audit": { + "all_bav_valid": true, + "boundary": "Audit evidence only. SAV/BAV does not change V2.1 event scores until a fresh holdout validates it.", + "event_house_sav": { + "10": { + "level": "极吉", + "sav_score": 30, + "sign": "Gemini" + }, + "11": { + "level": "极吉", + "sav_score": 39, + "sign": "Cancer" + }, + "6": { + "level": "极吉", + "sav_score": 31, + "sign": "Aquarius" + }, + "9": { + "level": "挑战", + "sav_score": 22, + "sign": "Taurus" + } + }, + "method": "Ashtakavarga八分法(BPHS/PVR书例校准v2.1)", + "sav_total": 337, + "sav_valid": true, + "scoring_effect": 0, + "settings": { + "ayanamsa": 24.0828, + "node_mode": "mean" + }, + "status": "used_non_scoring", + "transit_support": { + "Jupiter": { + "bav": 6, + "sav": 35, + "sign": "Leo" + }, + "Saturn": { + "bav": 4, + "sav": 29, + "sign": "Scorpio" + } + }, + "version": "2.1" + }, + "birth_source": { + "evidence_basis": "BC/BR in hand", + "source_grade": "primary", + "time_accuracy_rating": "AA", + "url": "https://www.astro.com/astro-databank/DiCaprio,_Leonardo" + }, + "domain_varga": { + "birth_info": "1974-11-11 02:47", + "divisional_charts": { + "D10_Dasamsa": { + "Jupiter": { + "sign": "Gemini", + "sign_cn": "双子座" + }, + "Ketu": { + "sign": "Gemini", + "sign_cn": "双子座" + }, + "Mars": { + "sign": "Pisces", + "sign_cn": "双鱼座" + }, + "Mercury": { + "sign": "Sagittarius", + "sign_cn": "射手座" + }, + "Moon": { + "sign": "Sagittarius", + "sign_cn": "射手座" + }, + "Rahu": { + "sign": "Sagittarius", + "sign_cn": "射手座" + }, + "Saturn": { + "sign": "Aquarius", + "sign_cn": "水瓶座" + }, + "Sun": { + "sign": "Gemini", + "sign_cn": "双子座" + }, + "Venus": { + "sign": "Gemini", + "sign_cn": "双子座" + }, + "ascendant": "Leo" + } + } + }, + "double_transit_pac": { + "cl": { + "jupiter": {}, + "saturn": { + "CL_10宫(Gemini)": [ + { + "desc": "7宫相位", + "offset": 7, + "type": "Aspect" + } + ] + } + }, + "d1": { + "jupiter": {}, + "saturn": { + "10宫(Gemini)": [ + { + "desc": "7宫相位", + "offset": 7, + "type": "Aspect" + } + ], + "Jupiter(对宫主)": [ + { + "desc": "3宫相位", + "offset": 3, + "type": "Aspect" + } + ] + } + }, + "d9": { + "jupiter": { + "D9_10宫(Sagittarius)": [ + { + "desc": "7宫相位", + "offset": 7, + "type": "Aspect" + } + ] + }, + "saturn": { + "D9_Jupiter(宫主)": [ + { + "desc": "3宫相位", + "offset": 3, + "type": "Aspect" + } + ], + "Mercury_D9(Scorpio)": [ + { + "desc": "同宫(9宫)", + "type": "Position" + }, + { + "desc": "合相(6.78°)", + "type": "Conjunction" + } + ] + } + }, + "double_transit": [], + "event_house": 10, + "stats": { + "chandra_lagna": "Virgo", + "cl_jupiter_targets": [], + "cl_overlap": [], + "cl_saturn_targets": [ + "CL_10宫(Gemini)" + ], + "d1_jupiter_targets": [], + "d1_overlap": [], + "d1_saturn_targets": [ + "10宫(Gemini)", + "Jupiter(对宫主)" + ], + "d9_ascendant": "Pisces", + "d9_jupiter_targets": [ + "D9_10宫(Sagittarius)" + ], + "d9_overlap": [], + "d9_saturn_targets": [ + "D9_Jupiter(宫主)", + "Mercury_D9(Scorpio)" + ], + "event_lord_d9_sign": "Scorpio" + }, + "summary": "❌ 无 Double Transit PAC 激活", + "transit_date": "2016-02-28" + }, + "event_source": { + "source_grade": "primary", + "url": "https://www.oscars.org/oscars/ceremonies/2016" + }, + "functional_benefic_malefic": { + "ascendant": "Virgo", + "effect_on_confidence": "高严谨模式下必须叠加功能性宫主吉凶与自然吉凶;若功能属性与自然属性冲突,应降低置信度或显式标记冲突。", + "functional_benefics": [ + "Mercury", + "Saturn", + "Venus" + ], + "functional_malefics": [ + "Mars", + "Moon", + "Sun" + ], + "functional_neutrals": [ + "Jupiter" + ], + "owned_houses": { + "Jupiter": [ + 4, + 7 + ], + "Mars": [ + 3, + 8 + ], + "Mercury": [ + 1, + 10 + ], + "Moon": [ + 11 + ], + "Saturn": [ + 5, + 6 + ], + "Sun": [ + 12 + ], + "Venus": [ + 2, + 9 + ] + }, + "source": "strict_functional_benefic_malefic_v1", + "status": "used", + "yogakarakas": [ + "Mercury" + ] + }, + "narayana": { + "ad": { + "end_age": 42.2769, + "lord": "Venus", + "sign": "Libra", + "sign_idx": 6, + "start_age": 40.2462, + "years": 2.0308 + }, + "md": { + "end_age": 46.0, + "lord": "Jupiter", + "sign": "Pisces", + "sign_idx": 11, + "start_age": 35.0, + "years": 11 + }, + "pd": { + "end_age": 41.3085, + "lord": "Saturn", + "sign": "Aquarius", + "sign_idx": 10, + "start_age": 41.1835, + "years": 0.125 + }, + "remaining_years": 4.7 + }, + "vimshottari": { + "antardasha": "Rahu", + "mahadasha": "Jupiter" + } + }, + "expected_label": "career_status", + "matched_expected_label": false, + "name": "Leonardo DiCaprio", + "outcome": "Won the Academy Award for Actor in a Leading Role", + "result_class": "miss", + "rule_version": "v2_1", + "score": 1, + "signals": [ + "Jupiter_occupies_event_house:6" + ] + }, + "positive_date": "2016-02-28", + "ranking": { + "candidate_count": 5, + "max_control_score": 8, + "positive_rank": 5, + "positive_score": 1, + "reciprocal_rank": 0.2, + "score_margin": -7, + "top_1": false, + "top_3": false + } + }, + { + "case_id": "markle_marriage_2018", + "controls": [ + { + "blocked": false, + "blocked_reason": null, + "date": "2016-05-19", + "result_class": "miss", + "score": 2 + }, + { + "blocked": false, + "blocked_reason": null, + "date": "2017-05-19", + "result_class": "miss", + "score": 2 + }, + { + "blocked": false, + "blocked_reason": null, + "date": "2019-05-19", + "result_class": "weak_hit", + "score": 5 + }, + { + "blocked": false, + "blocked_reason": null, + "date": "2020-05-18", + "result_class": "weak_hit", + "score": 4 + } + ], + "domain": "marriage", + "name": "Meghan Markle", + "positive": { + "actual_label": "legal_marriage", + "birth_time_rating": "AA", + "blocked": false, + "case_id": "markle_marriage_2018", + "domain": "marriage", + "event_date": "2018-05-19", + "evidence": { + "arudha_lord": "Sun", + "ascendant": { + "degree": 0.6845, + "degree_in_sign": 0.6845, + "degree_in_sign_raw": 90.68454836083515, + "degree_raw": 90.6845, + "lon": 90.6845, + "lord": "Moon", + "sign": "Cancer", + "sign_cn": "巨蟹座" + }, + "ashtakavarga_audit": { + "all_bav_valid": true, + "boundary": "Audit evidence only. SAV/BAV does not change V2.1 event scores until a fresh holdout validates it.", + "event_house_sav": { + "11": { + "level": "极吉", + "sav_score": 35, + "sign": "Taurus" + }, + "2": { + "level": "挑战", + "sav_score": 17, + "sign": "Leo" + }, + "5": { + "level": "中等", + "sav_score": 26, + "sign": "Scorpio" + }, + "7": { + "level": "挑战", + "sav_score": 23, + "sign": "Capricorn" + } + }, + "method": "Ashtakavarga八分法(BPHS/PVR书例校准v2.1)", + "sav_total": 337, + "sav_valid": true, + "scoring_effect": 0, + "settings": { + "ayanamsa": 24.1139, + "node_mode": "mean" + }, + "status": "used_non_scoring", + "transit_support": { + "Jupiter": { + "bav": 5, + "sav": 22, + "sign": "Libra" + }, + "Saturn": { + "bav": 2, + "sav": 31, + "sign": "Sagittarius" + } + }, + "version": "2.1" + }, + "birth_source": { + "evidence_basis": "BC/BR in hand", + "source_grade": "primary", + "time_accuracy_rating": "AA", + "url": "https://www.astro.com/astro-databank/Markle,_Meghan" + }, + "domain_varga": { + "birth_info": "1981-08-04 04:46", + "divisional_charts": { + "D9_Navamsa": { + "Jupiter": { + "sign": "Aries", + "sign_cn": "白羊座" + }, + "Ketu": { + "sign": "Pisces", + "sign_cn": "双鱼座" + }, + "Mars": { + "sign": "Pisces", + "sign_cn": "双鱼座" + }, + "Mercury": { + "sign": "Libra", + "sign_cn": "天秤座" + }, + "Moon": { + "sign": "Aries", + "sign_cn": "白羊座" + }, + "Rahu": { + "sign": "Virgo", + "sign_cn": "处女座" + }, + "Saturn": { + "sign": "Aries", + "sign_cn": "白羊座" + }, + "Sun": { + "sign": "Sagittarius", + "sign_cn": "射手座" + }, + "Venus": { + "sign": "Virgo", + "sign_cn": "处女座" + }, + "ascendant": "Cancer" + } + } + }, + "double_transit_pac": { + "cl": { + "jupiter": { + "CL_7宫(Pisces)": [ + { + "desc": "5宫相位", + "offset": 5, + "type": "Aspect" + } + ] + }, + "saturn": { + "CL_7宫(Pisces)": [ + { + "desc": "3宫相位", + "offset": 3, + "type": "Aspect" + } + ] + } + }, + "d1": { + "jupiter": {}, + "saturn": {} + }, + "d9": { + "jupiter": { + "D9_7宫(Capricorn)": [ + { + "desc": "9宫相位", + "offset": 9, + "type": "Aspect" + } + ] + }, + "saturn": { + "D9_7宫(Capricorn)": [ + { + "desc": "7宫相位", + "offset": 7, + "type": "Aspect" + } + ] + } + }, + "double_transit": [ + { + "jupiter_pac": [ + { + "desc": "9宫相位", + "offset": 9, + "type": "Aspect" + } + ], + "layer": "D9", + "saturn_pac": [ + { + "desc": "7宫相位", + "offset": 7, + "type": "Aspect" + } + ], + "strength": "strong", + "target": "D9_7宫(Capricorn)" + }, + { + "jupiter_pac": [ + { + "desc": "5宫相位", + "offset": 5, + "type": "Aspect" + } + ], + "layer": "CL", + "saturn_pac": [ + { + "desc": "3宫相位", + "offset": 3, + "type": "Aspect" + } + ], + "strength": "strong", + "target": "CL_7宫(Pisces)" + } + ], + "event_house": 7, + "stats": { + "chandra_lagna": "Virgo", + "cl_jupiter_targets": [ + "CL_7宫(Pisces)" + ], + "cl_overlap": [ + "CL_7宫(Pisces)" + ], + "cl_saturn_targets": [ + "CL_7宫(Pisces)" + ], + "d1_jupiter_targets": [], + "d1_overlap": [], + "d1_saturn_targets": [], + "d9_ascendant": "Cancer", + "d9_jupiter_targets": [ + "D9_7宫(Capricorn)" + ], + "d9_overlap": [ + "D9_7宫(Capricorn)" + ], + "d9_saturn_targets": [ + "D9_7宫(Capricorn)" + ], + "event_lord_d9_sign": "Aries" + }, + "summary": "✅ Double Transit PAC 确认: D9+CL 多层激活7宫主题", + "transit_date": "2018-05-19" + }, + "event_source": { + "source_grade": "primary", + "url": "https://www.royal.uk/wedding-duke-and-duchess-sussex" + }, + "functional_benefic_malefic": { + "ascendant": "Cancer", + "effect_on_confidence": "高严谨模式下必须叠加功能性宫主吉凶与自然吉凶;若功能属性与自然属性冲突,应降低置信度或显式标记冲突。", + "functional_benefics": [ + "Jupiter", + "Mars", + "Moon" + ], + "functional_malefics": [ + "Mercury", + "Saturn", + "Venus" + ], + "functional_neutrals": [ + "Sun" + ], + "owned_houses": { + "Jupiter": [ + 6, + 9 + ], + "Mars": [ + 5, + 10 + ], + "Mercury": [ + 3, + 12 + ], + "Moon": [ + 1 + ], + "Saturn": [ + 7, + 8 + ], + "Sun": [ + 2 + ], + "Venus": [ + 4, + 11 + ] + }, + "source": "strict_functional_benefic_malefic_v1", + "status": "used", + "yogakarakas": [ + "Mars" + ] + }, + "narayana": { + "ad": { + "end_age": 36.8684, + "lord": "Venus", + "sign": "Taurus", + "sign_idx": 1, + "start_age": 36.5921, + "years": 0.2763 + }, + "md": { + "end_age": 40.0, + "lord": "Mars", + "sign": "Scorpio", + "sign_idx": 7, + "start_age": 33.0, + "years": 7 + }, + "pd": { + "end_age": 36.8139, + "lord": "Saturn", + "sign": "Capricorn", + "sign_idx": 9, + "start_age": 36.7848, + "years": 0.0291 + }, + "remaining_years": 3.21 + }, + "vimshottari": { + "antardasha": "Saturn", + "mahadasha": "Jupiter" + } + }, + "expected_label": "legal_marriage", + "matched_expected_label": true, + "name": "Meghan Markle", + "outcome": "Married Prince Harry at St George's Chapel", + "result_class": "strong_hit", + "rule_version": "v2_1", + "score": 7, + "signals": [ + "Jupiter_domain_karaka", + "Saturn_owns_event_houses:[7]", + "active_dasha_matches_D9_7L:Saturn", + "narayana_lord_owns_event_house:Mars", + "double_transit_pac_strong" + ] + }, + "positive_date": "2018-05-19", + "ranking": { + "candidate_count": 5, + "max_control_score": 5, + "positive_rank": 1, + "positive_score": 7, + "reciprocal_rank": 1.0, + "score_margin": 2, + "top_1": true, + "top_3": true + } + } + ], + "control_band": "annual_plus_minus_one_two_years", + "control_offsets_days": [ + -730, + -365, + 365, + 730 + ], + "rule_version": "v2_1", + "summary": { + "balanced_accuracy": null, + "balanced_accuracy_blocked_reason": "controls_are_non_target_dates_not_independently_adjudicated_all-domain_non_events", + "case_count": 3, + "control_activation_rate": 0.5833333333333334, + "control_date_count": 12, + "control_strong_activation_rate": 0.16666666666666666, + "mean_reciprocal_rank": 0.4666666666666667, + "mean_score_margin": -2.3333333333333335, + "positive_top_1_rate": 0.3333333333333333, + "positive_top_3_rate": 0.3333333333333333, + "ranked_case_count": 3 + } +} diff --git a/docs/benchmark/public_real_case_negative_control_pilot_2026_07_11.json b/docs/benchmark/public_real_case_negative_control_pilot_2026_07_11.json new file mode 100644 index 00000000..25fd3a18 --- /dev/null +++ b/docs/benchmark/public_real_case_negative_control_pilot_2026_07_11.json @@ -0,0 +1,1254 @@ +{ + "benchmark_id": "public_real_case_negative_control_pilot_2026_07_11", + "boundary": "Controls are dates without the exact recorded target outcome. They may contain other life events. This pilot measures date ranking and false domain activation, not scientific causal validity.", + "cases": [ + { + "case_id": "trump_inauguration_2017", + "controls": [ + { + "blocked": false, + "blocked_reason": null, + "date": "2016-09-22", + "result_class": "strong_hit", + "score": 7 + }, + { + "blocked": false, + "blocked_reason": null, + "date": "2016-10-22", + "result_class": "strong_hit", + "score": 7 + }, + { + "blocked": false, + "blocked_reason": null, + "date": "2016-11-21", + "result_class": "miss", + "score": 3 + }, + { + "blocked": false, + "blocked_reason": null, + "date": "2016-12-21", + "result_class": "miss", + "score": 3 + }, + { + "blocked": false, + "blocked_reason": null, + "date": "2017-02-19", + "result_class": "miss", + "score": 1 + }, + { + "blocked": false, + "blocked_reason": null, + "date": "2017-03-21", + "result_class": "miss", + "score": 1 + }, + { + "blocked": false, + "blocked_reason": null, + "date": "2017-04-20", + "result_class": "miss", + "score": 1 + }, + { + "blocked": false, + "blocked_reason": null, + "date": "2017-05-20", + "result_class": "miss", + "score": 1 + } + ], + "domain": "career", + "name": "Donald Trump", + "positive": { + "actual_label": null, + "birth_time_rating": "AA", + "blocked": false, + "case_id": "trump_inauguration_2017", + "domain": "career", + "event_date": "2017-01-20", + "evidence": { + "arudha_lord": "Mercury", + "ascendant": { + "degree": 6.8497, + "degree_in_sign": 6.8497, + "degree_in_sign_raw": 126.84968959703062, + "degree_raw": 126.8497, + "lon": 126.8497, + "lord": "Sun", + "sign": "Leo", + "sign_cn": "狮子座" + }, + "ashtakavarga_audit": { + "all_bav_valid": true, + "boundary": "Audit evidence only. SAV/BAV does not change V2.1 event scores until a fresh holdout validates it.", + "event_house_sav": { + "10": { + "level": "极吉", + "sav_score": 34, + "sign": "Taurus" + }, + "11": { + "level": "极吉", + "sav_score": 30, + "sign": "Gemini" + }, + "6": { + "level": "中等", + "sav_score": 27, + "sign": "Capricorn" + }, + "9": { + "level": "极吉", + "sav_score": 30, + "sign": "Aries" + } + }, + "method": "Ashtakavarga八分法(BPHS/PVR书例校准v2.1)", + "sav_total": 337, + "sav_valid": true, + "scoring_effect": 0, + "settings": { + "ayanamsa": 24.0953, + "node_mode": "mean" + }, + "status": "used_non_scoring", + "transit_support": { + "Jupiter": { + "bav": 6, + "sav": 28, + "sign": "Virgo" + }, + "Saturn": { + "bav": 4, + "sav": 31, + "sign": "Scorpio" + } + }, + "version": "2.1" + }, + "birth_source": { + "evidence_basis": "BC/BR in hand", + "source_grade": "primary", + "time_accuracy_rating": "AA", + "url": "https://www.astro.com/astro-databank/Trump,_Donald" + }, + "domain_varga": { + "birth_info": "1946-06-14 10:54", + "divisional_charts": { + "D10_Dasamsa": { + "Jupiter": { + "sign": "Capricorn", + "sign_cn": "摩羯座" + }, + "Ketu": { + "sign": "Aries", + "sign_cn": "白羊座" + }, + "Mars": { + "sign": "Virgo", + "sign_cn": "处女座" + }, + "Mercury": { + "sign": "Scorpio", + "sign_cn": "天蝎座" + }, + "Moon": { + "sign": "Aries", + "sign_cn": "白羊座" + }, + "Rahu": { + "sign": "Libra", + "sign_cn": "天秤座" + }, + "Saturn": { + "sign": "Pisces", + "sign_cn": "双鱼座" + }, + "Sun": { + "sign": "Libra", + "sign_cn": "天秤座" + }, + "Venus": { + "sign": "Pisces", + "sign_cn": "双鱼座" + }, + "ascendant": "Libra" + } + } + }, + "double_transit_pac": { + "cl": { + "jupiter": {}, + "saturn": {} + }, + "d1": { + "jupiter": {}, + "saturn": {} + }, + "d9": { + "jupiter": { + "D9_10宫(Pisces)": [ + { + "desc": "9宫相位", + "offset": 9, + "type": "Aspect" + } + ], + "D9_Jupiter(宫主)": [ + { + "desc": "同宫(4宫)", + "type": "Position" + }, + { + "desc": "合相(4.27°)", + "type": "Conjunction" + } + ], + "Sun_D9(Virgo)": [ + { + "desc": "同宫(4宫)", + "type": "Position" + } + ] + }, + "saturn": { + "D9_10宫(Pisces)": [ + { + "desc": "7宫相位", + "offset": 7, + "type": "Aspect" + } + ], + "D9_Jupiter(宫主)": [ + { + "desc": "10宫相位", + "offset": 10, + "type": "Aspect" + } + ], + "Sun_D9(Virgo)": [ + { + "desc": "10宫相位", + "offset": 10, + "type": "Aspect" + } + ] + } + }, + "double_transit": [ + { + "jupiter_pac": [ + { + "desc": "同宫(4宫)", + "type": "Position" + }, + { + "desc": "合相(4.27°)", + "type": "Conjunction" + } + ], + "layer": "D9", + "saturn_pac": [ + { + "desc": "10宫相位", + "offset": 10, + "type": "Aspect" + } + ], + "strength": "strong", + "target": "D9_Jupiter(宫主)" + }, + { + "jupiter_pac": [ + { + "desc": "同宫(4宫)", + "type": "Position" + } + ], + "layer": "D9", + "saturn_pac": [ + { + "desc": "10宫相位", + "offset": 10, + "type": "Aspect" + } + ], + "strength": "strong", + "target": "Sun_D9(Virgo)" + }, + { + "jupiter_pac": [ + { + "desc": "9宫相位", + "offset": 9, + "type": "Aspect" + } + ], + "layer": "D9", + "saturn_pac": [ + { + "desc": "7宫相位", + "offset": 7, + "type": "Aspect" + } + ], + "strength": "strong", + "target": "D9_10宫(Pisces)" + } + ], + "event_house": 10, + "stats": { + "chandra_lagna": "Scorpio", + "cl_jupiter_targets": [], + "cl_overlap": [], + "cl_saturn_targets": [], + "d1_jupiter_targets": [], + "d1_overlap": [], + "d1_saturn_targets": [], + "d9_ascendant": "Gemini", + "d9_jupiter_targets": [ + "D9_10宫(Pisces)", + "D9_Jupiter(宫主)", + "Sun_D9(Virgo)" + ], + "d9_overlap": [ + "D9_10宫(Pisces)", + "D9_Jupiter(宫主)", + "Sun_D9(Virgo)" + ], + "d9_saturn_targets": [ + "D9_10宫(Pisces)", + "D9_Jupiter(宫主)", + "Sun_D9(Virgo)" + ], + "event_lord_d9_sign": "Cancer" + }, + "summary": "⚠️ D9 层 Double Transit 激活,D1/CL 层未确认", + "transit_date": "2017-01-20" + }, + "event_source": { + "source_grade": "primary", + "url": "https://www.inaugural.senate.gov/58th-inaugural-ceremonies/" + }, + "functional_benefic_malefic": { + "ascendant": "Leo", + "effect_on_confidence": "高严谨模式下必须叠加功能性宫主吉凶与自然吉凶;若功能属性与自然属性冲突,应降低置信度或显式标记冲突。", + "functional_benefics": [ + "Jupiter", + "Mars", + "Sun" + ], + "functional_malefics": [ + "Mercury", + "Moon", + "Saturn", + "Venus" + ], + "functional_neutrals": [], + "owned_houses": { + "Jupiter": [ + 5, + 8 + ], + "Mars": [ + 4, + 9 + ], + "Mercury": [ + 2, + 11 + ], + "Moon": [ + 12 + ], + "Saturn": [ + 6, + 7 + ], + "Sun": [ + 1 + ], + "Venus": [ + 3, + 10 + ] + }, + "source": "strict_functional_benefic_malefic_v1", + "status": "used", + "yogakarakas": [ + "Mars" + ] + }, + "narayana": { + "ad": { + "end_age": 71.5714, + "lord": "Sun", + "sign": "Leo", + "sign_idx": 4, + "start_age": 70.2857, + "years": 1.2857 + }, + "md": { + "end_age": 80.0, + "lord": "Mercury", + "sign": "Gemini", + "sign_idx": 2, + "start_age": 68.0, + "years": 12 + }, + "pd": { + "end_age": 70.699, + "lord": "Venus", + "sign": "Libra", + "sign_idx": 6, + "start_age": 70.5612, + "years": 0.1378 + }, + "remaining_years": 9.39 + }, + "vimshottari": { + "antardasha": "Jupiter", + "mahadasha": "Jupiter" + } + }, + "expected_label": "career_status", + "matched_expected_label": false, + "name": "Donald Trump", + "outcome": "Sworn in as President of the United States", + "result_class": "miss", + "rule_version": "v2_1", + "score": 3, + "signals": [ + "narayana_lord_owns_event_house:Mercury", + "double_transit_pac_strong" + ] + }, + "positive_date": "2017-01-20", + "ranking": { + "candidate_count": 9, + "max_control_score": 7, + "positive_rank": 5, + "positive_score": 3, + "reciprocal_rank": 0.2, + "score_margin": -4, + "top_1": false, + "top_3": false + } + }, + { + "case_id": "dicaprio_oscar_2016", + "controls": [ + { + "blocked": false, + "blocked_reason": null, + "date": "2015-10-31", + "result_class": "miss", + "score": 1 + }, + { + "blocked": false, + "blocked_reason": null, + "date": "2015-11-30", + "result_class": "miss", + "score": 1 + }, + { + "blocked": false, + "blocked_reason": null, + "date": "2015-12-30", + "result_class": "miss", + "score": 1 + }, + { + "blocked": false, + "blocked_reason": null, + "date": "2016-01-29", + "result_class": "miss", + "score": 1 + }, + { + "blocked": false, + "blocked_reason": null, + "date": "2016-03-29", + "result_class": "miss", + "score": 1 + }, + { + "blocked": false, + "blocked_reason": null, + "date": "2016-04-28", + "result_class": "miss", + "score": 1 + }, + { + "blocked": false, + "blocked_reason": null, + "date": "2016-05-28", + "result_class": "miss", + "score": 1 + }, + { + "blocked": false, + "blocked_reason": null, + "date": "2016-06-27", + "result_class": "miss", + "score": 1 + } + ], + "domain": "career", + "name": "Leonardo DiCaprio", + "positive": { + "actual_label": null, + "birth_time_rating": "AA", + "blocked": false, + "case_id": "dicaprio_oscar_2016", + "domain": "career", + "event_date": "2016-02-28", + "evidence": { + "arudha_lord": "Saturn", + "ascendant": { + "degree": 9.6131, + "degree_in_sign": 9.6131, + "degree_in_sign_raw": 159.61308341512213, + "degree_raw": 159.6131, + "lon": 159.6131, + "lord": "Mercury", + "sign": "Virgo", + "sign_cn": "处女座" + }, + "ashtakavarga_audit": { + "all_bav_valid": true, + "boundary": "Audit evidence only. SAV/BAV does not change V2.1 event scores until a fresh holdout validates it.", + "event_house_sav": { + "10": { + "level": "极吉", + "sav_score": 30, + "sign": "Gemini" + }, + "11": { + "level": "极吉", + "sav_score": 39, + "sign": "Cancer" + }, + "6": { + "level": "极吉", + "sav_score": 31, + "sign": "Aquarius" + }, + "9": { + "level": "挑战", + "sav_score": 22, + "sign": "Taurus" + } + }, + "method": "Ashtakavarga八分法(BPHS/PVR书例校准v2.1)", + "sav_total": 337, + "sav_valid": true, + "scoring_effect": 0, + "settings": { + "ayanamsa": 24.0828, + "node_mode": "mean" + }, + "status": "used_non_scoring", + "transit_support": { + "Jupiter": { + "bav": 6, + "sav": 35, + "sign": "Leo" + }, + "Saturn": { + "bav": 4, + "sav": 29, + "sign": "Scorpio" + } + }, + "version": "2.1" + }, + "birth_source": { + "evidence_basis": "BC/BR in hand", + "source_grade": "primary", + "time_accuracy_rating": "AA", + "url": "https://www.astro.com/astro-databank/DiCaprio,_Leonardo" + }, + "domain_varga": { + "birth_info": "1974-11-11 02:47", + "divisional_charts": { + "D10_Dasamsa": { + "Jupiter": { + "sign": "Gemini", + "sign_cn": "双子座" + }, + "Ketu": { + "sign": "Gemini", + "sign_cn": "双子座" + }, + "Mars": { + "sign": "Pisces", + "sign_cn": "双鱼座" + }, + "Mercury": { + "sign": "Sagittarius", + "sign_cn": "射手座" + }, + "Moon": { + "sign": "Sagittarius", + "sign_cn": "射手座" + }, + "Rahu": { + "sign": "Sagittarius", + "sign_cn": "射手座" + }, + "Saturn": { + "sign": "Aquarius", + "sign_cn": "水瓶座" + }, + "Sun": { + "sign": "Gemini", + "sign_cn": "双子座" + }, + "Venus": { + "sign": "Gemini", + "sign_cn": "双子座" + }, + "ascendant": "Leo" + } + } + }, + "double_transit_pac": { + "cl": { + "jupiter": {}, + "saturn": { + "CL_10宫(Gemini)": [ + { + "desc": "7宫相位", + "offset": 7, + "type": "Aspect" + } + ] + } + }, + "d1": { + "jupiter": {}, + "saturn": { + "10宫(Gemini)": [ + { + "desc": "7宫相位", + "offset": 7, + "type": "Aspect" + } + ], + "Jupiter(对宫主)": [ + { + "desc": "3宫相位", + "offset": 3, + "type": "Aspect" + } + ] + } + }, + "d9": { + "jupiter": { + "D9_10宫(Sagittarius)": [ + { + "desc": "7宫相位", + "offset": 7, + "type": "Aspect" + } + ] + }, + "saturn": { + "D9_Jupiter(宫主)": [ + { + "desc": "3宫相位", + "offset": 3, + "type": "Aspect" + } + ], + "Mercury_D9(Scorpio)": [ + { + "desc": "同宫(9宫)", + "type": "Position" + }, + { + "desc": "合相(6.78°)", + "type": "Conjunction" + } + ] + } + }, + "double_transit": [], + "event_house": 10, + "stats": { + "chandra_lagna": "Virgo", + "cl_jupiter_targets": [], + "cl_overlap": [], + "cl_saturn_targets": [ + "CL_10宫(Gemini)" + ], + "d1_jupiter_targets": [], + "d1_overlap": [], + "d1_saturn_targets": [ + "10宫(Gemini)", + "Jupiter(对宫主)" + ], + "d9_ascendant": "Pisces", + "d9_jupiter_targets": [ + "D9_10宫(Sagittarius)" + ], + "d9_overlap": [], + "d9_saturn_targets": [ + "D9_Jupiter(宫主)", + "Mercury_D9(Scorpio)" + ], + "event_lord_d9_sign": "Scorpio" + }, + "summary": "❌ 无 Double Transit PAC 激活", + "transit_date": "2016-02-28" + }, + "event_source": { + "source_grade": "primary", + "url": "https://www.oscars.org/oscars/ceremonies/2016" + }, + "functional_benefic_malefic": { + "ascendant": "Virgo", + "effect_on_confidence": "高严谨模式下必须叠加功能性宫主吉凶与自然吉凶;若功能属性与自然属性冲突,应降低置信度或显式标记冲突。", + "functional_benefics": [ + "Mercury", + "Saturn", + "Venus" + ], + "functional_malefics": [ + "Mars", + "Moon", + "Sun" + ], + "functional_neutrals": [ + "Jupiter" + ], + "owned_houses": { + "Jupiter": [ + 4, + 7 + ], + "Mars": [ + 3, + 8 + ], + "Mercury": [ + 1, + 10 + ], + "Moon": [ + 11 + ], + "Saturn": [ + 5, + 6 + ], + "Sun": [ + 12 + ], + "Venus": [ + 2, + 9 + ] + }, + "source": "strict_functional_benefic_malefic_v1", + "status": "used", + "yogakarakas": [ + "Mercury" + ] + }, + "narayana": { + "ad": { + "end_age": 42.2769, + "lord": "Venus", + "sign": "Libra", + "sign_idx": 6, + "start_age": 40.2462, + "years": 2.0308 + }, + "md": { + "end_age": 46.0, + "lord": "Jupiter", + "sign": "Pisces", + "sign_idx": 11, + "start_age": 35.0, + "years": 11 + }, + "pd": { + "end_age": 41.3085, + "lord": "Saturn", + "sign": "Aquarius", + "sign_idx": 10, + "start_age": 41.1835, + "years": 0.125 + }, + "remaining_years": 4.7 + }, + "vimshottari": { + "antardasha": "Rahu", + "mahadasha": "Jupiter" + } + }, + "expected_label": "career_status", + "matched_expected_label": false, + "name": "Leonardo DiCaprio", + "outcome": "Won the Academy Award for Actor in a Leading Role", + "result_class": "miss", + "rule_version": "v2_1", + "score": 1, + "signals": [ + "Jupiter_occupies_event_house:6" + ] + }, + "positive_date": "2016-02-28", + "ranking": { + "candidate_count": 9, + "max_control_score": 1, + "positive_rank": 9, + "positive_score": 1, + "reciprocal_rank": 0.1111111111111111, + "score_margin": 0, + "top_1": false, + "top_3": false + } + }, + { + "case_id": "markle_marriage_2018", + "controls": [ + { + "blocked": false, + "blocked_reason": null, + "date": "2018-01-19", + "result_class": "strong_hit", + "score": 7 + }, + { + "blocked": false, + "blocked_reason": null, + "date": "2018-02-18", + "result_class": "strong_hit", + "score": 7 + }, + { + "blocked": false, + "blocked_reason": null, + "date": "2018-03-20", + "result_class": "strong_hit", + "score": 7 + }, + { + "blocked": false, + "blocked_reason": null, + "date": "2018-04-19", + "result_class": "strong_hit", + "score": 7 + }, + { + "blocked": false, + "blocked_reason": null, + "date": "2018-06-18", + "result_class": "strong_hit", + "score": 7 + }, + { + "blocked": false, + "blocked_reason": null, + "date": "2018-07-18", + "result_class": "strong_hit", + "score": 7 + }, + { + "blocked": false, + "blocked_reason": null, + "date": "2018-08-17", + "result_class": "strong_hit", + "score": 7 + }, + { + "blocked": false, + "blocked_reason": null, + "date": "2018-09-16", + "result_class": "strong_hit", + "score": 7 + } + ], + "domain": "marriage", + "name": "Meghan Markle", + "positive": { + "actual_label": "legal_marriage", + "birth_time_rating": "AA", + "blocked": false, + "case_id": "markle_marriage_2018", + "domain": "marriage", + "event_date": "2018-05-19", + "evidence": { + "arudha_lord": "Sun", + "ascendant": { + "degree": 0.6845, + "degree_in_sign": 0.6845, + "degree_in_sign_raw": 90.68454836083515, + "degree_raw": 90.6845, + "lon": 90.6845, + "lord": "Moon", + "sign": "Cancer", + "sign_cn": "巨蟹座" + }, + "ashtakavarga_audit": { + "all_bav_valid": true, + "boundary": "Audit evidence only. SAV/BAV does not change V2.1 event scores until a fresh holdout validates it.", + "event_house_sav": { + "11": { + "level": "极吉", + "sav_score": 35, + "sign": "Taurus" + }, + "2": { + "level": "挑战", + "sav_score": 17, + "sign": "Leo" + }, + "5": { + "level": "中等", + "sav_score": 26, + "sign": "Scorpio" + }, + "7": { + "level": "挑战", + "sav_score": 23, + "sign": "Capricorn" + } + }, + "method": "Ashtakavarga八分法(BPHS/PVR书例校准v2.1)", + "sav_total": 337, + "sav_valid": true, + "scoring_effect": 0, + "settings": { + "ayanamsa": 24.1139, + "node_mode": "mean" + }, + "status": "used_non_scoring", + "transit_support": { + "Jupiter": { + "bav": 5, + "sav": 22, + "sign": "Libra" + }, + "Saturn": { + "bav": 2, + "sav": 31, + "sign": "Sagittarius" + } + }, + "version": "2.1" + }, + "birth_source": { + "evidence_basis": "BC/BR in hand", + "source_grade": "primary", + "time_accuracy_rating": "AA", + "url": "https://www.astro.com/astro-databank/Markle,_Meghan" + }, + "domain_varga": { + "birth_info": "1981-08-04 04:46", + "divisional_charts": { + "D9_Navamsa": { + "Jupiter": { + "sign": "Aries", + "sign_cn": "白羊座" + }, + "Ketu": { + "sign": "Pisces", + "sign_cn": "双鱼座" + }, + "Mars": { + "sign": "Pisces", + "sign_cn": "双鱼座" + }, + "Mercury": { + "sign": "Libra", + "sign_cn": "天秤座" + }, + "Moon": { + "sign": "Aries", + "sign_cn": "白羊座" + }, + "Rahu": { + "sign": "Virgo", + "sign_cn": "处女座" + }, + "Saturn": { + "sign": "Aries", + "sign_cn": "白羊座" + }, + "Sun": { + "sign": "Sagittarius", + "sign_cn": "射手座" + }, + "Venus": { + "sign": "Virgo", + "sign_cn": "处女座" + }, + "ascendant": "Cancer" + } + } + }, + "double_transit_pac": { + "cl": { + "jupiter": { + "CL_7宫(Pisces)": [ + { + "desc": "5宫相位", + "offset": 5, + "type": "Aspect" + } + ] + }, + "saturn": { + "CL_7宫(Pisces)": [ + { + "desc": "3宫相位", + "offset": 3, + "type": "Aspect" + } + ] + } + }, + "d1": { + "jupiter": {}, + "saturn": {} + }, + "d9": { + "jupiter": { + "D9_7宫(Capricorn)": [ + { + "desc": "9宫相位", + "offset": 9, + "type": "Aspect" + } + ] + }, + "saturn": { + "D9_7宫(Capricorn)": [ + { + "desc": "7宫相位", + "offset": 7, + "type": "Aspect" + } + ] + } + }, + "double_transit": [ + { + "jupiter_pac": [ + { + "desc": "9宫相位", + "offset": 9, + "type": "Aspect" + } + ], + "layer": "D9", + "saturn_pac": [ + { + "desc": "7宫相位", + "offset": 7, + "type": "Aspect" + } + ], + "strength": "strong", + "target": "D9_7宫(Capricorn)" + }, + { + "jupiter_pac": [ + { + "desc": "5宫相位", + "offset": 5, + "type": "Aspect" + } + ], + "layer": "CL", + "saturn_pac": [ + { + "desc": "3宫相位", + "offset": 3, + "type": "Aspect" + } + ], + "strength": "strong", + "target": "CL_7宫(Pisces)" + } + ], + "event_house": 7, + "stats": { + "chandra_lagna": "Virgo", + "cl_jupiter_targets": [ + "CL_7宫(Pisces)" + ], + "cl_overlap": [ + "CL_7宫(Pisces)" + ], + "cl_saturn_targets": [ + "CL_7宫(Pisces)" + ], + "d1_jupiter_targets": [], + "d1_overlap": [], + "d1_saturn_targets": [], + "d9_ascendant": "Cancer", + "d9_jupiter_targets": [ + "D9_7宫(Capricorn)" + ], + "d9_overlap": [ + "D9_7宫(Capricorn)" + ], + "d9_saturn_targets": [ + "D9_7宫(Capricorn)" + ], + "event_lord_d9_sign": "Aries" + }, + "summary": "✅ Double Transit PAC 确认: D9+CL 多层激活7宫主题", + "transit_date": "2018-05-19" + }, + "event_source": { + "source_grade": "primary", + "url": "https://www.royal.uk/wedding-duke-and-duchess-sussex" + }, + "functional_benefic_malefic": { + "ascendant": "Cancer", + "effect_on_confidence": "高严谨模式下必须叠加功能性宫主吉凶与自然吉凶;若功能属性与自然属性冲突,应降低置信度或显式标记冲突。", + "functional_benefics": [ + "Jupiter", + "Mars", + "Moon" + ], + "functional_malefics": [ + "Mercury", + "Saturn", + "Venus" + ], + "functional_neutrals": [ + "Sun" + ], + "owned_houses": { + "Jupiter": [ + 6, + 9 + ], + "Mars": [ + 5, + 10 + ], + "Mercury": [ + 3, + 12 + ], + "Moon": [ + 1 + ], + "Saturn": [ + 7, + 8 + ], + "Sun": [ + 2 + ], + "Venus": [ + 4, + 11 + ] + }, + "source": "strict_functional_benefic_malefic_v1", + "status": "used", + "yogakarakas": [ + "Mars" + ] + }, + "narayana": { + "ad": { + "end_age": 36.8684, + "lord": "Venus", + "sign": "Taurus", + "sign_idx": 1, + "start_age": 36.5921, + "years": 0.2763 + }, + "md": { + "end_age": 40.0, + "lord": "Mars", + "sign": "Scorpio", + "sign_idx": 7, + "start_age": 33.0, + "years": 7 + }, + "pd": { + "end_age": 36.8139, + "lord": "Saturn", + "sign": "Capricorn", + "sign_idx": 9, + "start_age": 36.7848, + "years": 0.0291 + }, + "remaining_years": 3.21 + }, + "vimshottari": { + "antardasha": "Saturn", + "mahadasha": "Jupiter" + } + }, + "expected_label": "legal_marriage", + "matched_expected_label": true, + "name": "Meghan Markle", + "outcome": "Married Prince Harry at St George's Chapel", + "result_class": "strong_hit", + "rule_version": "v2_1", + "score": 7, + "signals": [ + "Jupiter_domain_karaka", + "Saturn_owns_event_houses:[7]", + "active_dasha_matches_D9_7L:Saturn", + "narayana_lord_owns_event_house:Mars", + "double_transit_pac_strong" + ] + }, + "positive_date": "2018-05-19", + "ranking": { + "candidate_count": 9, + "max_control_score": 7, + "positive_rank": 9, + "positive_score": 7, + "reciprocal_rank": 0.1111111111111111, + "score_margin": 0, + "top_1": false, + "top_3": false + } + } + ], + "control_offsets_days": [ + -120, + -90, + -60, + -30, + 30, + 60, + 90, + 120 + ], + "rule_version": "v2_1", + "summary": { + "balanced_accuracy": null, + "balanced_accuracy_blocked_reason": "controls_are_non_target_dates_not_independently_adjudicated_all-domain_non_events", + "case_count": 3, + "control_activation_rate": 0.4166666666666667, + "control_date_count": 24, + "control_strong_activation_rate": 0.4166666666666667, + "mean_reciprocal_rank": 0.14074074074074075, + "mean_score_margin": -1.3333333333333333, + "positive_top_1_rate": 0.0, + "positive_top_3_rate": 0.0, + "ranked_case_count": 3 + } +} diff --git a/docs/research/public_real_case_calibration_release_2026_07_12.md b/docs/research/public_real_case_calibration_release_2026_07_12.md new file mode 100644 index 00000000..f1e4c99a --- /dev/null +++ b/docs/research/public_real_case_calibration_release_2026_07_12.md @@ -0,0 +1,33 @@ +# Public Real-Case Calibration Release Boundary + +Date: 2026-07-12 + +This release publishes reproducible public-case calibration safeguards, not a +claim of predictive accuracy. It contains no user birth data, private feedback, +or private event history. + +## Included Evidence + +- V2.1 scoring correction: `public_real_case_23_case_v21_corrected_observation_2026_07_11.json` +- Date-control pilot: `public_real_case_negative_control_pilot_2026_07_11.json` +- Annual-control pilot: `public_real_case_annual_control_pilot_2026_07_11.json` +- Human-readable correction and control reports in this directory. + +## Current Interpretation Boundary + +- V2.1 removes duplicate MD/AD scoring and labels legacy precision-like fields + as deprecated. +- SAV/BAV remains descriptive, non-scoring evidence. +- The date-control and annual-control pilots do not support exact-day or + exact-month claims from the current replay score. +- The calibration corpus uses known positive events and partially adjudicated + control dates. It does not establish specificity, balanced accuracy, or + general predictive accuracy. +- External same-chart parity is separate. Local replay must not be described as + VedAstro, PyJHora/JHora, or jyotishganit verified until all required raw + oracle fields are imported and compared. + +## Excluded Local Material + +Earlier V1/V2 snapshots, probe outputs, comparison intermediates, and planning +files remain local. They are retained for audit but are not release evidence. diff --git a/docs/research/public_real_case_negative_control_pilot_2026_07_11.md b/docs/research/public_real_case_negative_control_pilot_2026_07_11.md new file mode 100644 index 00000000..6a2c753d --- /dev/null +++ b/docs/research/public_real_case_negative_control_pilot_2026_07_11.md @@ -0,0 +1,45 @@ +# 真实案例负样本日期排序 Pilot(2026-07-11) + +## 设计 + +对 Trump 就职、DiCaprio 奥斯卡、Markle 婚姻三个 AA 案例,分别取真实事件日前后 `30/60/90/120` 天,共 8 个控制日期。控制日期只保证没有发生该项精确目标事件,不保证没有其他人生事件。 + +统一使用 V2.1;SAV/BAV 仍为非评分证据。并列采用保守排名:控制日期与真实日期同分时,控制日期排在真实日期前。 + +## 汇总 + +- 真实日期 Top-1:`0/3`。 +- 真实日期 Top-3:`0/3`。 +- Mean Reciprocal Rank:`0.1407`。 +- 平均真实日分数边际:`-1.3333`。 +- 24 个控制日期中,`10/24 = 41.67%` 达到 activation 阈值。 +- `10/24 = 41.67%` 达到 strong 阈值。 + +## 逐案 + +| 案例 | 真实分 | 最高控制分 | 真实日排名/9 | 结论 | +|---|---:|---:|---:|---| +| Trump 2017 就职 | 3 | 7 | 5 | 两个更早控制日期反而 strong | +| DiCaprio 2016 奥斯卡 | 1 | 1 | 9 | 九个日期全部同分,完全无日期区分力 | +| Markle 2018 婚姻 | 7 | 7 | 9 | 真实日和八个控制日期全部 strong | + +## 裁决 + +当前评分器主要识别持续数月或更长的 Dasha、分盘与慢行星背景,不能从该背景中确定具体月日。进一步使用 `±1年/±2年` 的 12 个年度控制日期后,真实日 Top-1/Top-3 也只有 `33.33%`:Trump、DiCaprio 均排最后,只有 Markle 婚姻排第一。 + +由此新增硬门: + +- `exact_day`:blocked。 +- `exact_month_from_current_replay_score`:blocked。 +- 当前最大支持精度:`unvalidated_broad_window`。 +- 事业 timing:blocked。 +- 婚姻宽窗口:partial candidate,仍需更多样本。 + +月级或日级输出只有在 PD/PrAD、Mudda/Varshaphala、精确 KP cusp、快速过境加入后,并在新的控制日期排名中通过,才能解除门控。 + +本 pilot 仍不能计算完整 balanced accuracy,因为控制日期未被独立核验为“所有同领域事件均未发生”。但它足以反证当前分数具有精确日期识别能力。 + +机器报告: + +- `docs/benchmark/public_real_case_negative_control_pilot_2026_07_11.json` +- `docs/benchmark/public_real_case_annual_control_pilot_2026_07_11.json` diff --git a/docs/research/public_real_case_v21_sav_correction_2026_07_11.md b/docs/research/public_real_case_v21_sav_correction_2026_07_11.md new file mode 100644 index 00000000..79634feb --- /dev/null +++ b/docs/research/public_real_case_v21_sav_correction_2026_07_11.md @@ -0,0 +1,42 @@ +# 真实案例 V2.1 计分修正与 SAV/BAV 审计(2026-07-11) + +## 修正内容 + +1. MD 与 AD 为同一颗星时,不再重复执行整套宫位、落宫和 karaka 加分。 +2. 保留旧字段兼容,但新增真实指标名: + - `known_event_activation_rate` + - `strong_activation_rate` +3. `positive_event_recall` 与 `exact_label_rate` 标记为 deprecated。 +4. 23 案例全部加入 D1 SAV/BAV、事件宫 SAV、事件日 Jupiter/Saturn 过境 SAV/BAV;本轮不参与评分。 + +## V2.1 观察结果 + +- 总体:`7 strong + 10 weak + 6 miss`。 +- known-event activation:`17/23 = 0.7391`。 +- strong activation:`7/23 = 0.3043`。 +- 事业:activation `8/12 = 0.6667`,strong `2/12 = 0.1667`。 +- 婚姻:activation `9/11 = 0.8182`,strong `5/11 = 0.4545`。 + +旧 23 案例 strong activation 为 `9/23 = 0.3913`。去重后降为 `7/23 = 0.3043`,确认重复 MD/AD 计分曾抬高强命中数量。 + +## SAV/BAV 描述性结果 + +| 分组 | 事件宫 SAV 均值 | Jupiter/Saturn 过境 SAV 均值 | 过境星自身 BAV 均值 | +|---|---:|---:|---:| +| 已激活 strong/weak | 28.809 | 28.735 | 3.882 | +| miss | 30.375 | 27.583 | 3.917 | + +当前正样本中,miss 的事件宫 SAV 均值反而高于已激活组,过境 BAV 几乎没有差异。因此: + +- SAV 不能直接作为“高分即发生事件”的加分器。 +- SAV 更适合作为本命承载力背景,与大运、宫主、BAV 和过境共同分析。 +- 是否具有日期区分能力,必须用同人物同年度负样本验证。 + +## 技术边界 + +- 本仓 SAV 总数和七曜 BAV 总数不变量已由 `tests/test_ashtakavarga_invariants.py` 守门。 +- 本轮 23/23 案例 SAV/BAV 状态为 `used_non_scoring`。 +- 尚未完成 JHora/PyJHora 的逐星座 SAV/BAV raw parity。 +- 本结果使用已见正事件,只是校正观察,不构成 V2.1 晋级验证。 + +机器报告:`docs/benchmark/public_real_case_23_case_v21_corrected_observation_2026_07_11.json`。 From c2bc58ce1ed3b2230f2801ae5c6020706ee1fb38 Mon Sep 17 00:00:00 2001 From: 732642856 <732642856@qq.com> Date: Sun, 12 Jul 2026 12:51:08 +0800 Subject: [PATCH 04/61] add guided skill and web consultation surfaces --- SKILL.md | 16 ++++ mcp_server.py | 21 ++++- scripts/jyotish_api_server.py | 75 ++++++++++++++- scripts/skill_experience.py | 135 +++++++++++++++++++++++++++ tests/test_api_async_job_contract.py | 59 ++++++++++++ tests/test_skill_experience.py | 82 ++++++++++++++++ web/evidence_packet.html | 26 ++++++ web/rectification.html | 13 +++ 8 files changed, 425 insertions(+), 2 deletions(-) create mode 100644 scripts/skill_experience.py create mode 100644 tests/test_api_async_job_contract.py create mode 100644 tests/test_skill_experience.py create mode 100644 web/evidence_packet.html create mode 100644 web/rectification.html diff --git a/SKILL.md b/SKILL.md index ee44677b..25c92818 100644 --- a/SKILL.md +++ b/SKILL.md @@ -78,6 +78,22 @@ python3 scripts/user_invocation_acceptance_check.py 该命令必须返回 `"status": "pass"`,并显式列出 VedAstro / PyJHora-JHora / jyotishganit 的可用、partial 或 blocked 状态;否则不得声称云端 Git 仓库调用已可高质量使用。 +### 首次调用与降级合同 + +普通用户不必先理解 API、MCP、分盘或校时方法。Skill/MCP 首次调用必须先使用 +`skill_onboarding`:缺出生字段时只收集日期、时间、经纬度;出生时间有误差时返回 +`rectification` 的选择题问卷;时间明确时进入 `direct_chart`。不得要求用户先提交长篇 +人生事件表。 + +安装或运行异常时调用 `skill_doctor`。它只报告本地资产与外部适配器 readiness,不得把 +adapter available 解释为已完成 VedAstro、PyJHora/JHora 或 jyotishganit raw-oracle 校验。 + +每个工作流结果必须包含 `execution_status`: + +- `official_verified`:仅此状态可说 VedAstro 官方 raw evidence 已被使用; +- `official_blocked`:官方请求失败、额度/网络/超时受阻; +- `local_fallback`:本地计算继续可用,但不能称为官方云端闭环。 + **强制工作流**(完整规范 → `references/ai-reading-workflow-prompt.md` v5.1.0): 0. **阶段负一**:问题类型路由(事业/婚恋/财务/应期/历史验证/综合解盘)→ 必须先读 `references/strict-workflow-router.md`,按对应 strict checklist 执行;用户不需要主动点名高级技法。 diff --git a/mcp_server.py b/mcp_server.py index c26d56db..4989f062 100644 --- a/mcp_server.py +++ b/mcp_server.py @@ -45,6 +45,7 @@ from mcp.server.fastmcp import FastMCP from functional_benefics import derive_functional_benefic_malefic from vedastro_priority import official_snapshot_evidence from unified_consultation_orchestrator import UnifiedConsultationOrchestrator +from skill_experience import build_skill_doctor, build_skill_onboarding, summarize_execution_status load_local_env(SCRIPT_DIR) @@ -718,7 +719,7 @@ def _execute_mcp_consultation_workflow( from jyotish_api_server import JyotishAPIHandler, execute_consultation_workflow handler = JyotishAPIHandler.__new__(JyotishAPIHandler) - return execute_consultation_workflow( + result = execute_consultation_workflow( handler, body={ "question": question, @@ -740,6 +741,9 @@ def _execute_mcp_consultation_workflow( }, surface="skill_mcp", ) + if isinstance(result, dict): + result["execution_status"] = summarize_execution_status(result) + return result def _safe_get(data: Dict[str, Any], *path: str) -> Any: @@ -4353,6 +4357,21 @@ def life_event_graph( } +# ============================================================================ +# Skill experience tools + +@mcp.tool() +def skill_onboarding(payload: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: + """Return the minimal next input or active rectification question set.""" + return build_skill_onboarding(payload) + + +@mcp.tool() +def skill_doctor() -> Dict[str, Any]: + """Check local Skill assets and external adapter readiness.""" + return build_skill_doctor() + + # ============================================================================ # Resources # ============================================================================ diff --git a/scripts/jyotish_api_server.py b/scripts/jyotish_api_server.py index ed23e3e1..ac3645d2 100644 --- a/scripts/jyotish_api_server.py +++ b/scripts/jyotish_api_server.py @@ -32,6 +32,18 @@ try: from scripts.unified_consultation_orchestrator import UnifiedConsultationOrchestrator except ModuleNotFoundError: # pragma: no cover - script execution path from unified_consultation_orchestrator import UnifiedConsultationOrchestrator +try: + from scripts.skill_experience import ( + build_rectification_questionnaire, + score_rectification_answers, + summarize_execution_status, + ) +except ModuleNotFoundError: # pragma: no cover - script execution path + from skill_experience import ( + build_rectification_questionnaire, + score_rectification_answers, + summarize_execution_status, + ) try: from scripts.western_oracle_adapter import build_packet_from_oracle_payload except ModuleNotFoundError: # pragma: no cover - script execution path @@ -54,6 +66,22 @@ _ASYNC_JOB_EXECUTOR = ThreadPoolExecutor( _ASYNC_JOB_CAPACITY = threading.BoundedSemaphore(_ASYNC_JOB_WORKERS + _ASYNC_JOB_QUEUE_SIZE) +def build_evidence_packet_view(job_record: dict | None) -> dict: + """Public, token-protected job view. Excludes prompt internals and raw input.""" + job_record = job_record or {} + result = job_record.get('result') + result = result if isinstance(result, dict) else {} + return { + 'scope': 'evidence_packet_view', + 'job_id': job_record.get('job_id'), + 'status': job_record.get('status', 'unknown'), + 'execution_status': summarize_execution_status(result), + 'machine_evidence_packet': result.get('machine_evidence_packet') or {}, + 'technique_audit': result.get('technique_audit') or result.get('technique_audit_table') or [], + 'warnings': result.get('warnings') or [], + } + + def _submit_background_job(callback): if not _ASYNC_JOB_CAPACITY.acquire(blocking=False): raise JobQueueFull('Async job queue is full') @@ -900,6 +928,17 @@ class JyotishAPIHandler(BaseHTTPRequestHandler): def _error_json(self, message, status=500, error_code='ERR_INTERNAL'): self._json({'success': False, 'error': message, 'error_code': error_code}, status) + def _html(self, content, status=200): + encoded = content.encode('utf-8') + self.send_response(status) + self.send_header('Content-Type', 'text/html; charset=utf-8') + self._send_cors_headers() + self.send_header('X-Content-Type-Options', 'nosniff') + self.send_header('Cache-Control', 'no-store') + self.send_header('Content-Length', str(len(encoded))) + self.end_headers() + self.wfile.write(encoded) + def _send_cors_headers(self): origin = self.headers.get('Origin') allowed = getattr(self.server, 'allowed_origins', DEFAULT_ALLOWED_ORIGINS) @@ -979,7 +1018,19 @@ class JyotishAPIHandler(BaseHTTPRequestHandler): path = urlparse(self.path).path try: self._enforce_request_security() - if path == '/api/health': + if path == '/evidence': + page = Path(REPO_ROOT) / 'web' / 'evidence_packet.html' + if not page.is_file(): + self._error_json('Evidence Packet page unavailable', 404, 'ERR_NOT_FOUND') + else: + self._html(page.read_text(encoding='utf-8')) + elif path == '/rectification': + page = Path(REPO_ROOT) / 'web' / 'rectification.html' + if not page.is_file(): + self._error_json('Rectification page unavailable', 404, 'ERR_NOT_FOUND') + else: + self._html(page.read_text(encoding='utf-8')) + elif path == '/api/health': swisseph_available = False swisseph_version = None try: @@ -1030,6 +1081,20 @@ class JyotishAPIHandler(BaseHTTPRequestHandler): self._error_json('Not found', 404, 'ERR_NOT_FOUND') else: self._json(result) + elif path.startswith('/api/evidence_packet/chart/'): + job_id = path.rsplit('/', 1)[-1] + result = self._get_chart_job(job_id) + if result is None: + self._error_json('Not found', 404, 'ERR_NOT_FOUND') + else: + self._json(build_evidence_packet_view(result)) + elif path.startswith('/api/evidence_packet/high_rigor_workflow/'): + job_id = path.rsplit('/', 1)[-1] + result = self._get_high_rigor_job(job_id) + if result is None: + self._error_json('Not found', 404, 'ERR_NOT_FOUND') + else: + self._json(build_evidence_packet_view(result)) elif path == '/api/real_case_revalidation': self._json(self._real_case_revalidation()) else: @@ -1146,6 +1211,14 @@ class JyotishAPIHandler(BaseHTTPRequestHandler): elif path == '/api/aspects': result = self._compute_aspects(body) self._json(result) + elif path == '/api/rectification/questionnaire': + self._json(build_rectification_questionnaire(body)) + elif path == '/api/rectification/answers': + questionnaire = body.get('questionnaire') + answers = body.get('answers') + if not isinstance(questionnaire, dict) or not isinstance(answers, dict): + raise BadRequest('questionnaire and answers must be JSON objects') + self._json(score_rectification_answers(questionnaire, answers)) elif path == '/api/rectification_gate': result = self._compute_rectification_gate(body) self._json(result) diff --git a/scripts/skill_experience.py b/scripts/skill_experience.py new file mode 100644 index 00000000..06f67834 --- /dev/null +++ b/scripts/skill_experience.py @@ -0,0 +1,135 @@ +"""Stable user-facing contracts shared by Skill and MCP entry points.""" + +from __future__ import annotations + +from pathlib import Path +from typing import Any + +from scripts.active_rectification_questions import build_questionnaire, score_answers +from scripts.diagnose_external_engine_adapters import build_report as adapter_report + + +ROOT = Path(__file__).resolve().parents[1] +_REQUIRED_BIRTH_FIELDS = ("year", "month", "day", "hour", "minute", "lat", "lon") + + +def _missing_birth_fields(payload: dict[str, Any]) -> list[str]: + return [field for field in _REQUIRED_BIRTH_FIELDS if payload.get(field) is None] + + +def build_skill_onboarding(payload: dict[str, Any] | None = None) -> dict[str, Any]: + """Return the next minimal user action; never infer missing birth inputs.""" + payload = payload or {} + missing = _missing_birth_fields(payload) + if missing: + return { + "scope": "skill_onboarding", + "status": "needs_birth_data", + "entry_mode": "pending", + "missing_fields": missing, + "next_action": "collect_birth_data", + "input_template": { + "year": "YYYY", "month": "MM", "day": "DD", + "hour": "0-23", "minute": "0-59", "lat": "decimal", "lon": "decimal", + "time_uncertainty_minutes": "optional; use when birth time is approximate", + "question": "optional; career, relationship, wealth, health, general", + }, + } + + uncertainty = int(payload.get("time_uncertainty_minutes") or 0) + if uncertainty > 0: + birth_time = ( + f"{int(payload['year']):04d}-{int(payload['month']):02d}-{int(payload['day']):02d} " + f"{int(payload['hour']):02d}:{int(payload['minute']):02d}" + ) + questionnaire = build_questionnaire(birth_time, uncertainty_minutes=uncertainty) + first_question = questionnaire.get("questions", [{}])[0] + return { + "scope": "skill_onboarding", + "status": "ready", + "entry_mode": "rectification", + "next_action": "run_rectification_questionnaire", + "first_question": first_question, + "questionnaire": questionnaire, + } + + return { + "scope": "skill_onboarding", + "status": "ready", + "entry_mode": "direct_chart", + "next_action": "run_consultation_workflow", + "question": str(payload.get("question") or ""), + } + + +def build_rectification_questionnaire(payload: dict[str, Any]) -> dict[str, Any]: + """Build the active-choice questionnaire from a minimal approximate time.""" + required = ("year", "month", "day", "hour", "minute") + missing = [field for field in required if payload.get(field) is None] + if missing: + raise ValueError(f"missing rectification fields: {', '.join(missing)}") + birth_time = ( + f"{int(payload['year']):04d}-{int(payload['month']):02d}-{int(payload['day']):02d} " + f"{int(payload['hour']):02d}:{int(payload['minute']):02d}" + ) + uncertainty = max(int(payload.get("time_uncertainty_minutes") or 30), 1) + step = max(int(payload.get("step_minutes") or 1), 1) + return build_questionnaire(birth_time, uncertainty_minutes=uncertainty, step_minutes=step) + + +def score_rectification_answers(questionnaire: dict[str, Any], answers: dict[str, str]) -> dict[str, Any]: + """Score user choices; preserves the boundary against false minute precision.""" + return score_answers(questionnaire, answers or {}) + + +def build_skill_doctor() -> dict[str, Any]: + """Expose readiness, not an unsupported promise that all engines are usable.""" + assets = { + "skill_instructions": (ROOT / "SKILL.md").is_file(), + "mcp_server": (ROOT / "mcp_server.py").is_file(), + "native_engine": (ROOT / "scripts" / "jyotish_engine.py").is_file(), + "unified_orchestrator": (ROOT / "scripts" / "unified_consultation_orchestrator.py").is_file(), + } + adapters = adapter_report() + adapter_status = adapters.get("status", "blocked") + return { + "scope": "skill_doctor", + "status": "ready" if all(assets.values()) and adapter_status == "ready" else "degraded", + "core_assets": assets, + "external_engine_adapters": adapters, + "boundary": "Readiness only. An available adapter is not external raw-oracle verification.", + } + + +def _vedastro_status(result: dict[str, Any]) -> str: + engines = result.get("external_engine_cross_validation") + if isinstance(engines, dict): + engines = engines.get("engines") + vedastro = engines.get("VedAstro") if isinstance(engines, dict) else None + if isinstance(vedastro, dict): + return str(vedastro.get("status") or "") + return "" + + +def summarize_execution_status(result: dict[str, Any] | None) -> dict[str, Any]: + """Normalize official/local evidence state for every conversational surface.""" + result = result or {} + fallback_reason = str(result.get("fallback_reason") or "") + vedastro = _vedastro_status(result) + raw_status = str(result.get("official_evidence_status") or "") + if raw_status == "official_verified" or vedastro == "official_verified": + official, source = "official_verified", "official_raw" + elif fallback_reason or vedastro in {"local_fallback", "official_blocked", "blocked"}: + official, source = "official_blocked", "local_fallback" + else: + official, source = "official_not_requested", "local_or_unverified" + return { + "scope": "execution_status", + "official_evidence_status": official, + "calculation_source": source, + "fallback_reason": fallback_reason or None, + "allowed_claims": ["official_verified", "official_blocked", "local_fallback"], + "claim_boundary": ( + "Only official_verified permits claims that VedAstro official raw evidence was used." + ), + } diff --git a/tests/test_api_async_job_contract.py b/tests/test_api_async_job_contract.py new file mode 100644 index 00000000..4013344d --- /dev/null +++ b/tests/test_api_async_job_contract.py @@ -0,0 +1,59 @@ +import json +from pathlib import Path + +from scripts import jyotish_api_server as api + + +def test_evidence_packet_view_exposes_only_auditable_result_sections(): + packet = api.build_evidence_packet_view({ + "job_id": "job_1", + "status": "completed", + "result": { + "fallback_reason": "VedAstro official snapshot blocked: timeout", + "machine_evidence_packet": {"status": "draft", "metadata": {"capture_id": "x"}}, + "technique_audit": [{"technique": "D9", "status": "used"}], + "ai_prompt_pack": {"prompt_zh": "internal prompt"}, + }, + }) + + assert packet["job_id"] == "job_1" + assert packet["execution_status"]["official_evidence_status"] == "official_blocked" + assert packet["machine_evidence_packet"]["metadata"]["capture_id"] == "x" + assert "ai_prompt_pack" not in packet + + +def test_async_job_route_extracts_id_before_loading(monkeypatch, tmp_path): + record = {"job_id": "chart_abc", "status": "completed", "result": {}} + monkeypatch.setattr(api, "_load_async_job_record", lambda *args, **kwargs: record) + + handler = api.JyotishAPIHandler.__new__(api.JyotishAPIHandler) + handler.path = "/api/chart/jobs/chart_abc" + handler.headers = {"Origin": ""} + handler._enforce_request_security = lambda: None + captured = {} + handler._json = lambda data, status=200: captured.update(data=data, status=status) + handler._error_json = lambda message, status=500, error_code="ERR_INTERNAL": captured.update(error=error_code, status=status) + handler._job_access_token = lambda: "token" + + handler.do_GET() + + assert captured["status"] == 200 + assert captured["data"]["job_id"] == "chart_abc" + + +def test_evidence_packet_page_is_present_and_does_not_embed_birth_data(): + page = Path(api.REPO_ROOT) / "web" / "evidence_packet.html" + source = page.read_text(encoding="utf-8") + + assert "Evidence Packet" in source + assert "access token" in source + assert "birth" not in source.lower() + + +def test_rectification_page_uses_choice_questionnaire_contract(): + page = Path(api.REPO_ROOT) / "web" / "rectification.html" + source = page.read_text(encoding="utf-8") + + assert "/api/rectification/questionnaire" in source + assert "/api/rectification/answers" in source + assert "候选簇排序" in source diff --git a/tests/test_skill_experience.py b/tests/test_skill_experience.py new file mode 100644 index 00000000..7406995e --- /dev/null +++ b/tests/test_skill_experience.py @@ -0,0 +1,82 @@ +from scripts.skill_experience import ( + build_rectification_questionnaire, + build_skill_doctor, + build_skill_onboarding, + score_rectification_answers, + summarize_execution_status, +) + + +def test_onboarding_requests_only_missing_birth_fields(): + packet = build_skill_onboarding({"year": 1993, "month": 4, "day": 17}) + + assert packet["status"] == "needs_birth_data" + assert packet["entry_mode"] == "pending" + assert packet["missing_fields"] == ["hour", "minute", "lat", "lon"] + assert packet["next_action"] == "collect_birth_data" + + +def test_onboarding_selects_rectification_for_uncertain_time(): + packet = build_skill_onboarding({ + "year": 1993, + "month": 4, + "day": 17, + "hour": 14, + "minute": 49, + "lat": 36.68, + "lon": 114.35, + "time_uncertainty_minutes": 20, + }) + + assert packet["status"] == "ready" + assert packet["entry_mode"] == "rectification" + assert packet["next_action"] == "run_rectification_questionnaire" + assert packet["first_question"] + + +def test_execution_status_makes_official_fallback_machine_readable(): + status = summarize_execution_status({ + "fallback_reason": "VedAstro official snapshot blocked: official_snapshot_budget_exhausted", + "external_engine_cross_validation": { + "engines": {"VedAstro": {"status": "local_fallback"}} + }, + }) + + assert status["official_evidence_status"] == "official_blocked" + assert status["calculation_source"] == "local_fallback" + assert status["fallback_reason"] == "VedAstro official snapshot blocked: official_snapshot_budget_exhausted" + assert "official_verified" in status["allowed_claims"] + + +def test_doctor_has_machine_readable_core_and_adapter_state(): + packet = build_skill_doctor() + + assert packet["scope"] == "skill_doctor" + assert "core_assets" in packet + assert "external_engine_adapters" in packet + assert packet["status"] in {"ready", "degraded"} + + +def test_mcp_exposes_skill_experience_tools(): + import mcp_server + + onboarding = mcp_server.skill_onboarding({}) + doctor = mcp_server.skill_doctor() + + assert onboarding["scope"] == "skill_onboarding" + assert doctor["scope"] == "skill_doctor" + + +def test_rectification_contract_generates_and_scores_choice_answers(): + questionnaire = build_rectification_questionnaire({ + "year": 1993, "month": 4, "day": 17, "hour": 14, "minute": 49, + "time_uncertainty_minutes": 20, + }) + scored = score_rectification_answers(questionnaire, { + "education_environment_shift": "A", + "health_crisis_or_low_period": "C", + }) + + assert questionnaire["scope"] == "active_birth_time_rectification_questionnaire" + assert scored["scope"] == "active_birth_time_rectification_scoring" + assert scored["candidate_cluster_rankings"] diff --git a/web/evidence_packet.html b/web/evidence_packet.html new file mode 100644 index 00000000..7b0a1104 --- /dev/null +++ b/web/evidence_packet.html @@ -0,0 +1,26 @@ + + + + +
仅显示已完成任务的可审计计算状态、证据包和技法审计。不会展示内部提示词或原始出生输入。
+ +-
-
-
先扫描候选时间,再回答选择题。结果只缩小候选簇,不宣称已经精确到分钟。
+先确认出生资料,再选择直接排盘或主动问询式生时校正。外部引擎状态将在证据包中明示。
+可使用本地城市库;未收录时请手填经纬度。此操作不调用第三方地理服务。
等待检查
先扫描候选时间,再回答选择题。结果只缩小候选簇,不宣称已经精确到分钟。
-