From fa434fa7c5a00447b0182b11be12769d8808dc99 Mon Sep 17 00:00:00 2001 From: 732642856 <732642856@qq.com> Date: Thu, 16 Jul 2026 23:29:48 +0800 Subject: [PATCH] feat: harden VedAstro parity evidence --- .../vedastro_parity_matrix_latest.json | 4 +- .../research/vedastro_parity_matrix_latest.md | 4 +- .../pyjhora_oracle_artifact_manifest.json | 12 +- .../three_engine_parity_replay_manifest.json | 233 +++++++++++++++-- scripts/configure_vedastro_secret.py | 64 +++++ scripts/three_engine_parity_runner.py | 236 ++++++++++++++++-- scripts/vedastro_gateway.py | 1 + scripts/vedastro_service_adapter.py | 40 ++- tests/test_configure_vedastro_secret.py | 18 ++ tests/test_skill_release_manifest.py | 2 +- ...st_three_engine_parity_replay_validator.py | 39 ++- tests/test_three_engine_parity_runner.py | 125 +++++++++- tests/test_vedastro_gateway.py | 13 + ...est_vedastro_official_artifact_manifest.py | 26 ++ tests/test_vedastro_official_mcp_bridge.py | 4 +- .../test_vedastro_service_adapter_executor.py | 13 +- 16 files changed, 758 insertions(+), 76 deletions(-) create mode 100644 scripts/configure_vedastro_secret.py create mode 100644 tests/test_configure_vedastro_secret.py create mode 100644 tests/test_vedastro_official_artifact_manifest.py diff --git a/docs/research/vedastro_parity_matrix_latest.json b/docs/research/vedastro_parity_matrix_latest.json index 5cb78ec9..0710b533 100644 --- a/docs/research/vedastro_parity_matrix_latest.json +++ b/docs/research/vedastro_parity_matrix_latest.json @@ -1,7 +1,7 @@ { "scope": "vedastro_parity_matrix", "schema_version": 1, - "generated_at": "2026-06-28T23:49:39.845848+00:00", + "generated_at": "2026-07-16T11:53:05.440396+00:00", "summary": { "row_count": 13, "p0_count": 11, @@ -24,7 +24,7 @@ "rest_adapter": 2, "external_evidence_only": 1 }, - "local_registry_technique_count": 89 + "local_registry_technique_count": 91 }, "boundary": { "not_a_clone_claim": true, diff --git a/docs/research/vedastro_parity_matrix_latest.md b/docs/research/vedastro_parity_matrix_latest.md index 6bd8f94e..5e28a6c9 100644 --- a/docs/research/vedastro_parity_matrix_latest.md +++ b/docs/research/vedastro_parity_matrix_latest.md @@ -1,9 +1,9 @@ # VedAstro Parity Matrix -- Generated: `2026-06-28T23:49:39.845848+00:00` +- Generated: `2026-07-16T11:53:05.440396+00:00` - Rows: `13` - P0 rows: `11` -- Local registry technique count: `89` +- Local registry technique count: `91` ## Honesty Boundary diff --git a/references/oracle/artifacts/pyjhora_oracle_artifact_manifest.json b/references/oracle/artifacts/pyjhora_oracle_artifact_manifest.json index a4cdab93..bcda12f8 100644 --- a/references/oracle/artifacts/pyjhora_oracle_artifact_manifest.json +++ b/references/oracle/artifacts/pyjhora_oracle_artifact_manifest.json @@ -1,8 +1,8 @@ { "scope": "pyjhora_oracle_artifact_manifest", - "generated_at": "2026-07-01T10:31:50.603970+00:00", + "generated_at": "2026-07-16T10:24:25.190784+00:00", "artifact_count": 9, - "packet_count": 5, + "packet_count": 6, "artifacts": [ "pyjhora_einstein_varshaphala_1905_lahiri_partial_20260629.txt", "pyjhora_extreme_latitude_kp_shadbala_stdout_20260627.txt", @@ -19,7 +19,8 @@ "external_template_historical_epoch_lahiri_pyjhora_20260627.json", "external_template_steve_jobs_dasha_lahiri_pyjhora_20260627.json", "external_template_steve_jobs_shadbala_lahiri_pyjhora_20260627.json", - "external_template_steve_jobs_varshaphala_1984_lahiri_pyjhora_20260627.json" + "external_template_steve_jobs_varshaphala_1984_lahiri_pyjhora_20260627.json", + "external_template_synthetic_north_china_shadbala_raman_pyjhora_20260627.json" ], "fronts": { "dasha": { @@ -37,13 +38,14 @@ }, "shadbala": { "artifact_count": 2, - "packet_count": 1, + "packet_count": 2, "artifacts": [ "pyjhora_extreme_latitude_kp_shadbala_stdout_20260627.txt", "pyjhora_steve_jobs_shadbala_lahiri_stdout_20260627.txt" ], "pending_packets": [ - "external_template_steve_jobs_shadbala_lahiri_pyjhora_20260627.json" + "external_template_steve_jobs_shadbala_lahiri_pyjhora_20260627.json", + "external_template_synthetic_north_china_shadbala_raman_pyjhora_20260627.json" ] }, "tajika_sahams": { diff --git a/references/oracle/three_engine_parity_replay_manifest.json b/references/oracle/three_engine_parity_replay_manifest.json index ec4cdd97..cf9c055b 100644 --- a/references/oracle/three_engine_parity_replay_manifest.json +++ b/references/oracle/three_engine_parity_replay_manifest.json @@ -1,25 +1,222 @@ { - "case_id": "contract_ready_no_same_chart_rows", - "birth_data_policy": "operator_local_only", - "status": "not_started", - "engines": { - "VedAstro": { - "status": "blocked", - "official_raw_response_path": "", - "artifact_hash": "" + "birth_data_policy": "public_case_only", + "blocked_reason": "none", + "case_id": "steve_jobs_public_1955_lahiri", + "comparison_rows": [ + { + "field": "Sun.sign", + "local_value": "Aquarius", + "oracle_values": { + "PyJHora_JHora": "Aquarius", + "VedAstro": "Aquarius", + "jyotishganit": "Aquarius" + }, + "section": "D1", + "status": "match" }, + { + "field": "Sun.longitude", + "local_value": 312.51748591136726, + "oracle_values": { + "PyJHora_JHora": 312.51819523181405, + "VedAstro": 312.51222222222225, + "jyotishganit": 312.5241179806235 + }, + "section": "D1", + "status": "match" + }, + { + "field": "Moon.sign", + "local_value": "Pisces", + "oracle_values": { + "PyJHora_JHora": "Pisces", + "VedAstro": "Pisces", + "jyotishganit": "Pisces" + }, + "section": "D1", + "status": "match" + }, + { + "field": "Moon.longitude", + "local_value": 344.5165209098362, + "oracle_values": { + "PyJHora_JHora": 344.51168181221095, + "VedAstro": 344.5113888888889, + "jyotishganit": 344.5244975534928 + }, + "section": "D1", + "status": "match" + }, + { + "field": "Mars.sign", + "local_value": "Aries", + "oracle_values": { + "PyJHora_JHora": "Aries", + "VedAstro": "Aries", + "jyotishganit": "Aries" + }, + "section": "D1", + "status": "match" + }, + { + "field": "Mars.longitude", + "local_value": 5.8598225437926565, + "oracle_values": { + "PyJHora_JHora": 5.862334338243072, + "VedAstro": 5.854722222222223, + "jyotishganit": 5.866405267585137 + }, + "section": "D1", + "status": "match" + }, + { + "field": "Mercury.sign", + "local_value": "Capricorn", + "oracle_values": { + "PyJHora_JHora": "Capricorn", + "VedAstro": "Capricorn", + "jyotishganit": "Capricorn" + }, + "section": "D1", + "status": "match" + }, + { + "field": "Mercury.longitude", + "local_value": 291.13107265785646, + "oracle_values": { + "PyJHora_JHora": 291.12589362817465, + "VedAstro": 291.12583333333333, + "jyotishganit": 291.1374137657774 + }, + "section": "D1", + "status": "match" + }, + { + "field": "Jupiter.sign", + "local_value": "Gemini", + "oracle_values": { + "PyJHora_JHora": "Gemini", + "VedAstro": "Gemini", + "jyotishganit": "Gemini" + }, + "section": "D1", + "status": "match" + }, + { + "field": "Jupiter.longitude", + "local_value": 87.27723563442187, + "oracle_values": { + "PyJHora_JHora": 87.27055150216493, + "VedAstro": 87.27222222222223, + "jyotishganit": 87.28382648671735 + }, + "section": "D1", + "status": "match" + }, + { + "field": "Venus.sign", + "local_value": "Sagittarius", + "oracle_values": { + "PyJHora_JHora": "Sagittarius", + "VedAstro": "Sagittarius", + "jyotishganit": "Sagittarius" + }, + "section": "D1", + "status": "match" + }, + { + "field": "Venus.longitude", + "local_value": 267.941228624149, + "oracle_values": { + "PyJHora_JHora": 267.94211928370015, + "VedAstro": 267.93611111111113, + "jyotishganit": 267.94779146740916 + }, + "section": "D1", + "status": "match" + }, + { + "field": "Saturn.sign", + "local_value": "Libra", + "oracle_values": { + "PyJHora_JHora": "Libra", + "VedAstro": "Libra", + "jyotishganit": "Libra" + }, + "section": "D1", + "status": "match" + }, + { + "field": "Saturn.longitude", + "local_value": 207.93204028705406, + "oracle_values": { + "PyJHora_JHora": 207.92738390514674, + "VedAstro": 207.92694444444444, + "jyotishganit": 207.93866866093018 + }, + "section": "D1", + "status": "match" + }, + { + "field": "raw_capture", + "local_value": null, + "oracle_values": { + "PyJHora_JHora": "structured_d1_captured", + "VedAstro": null, + "jyotishganit": "captured" + }, + "reason": "three_engine_scope_does_not_share_this_normalized_field", + "section": "Panchanga", + "status": "not_comparable" + } + ], + "engines": { "PyJHora_JHora": { - "status": "blocked", - "raw_output_path": "", - "settings": {} + "artifact_hash": "07115ec1b5332b679259f14d8ca70964cc3ed394a31b40e994db6367d2da0ccf", + "raw_output_path": "scratch/local/three_engine_parity_live/pyjhora_structured_d1.json", + "settings": { + "ayanamsa": "LAHIRI", + "node_mode": "PyJHora default" + }, + "status": "structured_captured" + }, + "VedAstro": { + "artifact_hash": "84dbf7b2d7c4e13b8b26a4d42256f1ca5f8a76ffcaa0ab9695e14b646c77990b", + "official_raw_response_path": "/Users/wuyongnaren/Documents/印度占星/scratch/local/vedastro_adapter/official_full_snapshot-9581ff2b2603-8954c121bd0d.json", + "reason": "imported_latest_official_full_snapshot_artifact", + "settings": { + "ayanamsa": "lahiri", + "node_mode": "mean" + }, + "status": "official_verified" }, "jyotishganit": { - "status": "blocked", - "raw_output_path": "", - "settings": {} + "error": null, + "raw_output_path": "scratch/local/three_engine_parity_live/jyotishganit_raw.json", + "status": "raw_captured" } }, - "comparison_rows": [], - "blocked_reason": "no_same_chart_oracle_rows_imported", - "runtime_boundary": "This manifest defines the same-chart parity replay contract only. It does not prove VedAstro/PyJHora/JHora/jyotishganit parity until raw oracle rows are imported." -} + "local": { + "calculation_contract": { + "algorithm": "sidereal_natal_chart", + "contract_version": "1.0.0", + "effective": { + "ayanamsa": "lahiri", + "ephemeris_flags_verified": false, + "ephemeris_source": "swisseph_calc_ut", + "node_mode": "mean", + "timezone_offset": -8.0, + "timezone_source": "explicit_offset" + }, + "requested": { + "ayanamsa": "lahiri", + "node_mode": "mean", + "timezone_offset": -8.0 + } + }, + "result_hash": "5e3b6ec860177ff29506820a3a80b962fae9bca957af8ffc8302d0e85c733a68" + }, + "runtime_boundary": "This packet has real public raw artifacts and may include VedAstro official raw evidence, and normalized D1 planet sign/longitude parity is tested. Non-D1 scopes still require separate gates.", + "status": "pass", + "tested": true +} \ No newline at end of file diff --git a/scripts/configure_vedastro_secret.py b/scripts/configure_vedastro_secret.py new file mode 100644 index 00000000..9d510216 --- /dev/null +++ b/scripts/configure_vedastro_secret.py @@ -0,0 +1,64 @@ +"""Configure VedAstro local secret without echoing it. + +Run manually from a trusted terminal. This script writes only to ignored local +env files; it must never be used to commit or print secrets. +""" + +from __future__ import annotations + +import getpass +from pathlib import Path + + +DEFAULT_ENV = Path(".env.local") +DEFAULT_ENDPOINT = "https://api.vedastro.org/api" + + +def update_env_text(text: str, updates: dict[str, str]) -> str: + lines = text.splitlines() + seen: set[str] = set() + output: list[str] = [] + for line in lines: + stripped = line.strip() + if not stripped or stripped.startswith("#") or "=" not in line: + output.append(line) + continue + key, _value = line.split("=", 1) + key = key.strip() + if key in updates: + output.append(f"{key}={updates[key]}") + seen.add(key) + else: + output.append(line) + for key, value in updates.items(): + if key not in seen: + output.append(f"{key}={value}") + return "\n".join(output).rstrip() + "\n" + + +def write_env(path: Path, updates: dict[str, str]) -> None: + existing = path.read_text(encoding="utf-8") if path.exists() else "" + path.write_text(update_env_text(existing, updates), encoding="utf-8") + + +def main() -> int: + key = getpass.getpass("VedAstro API key (hidden): ").strip() + if not key: + print("No key entered; nothing changed.") + return 1 + endpoint = input(f"VedAstro endpoint [{DEFAULT_ENDPOINT}]: ").strip() or DEFAULT_ENDPOINT + write_env( + DEFAULT_ENV, + { + "VEDASTRO_API_KEY": key, + "VEDASTRO_API_ENDPOINT": endpoint, + "VEDASTRO_ENABLE_NETWORK": "1", + "VEDASTRO_TIMEOUT_SECONDS": "20", + }, + ) + print(f"Updated {DEFAULT_ENV}; secret value was not printed.") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/three_engine_parity_runner.py b/scripts/three_engine_parity_runner.py index f1800b79..3a01e87b 100644 --- a/scripts/three_engine_parity_runner.py +++ b/scripts/three_engine_parity_runner.py @@ -4,6 +4,7 @@ from __future__ import annotations import argparse +import hashlib import json import sys from datetime import datetime @@ -16,6 +17,7 @@ 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" +VEDASTRO_ARTIFACT_DIR = ROOT / "scratch/local/vedastro_adapter" PUBLIC_CASE = { "case_id": "steve_jobs_public_1955_lahiri", @@ -31,6 +33,9 @@ PUBLIC_CASE = { "ayanamsa": "lahiri", "node_mode": "mean", } +PLANETS = ("Sun", "Moon", "Mars", "Mercury", "Jupiter", "Venus", "Saturn") +SIGNS = ("Aries", "Taurus", "Gemini", "Cancer", "Leo", "Virgo", "Libra", "Scorpio", "Sagittarius", "Capricorn", "Aquarius", "Pisces") +LONGITUDE_TOLERANCE_DEGREES = 0.02 def _write_json(path: Path, value: dict[str, Any]) -> Path: @@ -71,6 +76,47 @@ def _capture_jyotishganit_raw(output_dir: Path) -> tuple[dict[str, Any], str]: pass +def _capture_pyjhora_structured_d1(output_dir: Path) -> tuple[dict[str, Any], str]: + try: + import contextlib + import importlib + import io + + with contextlib.redirect_stdout(io.StringIO()): + utils = importlib.import_module("jhora.utils") + charts = importlib.import_module("jhora.horoscope.chart.charts") + drik = importlib.import_module("jhora.panchanga.drik") + + jd = utils.julian_day_number( + (PUBLIC_CASE["year"], PUBLIC_CASE["month"], PUBLIC_CASE["day"]), + (PUBLIC_CASE["hour"], PUBLIC_CASE["minute"], PUBLIC_CASE["second"]), + ) + drik.set_ayanamsa_mode("LAHIRI", jd=jd) + place = drik.Place("San Francisco, CA", PUBLIC_CASE["lat"], PUBLIC_CASE["lon"], PUBLIC_CASE["tz"]) + with contextlib.redirect_stdout(io.StringIO()): + raw = charts.rasi_chart(jd, place) + index_to_planet = {0: "Sun", 1: "Moon", 2: "Mars", 3: "Mercury", 4: "Jupiter", 5: "Venus", 6: "Saturn"} + planets: dict[str, dict[str, float | str]] = {} + for body, position in raw: + if body not in index_to_planet: + continue + sign_index, degree = position + planets[index_to_planet[body]] = { + "sign": SIGNS[int(sign_index)], + "longitude": int(sign_index) * 30 + float(degree), + } + payload = { + "source": "PyJHora.jhora.horoscope.chart.charts.rasi_chart", + "settings": {"ayanamsa": "LAHIRI", "jd_input": "local_birth_time", "node_mode": "PyJHora default"}, + "raw": raw, + "planets": planets, + } + path = _write_json(output_dir / "pyjhora_structured_d1.json", payload) + return payload, str(path) + except Exception as exc: + return {"error": f"{exc.__class__.__name__}: {exc}"}, "" + + def _vedastro_state(*, allow_network: bool) -> dict[str, Any]: if not allow_network: return { @@ -78,6 +124,15 @@ def _vedastro_state(*, allow_network: bool) -> dict[str, Any]: "official_raw_response_path": "", "reason": "network_disabled_for_public_replay", } + artifact = _latest_vedastro_official_raw_artifact() + if artifact: + return { + "status": "official_verified", + "official_raw_response_path": str(artifact), + "artifact_hash": hashlib.sha256(artifact.read_bytes()).hexdigest(), + "settings": {"ayanamsa": PUBLIC_CASE["ayanamsa"], "node_mode": PUBLIC_CASE["node_mode"]}, + "reason": "imported_latest_official_full_snapshot_artifact", + } return { "status": "blocked", "official_raw_response_path": "", @@ -85,50 +140,187 @@ def _vedastro_state(*, allow_network: bool) -> dict[str, Any]: } +def _latest_vedastro_official_raw_artifact() -> Path | None: + if not VEDASTRO_ARTIFACT_DIR.exists(): + return None + candidates: list[Path] = [] + for path in VEDASTRO_ARTIFACT_DIR.glob("official_full_snapshot-*.json"): + try: + payload = json.loads(path.read_text(encoding="utf-8")) + except json.JSONDecodeError: + continue + raw = payload.get("official_raw_response") or payload.get("raw_response") + source = str(raw.get("source") or "") if isinstance(raw, dict) else "" + if ( + payload.get("status") == "ok" + and source.startswith("vedastro_official") + and _artifact_matches_public_case(payload) + ): + candidates.append(path) + if not candidates: + return None + return max(candidates, key=lambda item: item.stat().st_mtime) + + +def _artifact_matches_public_case(payload: dict[str, Any]) -> bool: + manifest = payload.get("request_manifest") if isinstance(payload.get("request_manifest"), dict) else {} + text = json.dumps(manifest, ensure_ascii=False, sort_keys=True) + return ( + "24/02/1955" in text + and "19:15" in text + and "37.7749" in text + and "-122.4194" in text + ) + + +def _load_vedastro_artifact(path: str) -> dict[str, Any]: + if not path: + return {} + try: + return json.loads(Path(path).read_text(encoding="utf-8")) + except (OSError, json.JSONDecodeError): + return {} + + +def _vedastro_d1(artifact: dict[str, Any], planet: str) -> dict[str, Any]: + try: + payload = artifact["snapshot_sections"]["chart_core"][planet]["Payload"]["AllPlanetData"] + return { + "sign": payload["PlanetRasiD1Sign"]["Name"], + "longitude": float(payload["PlanetNirayanaLongitude"]["TotalDegrees"]), + } + except (KeyError, TypeError, ValueError): + return {} + + +def _jyotishganit_d1(raw: dict[str, Any], planet: str) -> dict[str, Any]: + for house in (raw.get("d1Chart") or {}).get("houses") or []: + for occupant in house.get("occupants") or []: + if occupant.get("celestialBody") != planet: + continue + sign = occupant.get("sign") + degree = occupant.get("signDegrees") + if sign not in SIGNS or degree is None: + return {} + return {"sign": sign, "longitude": SIGNS.index(sign) * 30 + float(degree)} + return {} + + +def _pyjhora_d1(raw: dict[str, Any], planet: str) -> dict[str, Any]: + value = raw.get("planets", {}).get(planet) + return value if isinstance(value, dict) else {} + + +def _d1_comparison_rows( + local: dict[str, Any], + vedastro_artifact: dict[str, Any], + jyotishganit_raw: dict[str, Any], + pyjhora_raw: dict[str, Any], +) -> list[dict[str, Any]]: + rows: list[dict[str, Any]] = [] + for planet in PLANETS: + local_planet = local.get("planets", {}).get(planet) or {} + vedastro = _vedastro_d1(vedastro_artifact, planet) + jyotishganit = _jyotishganit_d1(jyotishganit_raw, planet) + pyjhora = _pyjhora_d1(pyjhora_raw, planet) + local_sign = local_planet.get("sign") + sign_values = { + "VedAstro": vedastro.get("sign"), + "PyJHora_JHora": pyjhora.get("sign"), + "jyotishganit": jyotishganit.get("sign"), + } + comparable_signs = [value for value in sign_values.values() if value is not None] + rows.append({ + "section": "D1", + "field": f"{planet}.sign", + "local_value": local_sign, + "oracle_values": sign_values, + "status": ( + "match" + if local_sign and comparable_signs and all(value == local_sign for value in comparable_signs) + else "blocked" if not comparable_signs else "mismatch" + ), + }) + + local_lon = local_planet.get("lon") + longitude_values = { + "VedAstro": vedastro.get("longitude"), + "PyJHora_JHora": pyjhora.get("longitude"), + "jyotishganit": jyotishganit.get("longitude"), + } + comparable = [value for value in longitude_values.values() if isinstance(value, (int, float))] + rows.append({ + "section": "D1", + "field": f"{planet}.longitude", + "local_value": local_lon, + "oracle_values": longitude_values, + "status": ( + "match" + if isinstance(local_lon, (int, float)) + and comparable + and all(abs(value - local_lon) <= LONGITUDE_TOLERANCE_DEGREES for value in comparable) + else "blocked" if not comparable else "mismatch" + ), + }) + return rows + + 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_raw, pyjhora_path = _capture_pyjhora_structured_d1(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", - }, + vedastro_artifact = _load_vedastro_artifact(vedastro.get("official_raw_response_path", "")) + rows = _d1_comparison_rows(local, vedastro_artifact, jyotishganit_raw, pyjhora_raw) + [ { "section": "Panchanga", "field": "raw_capture", "local_value": None, "oracle_values": { "VedAstro": None, - "PyJHora_JHora": "dasha_only_artifact", + "PyJHora_JHora": "structured_d1_captured" if pyjhora_path else "dasha_only_artifact", "jyotishganit": "captured" if jyotishganit_path else None, }, "status": "not_comparable", "reason": "three_engine_scope_does_not_share_this_normalized_field", }, ] + has_blocked = any(row.get("status") == "blocked" for row in rows) + has_mismatch = any(row.get("status") == "mismatch" for row in rows) + has_required_raw = vedastro.get("status") == "official_verified" and bool(pyjhora_path) and bool(jyotishganit_path) + blocked_reason = ( + "official_vedastro_raw_missing_or_unverified" + if vedastro.get("status") != "official_verified" + else "some_comparison_rows_blocked" + if has_blocked + else "comparison_rows_mismatch" + if has_mismatch + else "none" + ) + report_status = ( + "blocked" + if not bool(jyotishganit_path) + else "mismatch" + if has_mismatch + else "partial" + if not has_required_raw or has_blocked + else "pass" + ) + pyjhora_artifact_path = pyjhora_path or (str(PYJHORA_ARTIFACT) if pyjhora_available else "") 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", + "status": report_status, + "tested": vedastro.get("status") == "official_verified" and bool(rows), + "blocked_reason": blocked_reason, "engines": { "VedAstro": vedastro, "PyJHora_JHora": { - "status": "raw_imported" if pyjhora_available else "blocked", - "raw_output_path": str(PYJHORA_ARTIFACT) if pyjhora_available else "", + "status": "structured_captured" if pyjhora_path else "raw_imported" if pyjhora_available else "blocked", + "raw_output_path": pyjhora_artifact_path, + "artifact_hash": hashlib.sha256(Path(pyjhora_artifact_path).read_bytes()).hexdigest() if pyjhora_artifact_path else "", "settings": {"ayanamsa": "LAHIRI", "node_mode": "PyJHora default"}, }, "jyotishganit": { @@ -143,8 +335,8 @@ def build_public_case_replay(*, output_dir: Path, allow_vedastro_network: bool = }, "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." + "This packet has real public raw artifacts and may include VedAstro official raw evidence, " + "and normalized D1 planet sign/longitude parity is tested. Non-D1 scopes still require separate gates." ), } _write_json(output_dir / "three_engine_parity_replay.json", report) diff --git a/scripts/vedastro_gateway.py b/scripts/vedastro_gateway.py index 775ea6d2..7daeed36 100644 --- a/scripts/vedastro_gateway.py +++ b/scripts/vedastro_gateway.py @@ -235,6 +235,7 @@ def gateway_status() -> dict[str, Any]: "active_backend": _active_backend(config), "self_host_configured": config["self_host_endpoint_configured"], "official_configured": config["official_endpoint_configured"], + "credential_configured": bool(os.environ.get("VEDASTRO_API_KEY", "").strip()), "cache_ttl_seconds": config["cache_ttl_seconds"], "queue_enabled": config["queue_enabled"], "fail_open_local": config["fail_open_local"], diff --git a/scripts/vedastro_service_adapter.py b/scripts/vedastro_service_adapter.py index f08bf481..310288df 100644 --- a/scripts/vedastro_service_adapter.py +++ b/scripts/vedastro_service_adapter.py @@ -419,6 +419,34 @@ def _write_artifact(result: dict[str, Any]) -> str: return _repo_relative(artifact) +def list_official_full_snapshot_artifacts() -> dict[str, Any]: + artifacts: list[dict[str, Any]] = [] + if ARTIFACT_DIR.exists(): + for path in sorted(ARTIFACT_DIR.glob("official_full_snapshot-*.json")): + try: + payload = json.loads(path.read_text(encoding="utf-8")) + except json.JSONDecodeError: + continue + raw = payload.get("official_raw_response") or payload.get("raw_response") + raw_source = str(raw.get("source") or "") if isinstance(raw, dict) else "" + official_raw_available = raw_source.startswith("vedastro_official") + artifacts.append( + { + "path": _repo_relative(path), + "status": payload.get("status"), + "operation": payload.get("operation"), + "official_raw_response_available": official_raw_available, + "section_count": len(payload.get("snapshot_sections") or {}), + "request_manifest_available": bool(payload.get("request_manifest")), + } + ) + return { + "scope": "vedastro_official_full_snapshot_artifact_manifest", + "artifact_count": len(artifacts), + "artifacts": artifacts, + } + + def _cache_ttl_seconds() -> float: raw = os.environ.get(CACHE_TTL_ENV, "").strip() if not raw: @@ -772,9 +800,6 @@ def _build_official_search_events_profile(request_preview: dict[str, Any]) -> di body["EndTime"] = end_time body["PrecisionHours"] = 100 headers: dict[str, str] = {"Content-Type": "application/json"} - api_key = os.environ.get("VEDASTRO_API_KEY", "").strip() - if api_key: - headers["x-api-key"] = api_key return { "profile_version": OFFICIAL_SEARCH_EVENTS_PROFILE_VERSION, "endpoint_path": OFFICIAL_SEARCH_EVENTS_ENDPOINT_PATH, @@ -794,9 +819,6 @@ def _build_live_sampling_search_events_profile(request_preview: dict[str, Any]) "AtTime": _time_json_from_case(case, str(request_preview["start_date"])), } headers: dict[str, str] = {"Content-Type": "application/json"} - api_key = os.environ.get("VEDASTRO_API_KEY", "").strip() - if api_key: - headers["x-api-key"] = api_key return { "profile_version": f"{OFFICIAL_SEARCH_EVENTS_PROFILE_VERSION}_live_sampling", "endpoint_path": OFFICIAL_SEARCH_EVENTS_ENDPOINT_PATH, @@ -865,9 +887,6 @@ def _official_full_snapshot_manifest(case: dict[str, Any], case_id: str = "user_ common_body = _official_common_body(case) reference_date = _official_snapshot_reference_date(case) headers: dict[str, str] = {"Content-Type": "application/json"} - api_key = os.environ.get("VEDASTRO_API_KEY", "").strip() - if api_key: - headers["x-api-key"] = api_key requests = [] for item in OFFICIAL_FULL_SNAPSHOT_METHODS: body = dict(common_body) @@ -1603,6 +1622,9 @@ def _build_live_request( request_url = f"{endpoint.rstrip('/')}{official_request_profile.get('endpoint_path', '')}" headers = dict(official_request_profile.get("headers") or headers) vedastro_payload = dict(official_request_profile.get("body") or {}) + api_key = os.environ.get("VEDASTRO_API_KEY", "").strip() + if api_key: + headers["x-api-key"] = api_key return request_url, headers, vedastro_payload diff --git a/tests/test_configure_vedastro_secret.py b/tests/test_configure_vedastro_secret.py new file mode 100644 index 00000000..c251dee7 --- /dev/null +++ b/tests/test_configure_vedastro_secret.py @@ -0,0 +1,18 @@ +from scripts.configure_vedastro_secret import update_env_text + + +def test_update_env_text_adds_and_replaces_vedastro_settings(): + text = "VEDASTRO_API_ENDPOINT=https://old.example/api\nOTHER=value\n" + updated = update_env_text( + text, + { + "VEDASTRO_API_KEY": "sample-secret", + "VEDASTRO_API_ENDPOINT": "https://api.vedastro.org/api", + "VEDASTRO_ENABLE_NETWORK": "1", + }, + ) + assert "VEDASTRO_API_ENDPOINT=https://api.vedastro.org/api" in updated + assert "VEDASTRO_API_KEY=sample-secret" in updated + assert "VEDASTRO_ENABLE_NETWORK=1" in updated + assert "OTHER=value" in updated + assert "https://old.example/api" not in updated diff --git a/tests/test_skill_release_manifest.py b/tests/test_skill_release_manifest.py index b4c73709..54a45884 100644 --- a/tests/test_skill_release_manifest.py +++ b/tests/test_skill_release_manifest.py @@ -26,7 +26,7 @@ def test_skill_release_manifest_defines_basic_and_premium_boundaries() -> None: def test_skill_release_manifest_contains_no_private_birth_data() -> None: text = json.dumps(build_report(), ensure_ascii=False) - private_date = "-".join(["REDACTED_YEAR", "04", "17"]) + private_date = "-".join(["1990", "04", "17"]) private_time = "14" + "点" + "49" private_place = "第四" + "人民医院" for forbidden in (private_date, private_time, private_place): diff --git a/tests/test_three_engine_parity_replay_validator.py b/tests/test_three_engine_parity_replay_validator.py index b7c9a266..1f27d9fc 100644 --- a/tests/test_three_engine_parity_replay_validator.py +++ b/tests/test_three_engine_parity_replay_validator.py @@ -12,13 +12,14 @@ from scripts.three_engine_parity_replay_validator import validate_manifest ROOT = Path(__file__).resolve().parents[1] -def test_three_engine_parity_manifest_blocks_without_oracle_rows() -> None: +def test_three_engine_parity_manifest_passes_d1_with_vedastro_and_pyjhora_raw() -> None: result = validate_manifest(ROOT / "references/oracle/three_engine_parity_replay_manifest.json") - assert result["status"] == "blocked" - assert result["comparison_row_count"] == 0 - assert result["tested"] is False - assert result["blocked_reason"] == "no_same_chart_oracle_rows_imported" + assert result["status"] == "pass" + assert result["comparison_row_count"] == 15 + assert result["match_count"] == 14 + assert result["tested"] is True + assert result["blocked_reason"] is None def test_three_engine_parity_validator_accepts_one_same_chart_row(tmp_path: Path) -> None: @@ -59,10 +60,34 @@ def test_three_engine_parity_validator_accepts_one_same_chart_row(tmp_path: Path assert result["match_count"] == 1 -def test_validator_require_pass_rejects_blocked_manifest() -> None: +def test_validator_require_pass_rejects_blocked_manifest(tmp_path: Path) -> None: root = Path(__file__).resolve().parents[1] + manifest = tmp_path / "blocked_manifest.json" + manifest.write_text( + json.dumps( + { + "case_id": "blocked_case", + "birth_data_policy": "public_case_only", + "engines": { + "VedAstro": {"status": "blocked"}, + "PyJHora_JHora": {"status": "blocked"}, + "jyotishganit": {"status": "blocked"}, + }, + "comparison_rows": [ + { + "section": "D1", + "field": "Sun.longitude", + "local_value": 1.0, + "oracle_values": {}, + "status": "blocked", + } + ], + } + ), + encoding="utf-8", + ) completed = subprocess.run( - [sys.executable, "scripts/three_engine_parity_replay_validator.py", "--require-pass"], + [sys.executable, "scripts/three_engine_parity_replay_validator.py", str(manifest), "--require-pass"], cwd=root, capture_output=True, text=True, diff --git a/tests/test_three_engine_parity_runner.py b/tests/test_three_engine_parity_runner.py index 3887cbc8..34aebc7e 100644 --- a/tests/test_three_engine_parity_runner.py +++ b/tests/test_three_engine_parity_runner.py @@ -1,5 +1,6 @@ from __future__ import annotations +import json import sys from pathlib import Path @@ -9,6 +10,7 @@ SCRIPTS = ROOT / "scripts" if str(SCRIPTS) not in sys.path: sys.path.insert(0, str(SCRIPTS)) +import three_engine_parity_runner # noqa: E402 from three_engine_parity_runner import build_public_case_replay # noqa: E402 @@ -17,10 +19,129 @@ def test_public_same_chart_replay_never_promotes_missing_vedastro_raw(tmp_path: 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"]["PyJHora_JHora"]["status"] == "structured_captured" 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"]) + assert any(row["status"] == "match" for row in report["comparison_rows"]) + assert all(row["status"] in {"blocked", "match", "not_comparable"} for row in report["comparison_rows"]) + + +def test_public_same_chart_replay_imports_verified_vedastro_artifact(tmp_path: Path, monkeypatch) -> None: + artifact_root = tmp_path / "vedastro_adapter" + artifact_root.mkdir() + artifact = artifact_root / "official_full_snapshot-abc-def.json" + artifact.write_text( + json.dumps( + { + "status": "ok", + "operation": "official_full_snapshot", + "raw_response": {"source": "vedastro_official_full_snapshot", "sections": {"chart_core": {}}}, + "request_manifest": { + "settings": {"ayanamsa": "lahiri", "node_mode": "mean"}, + "requests": [ + { + "body": { + "BirthTime": { + "StdTime": "19:15 24/02/1955 -08:00", + "Location": {"Latitude": 37.7749, "Longitude": -122.4194}, + } + } + } + ], + }, + "snapshot_sections": {"chart_core": {"Status": "Pass"}}, + } + ), + encoding="utf-8", + ) + monkeypatch.setattr(three_engine_parity_runner, "VEDASTRO_ARTIFACT_DIR", artifact_root) + + report = build_public_case_replay(output_dir=tmp_path / "out", allow_vedastro_network=True) + + assert report["engines"]["VedAstro"]["status"] == "official_verified" + assert report["engines"]["VedAstro"]["official_raw_response_path"] == str(artifact) + assert len(report["engines"]["VedAstro"]["artifact_hash"]) == 64 + assert report["blocked_reason"] == "none" + + +def test_public_same_chart_replay_rejects_wrong_vedastro_artifact(tmp_path: Path, monkeypatch) -> None: + artifact_root = tmp_path / "vedastro_adapter" + artifact_root.mkdir() + artifact = artifact_root / "official_full_snapshot-wrong-chart.json" + artifact.write_text( + json.dumps( + { + "status": "ok", + "raw_response": {"source": "vedastro_official_full_snapshot"}, + "request_manifest": { + "requests": [ + { + "body": { + "BirthTime": { + "StdTime": "12:00 01/01/1990 +05:30", + "Location": {"Latitude": 28.6139, "Longitude": 77.209}, + } + } + } + ] + }, + "snapshot_sections": {"chart_core": {"Status": "Pass"}}, + } + ), + encoding="utf-8", + ) + monkeypatch.setattr(three_engine_parity_runner, "VEDASTRO_ARTIFACT_DIR", artifact_root) + + report = build_public_case_replay(output_dir=tmp_path / "out", allow_vedastro_network=True) + + assert report["engines"]["VedAstro"]["status"] == "blocked" + assert report["engines"]["VedAstro"]["reason"] == "official_runner_requires_explicit_raw_capture_workflow" + + +def test_public_same_chart_replay_adds_normalized_d1_rows(tmp_path: Path, monkeypatch) -> None: + artifact_root = tmp_path / "vedastro_adapter" + artifact_root.mkdir() + artifact = artifact_root / "official_full_snapshot-abc-def.json" + artifact.write_text( + json.dumps( + { + "status": "ok", + "raw_response": {"source": "vedastro_official_full_snapshot"}, + "request_manifest": { + "requests": [ + { + "body": { + "BirthTime": { + "StdTime": "19:15 24/02/1955 -08:00", + "Location": {"Latitude": 37.7749, "Longitude": -122.4194}, + } + } + } + ] + }, + "snapshot_sections": { + "chart_core": { + "Sun": { + "Payload": { + "AllPlanetData": { + "PlanetNirayanaLongitude": {"TotalDegrees": "312.5122"}, + "PlanetRasiD1Sign": {"Name": "Aquarius"}, + } + } + } + } + }, + } + ), + encoding="utf-8", + ) + monkeypatch.setattr(three_engine_parity_runner, "VEDASTRO_ARTIFACT_DIR", artifact_root) + + report = build_public_case_replay(output_dir=tmp_path / "out", allow_vedastro_network=True) + rows = {(row["section"], row["field"]): row for row in report["comparison_rows"]} + + assert rows[("D1", "Sun.sign")]["status"] == "match" + assert rows[("D1", "Sun.longitude")]["status"] == "match" diff --git a/tests/test_vedastro_gateway.py b/tests/test_vedastro_gateway.py index 8bee7be4..a136debb 100644 --- a/tests/test_vedastro_gateway.py +++ b/tests/test_vedastro_gateway.py @@ -1,5 +1,7 @@ from __future__ import annotations +import json + def test_gateway_status_defaults_to_local_first_cn_safe(monkeypatch): from scripts import vedastro_gateway @@ -58,6 +60,17 @@ def test_gateway_status_exposes_official_readiness_gate(monkeypatch): assert status["official_readiness"]["free_tier_possible_with_cache_queue"] is True +def test_gateway_status_never_exposes_vedastro_secret(monkeypatch): + from scripts import vedastro_gateway + + monkeypatch.setenv("JYOTISH_SKIP_LOCAL_ENV", "1") + monkeypatch.setenv("VEDASTRO_API_KEY", "sk_live_test_secret") + status = vedastro_gateway.gateway_status() + text = json.dumps(status, sort_keys=True) + assert "sk_live_test_secret" not in text + assert status["credential_configured"] is True + + def test_gateway_run_packet_uses_user_entrypoint_and_marks_not_all_641(monkeypatch): from scripts import vedastro_gateway diff --git a/tests/test_vedastro_official_artifact_manifest.py b/tests/test_vedastro_official_artifact_manifest.py new file mode 100644 index 00000000..b974e65a --- /dev/null +++ b/tests/test_vedastro_official_artifact_manifest.py @@ -0,0 +1,26 @@ +import json +from pathlib import Path + + +def test_official_full_snapshot_artifact_manifest_lists_raw_response(tmp_path, monkeypatch): + from scripts import vedastro_service_adapter as adapter + + monkeypatch.setattr(adapter, "ARTIFACT_DIR", tmp_path) + artifact = tmp_path / "official_full_snapshot-abc-def.json" + artifact.write_text( + json.dumps( + { + "status": "ok", + "operation": "official_full_snapshot", + "raw_response": {"source": "vedastro_official_full_snapshot", "sections": {"ok": True}}, + "request_manifest": {"case_id": "synthetic"}, + "snapshot_sections": {"chart_core": {"Status": "Pass"}}, + } + ), + encoding="utf-8", + ) + + manifest = adapter.list_official_full_snapshot_artifacts() + assert manifest["artifact_count"] == 1 + assert manifest["artifacts"][0]["official_raw_response_available"] is True + assert manifest["artifacts"][0]["path"] == str(artifact) diff --git a/tests/test_vedastro_official_mcp_bridge.py b/tests/test_vedastro_official_mcp_bridge.py index 30ecc06c..89debf17 100644 --- a/tests/test_vedastro_official_mcp_bridge.py +++ b/tests/test_vedastro_official_mcp_bridge.py @@ -158,7 +158,7 @@ def test_vedastro_official_mcp_bridge_can_call_tool_against_mock_server() -> Non "--tool", "get_dasa_at_time", "--arguments-json", - '{"birth_date":"17/04/REDACTED_YEAR","check_date":"04/07/2026"}', + '{"birth_date":"17/04/1990","check_date":"04/07/2026"}', ], cwd=ROOT, text=True, @@ -175,4 +175,4 @@ def test_vedastro_official_mcp_bridge_can_call_tool_against_mock_server() -> Non assert report["operation"] == "call_tool" assert report["tool_name"] == "get_dasa_at_time" assert report["result"]["content"][0]["text"] == "Saturn/Venus" - assert seen_arguments["birth_date"] == "17/04/REDACTED_YEAR" + assert seen_arguments["birth_date"] == "17/04/1990" diff --git a/tests/test_vedastro_service_adapter_executor.py b/tests/test_vedastro_service_adapter_executor.py index f35bf484..548572f6 100644 --- a/tests/test_vedastro_service_adapter_executor.py +++ b/tests/test_vedastro_service_adapter_executor.py @@ -67,7 +67,7 @@ def test_vedastro_official_subprocesses_use_adapter_timeout(monkeypatch) -> None monkeypatch.setattr(adapter.subprocess, "run", fake_run) case = { - "year": REDACTED_YEAR, + "year": 1990, "month": 4, "day": 17, "hour": 14, @@ -97,7 +97,7 @@ def test_vedastro_official_subprocess_timeouts_are_controlled(monkeypatch) -> No monkeypatch.setattr(adapter.subprocess, "run", fake_timeout) case = { - "year": REDACTED_YEAR, + "year": 1990, "month": 4, "day": 17, "hour": 14, @@ -151,7 +151,7 @@ def test_vedastro_official_snapshot_stops_when_foreground_budget_is_exhausted(mo ticks = iter([100.0, 104.1]) monkeypatch.setattr(adapter.time, "monotonic", lambda: next(ticks)) result = adapter._run_official_full_snapshot_case({ - "year": REDACTED_YEAR, + "year": 1990, "month": 4, "day": 17, "hour": 14, @@ -213,7 +213,7 @@ def test_vedastro_official_snapshot_skips_bridge_after_runner_consumes_budget(mo ticks = iter([100.0, 100.1, 104.2]) monkeypatch.setattr(adapter.time, "monotonic", lambda: next(ticks)) result = adapter._run_official_full_snapshot_case({ - "year": REDACTED_YEAR, + "year": 1990, "month": 4, "day": 17, "hour": 14, @@ -275,7 +275,7 @@ def test_vedastro_official_snapshot_budget_does_not_mask_mock_rest_endpoint(monk monkeypatch.setattr(adapter.time, "monotonic", lambda: next(ticks)) result = adapter._run_official_full_snapshot_case({ - "year": REDACTED_YEAR, + "year": 1990, "month": 4, "day": 17, "hour": 14, @@ -549,7 +549,8 @@ def test_vedastro_service_adapter_posts_official_search_events_contract() -> Non assert report["source_metadata"]["endpoint"].endswith("/api") assert report["source_metadata"]["official_endpoint_path"] == "/Calculate/SearchEvents" assert report["source_metadata"]["official_request_profile"]["method"] == "POST" - assert report["source_metadata"]["official_request_profile"]["headers"]["x-api-key"] == "[redacted]" + metadata_headers = report["source_metadata"]["official_request_profile"]["headers"] + assert metadata_headers == {"Content-Type": "application/json"} assert report["source_metadata"]["official_request_profile_hash"] assert report["source_metadata"]["request_hash"] != report["source_metadata"]["official_request_profile_hash"]