From 64fcb62f833e6bccd9461d78861350cbf81e5e9d Mon Sep 17 00:00:00 2001 From: 732642856 <732642856@qq.com> Date: Mon, 20 Jul 2026 01:28:57 +0800 Subject: [PATCH] feat: queue VedAstro rectification evidence packets * feat: queue VedAstro rectification evidence packets * fix: make report artifacts portable and normalize d11 * test: use portable runtime smoke temp directory * test: make frontend smoke temp paths portable --- docs/research/pre_work_error_ledger.md | 4 ++ .../minute_rectification_holdout_v1.json | 14 +++++ scripts/jyotish_api_server.py | 12 +++- scripts/jyotish_engine.py | 11 ++++ .../minute_rectification_holdout_validator.py | 48 ++++++++++++++++ scripts/rectification_three_engine_packet.py | 56 ++++++++++++++++++- tests/run_frontend_click_smoke.py | 3 +- tests/run_frontend_runtime_smoke.py | 3 +- tests/test_active_rectification_api.py | 37 ++++++++++++ tests/test_frontend_productization.py | 3 +- ..._minute_rectification_holdout_validator.py | 8 +++ .../test_rectification_three_engine_packet.py | 36 ++++++++++++ 12 files changed, 228 insertions(+), 7 deletions(-) create mode 100644 references/real_case_calibration/minute_rectification_holdout_v1.json create mode 100644 scripts/minute_rectification_holdout_validator.py create mode 100644 tests/test_minute_rectification_holdout_validator.py diff --git a/docs/research/pre_work_error_ledger.md b/docs/research/pre_work_error_ledger.md index 9ba7759b..b443427d 100644 --- a/docs/research/pre_work_error_ledger.md +++ b/docs/research/pre_work_error_ledger.md @@ -147,3 +147,7 @@ done `frontend` local `npm run build` ends during Next.js 16.2.10 compile/static generation, both without configuration and with the CI Supabase placeholders. It leaves no `.next/BUILD_ID` or `.next/prerender-manifest.json`, and no application stack trace. Frontend contracts (`270 passed`), lint, and the selected Python commercial workflow regressions (`128 passed`) remain green. Prevention: do not equate this local host failure with an astrology capability regression. Treat GitHub Actions Node 22 build evidence as the deployment gate before merge. Keep VedAstro `premium_key_missing` and official raw snapshot status explicitly degraded. + +## ERR-085 | Public production health cannot prove release identity, database migration, or authenticated workflow | active 2026-07-19 + +`https://jyotisha.chat` homepage and `/api/health` are reachable and healthy, but those responses do not expose a deployed Git SHA, Supabase migration ledger, evidence-packet TTL policy, or an authorized test-account session. Do not treat HTTP `200` as full release acceptance. Close this only through a deployment-attested SHA plus read-only migration/TTL evidence and an authorized browser acceptance account. diff --git a/references/real_case_calibration/minute_rectification_holdout_v1.json b/references/real_case_calibration/minute_rectification_holdout_v1.json new file mode 100644 index 00000000..1af9e696 --- /dev/null +++ b/references/real_case_calibration/minute_rectification_holdout_v1.json @@ -0,0 +1,14 @@ +{ + "benchmark_id": "minute_rectification_holdout_v1", + "scope": "public_birth_minute_rectification", + "status": "blocked_awaiting_public_aa_cases", + "truth_policy": "Only independently published AA birth-minute records and independently sourced dated life events qualify.", + "negative_control_policy": "Each case requires blinded false-minute candidates on both sides of the published minute; generated timing-date controls are not substitutes.", + "minimum_gate": { + "public_aa_cases": 20, + "events_per_case": 3, + "negative_minutes_per_case": 4 + }, + "cases": [], + "boundary": "This empty protocol is not evidence of minute-level accuracy. It blocks any verified-minute claim until independent public cases and negative controls are frozen." +} diff --git a/scripts/jyotish_api_server.py b/scripts/jyotish_api_server.py index 458f439b..12d082a9 100644 --- a/scripts/jyotish_api_server.py +++ b/scripts/jyotish_api_server.py @@ -20,6 +20,7 @@ import secrets import sqlite3 import threading import time +import tempfile import uuid from concurrent.futures import ThreadPoolExecutor from datetime import datetime, timedelta @@ -1312,7 +1313,10 @@ MAX_IMPORT_FILE_BYTES = 1536 * 1024 MAX_IMPORT_TEXT_CHARS = 500_000 MAX_REPORT_HTML_CHARS = 1_200_000 MAX_REPORT_BASE64_BYTES = 8 * 1024 * 1024 -REPORT_ARTIFACT_DIR = os.path.join('/private/tmp', 'jyotish-reports') +REPORT_ARTIFACT_DIR = os.environ.get( + 'JYOTISH_REPORT_ARTIFACT_DIR', + os.path.join(tempfile.gettempdir(), 'jyotish-reports'), +) API_COMMAND_MAP = { 'chart': '/api/chart', @@ -6986,7 +6990,11 @@ class JyotishAPIHandler(BaseHTTPRequestHandler): 'lat': lat, 'lon': lon, 'tz': tz, - }) + }, + enqueue_vedastro_gateway=True, + vedastro_question='High-rigor birth-time rectification evidence packet', + vedastro_reference_date=datetime.now().strftime('%Y-%m-%d'), + ) result['can_apply'] = False result.setdefault('reasons', []).append('three_engine_parity_not_passed') return { diff --git a/scripts/jyotish_engine.py b/scripts/jyotish_engine.py index 319a6cec..65f503d9 100644 --- a/scripts/jyotish_engine.py +++ b/scripts/jyotish_engine.py @@ -4923,6 +4923,17 @@ def cmd_full_reading(args): ) except Exception as d11_error: varga_result["D11_Rudramsa"] = {"error": str(d11_error)} + d11 = varga_result.get("D11_Rudramsa") + if isinstance(d11, dict) and isinstance(d11.get("Ascendant"), dict): + asc_sign = d11["Ascendant"].get("sign_idx") + if isinstance(asc_sign, int): + d11["planets"] = { + name: {**data, "house": ((int(data["sign_idx"]) - asc_sign) % 12) + 1} + for name, data in d11.items() + if name not in {"_meta", "Ascendant", "planets"} + and isinstance(data, dict) + and isinstance(data.get("sign_idx"), int) + } report['modules']['varga_full'] = varga_result # v6.1.7: Re-run Yoga with D9/D60 context after varga-full is available. diff --git a/scripts/minute_rectification_holdout_validator.py b/scripts/minute_rectification_holdout_validator.py new file mode 100644 index 00000000..3822e38a --- /dev/null +++ b/scripts/minute_rectification_holdout_validator.py @@ -0,0 +1,48 @@ +#!/usr/bin/env python3 +"""Validate the public, minute-specific rectification holdout gate.""" +from __future__ import annotations + +import json +from pathlib import Path +from typing import Any + +ROOT = Path(__file__).resolve().parents[1] +DEFAULT_MANIFEST = ROOT / "references" / "real_case_calibration" / "minute_rectification_holdout_v1.json" + + +def validate(manifest_path: Path = DEFAULT_MANIFEST) -> dict[str, Any]: + manifest = json.loads(manifest_path.read_text(encoding="utf-8")) + gate = manifest.get("minimum_gate") if isinstance(manifest.get("minimum_gate"), dict) else {} + cases = manifest.get("cases") if isinstance(manifest.get("cases"), list) else [] + valid_cases = 0 + invalid: list[str] = [] + for case in cases: + if not isinstance(case, dict): + invalid.append("non_object_case") + continue + birth = case.get("birth_source") if isinstance(case.get("birth_source"), dict) else {} + events = case.get("events") if isinstance(case.get("events"), list) else [] + negatives = case.get("negative_minutes") if isinstance(case.get("negative_minutes"), list) else [] + required = birth.get("time_accuracy_rating") == "AA" and bool(birth.get("url")) + required = required and len(events) >= int(gate.get("events_per_case", 3)) + required = required and len(negatives) >= int(gate.get("negative_minutes_per_case", 4)) + if required: + valid_cases += 1 + else: + invalid.append(str(case.get("case_id") or "unnamed_case")) + needed = int(gate.get("public_aa_cases", 20)) + status = "ready_for_blind_replay" if valid_cases >= needed else "blocked_awaiting_public_aa_cases" + return { + "scope": "minute_rectification_holdout_validation", + "benchmark_id": manifest.get("benchmark_id"), + "status": status, + "valid_public_aa_cases": valid_cases, + "minimum_public_aa_cases": needed, + "invalid_cases": invalid, + "verified_minute_claim_allowed": False, + "boundary": manifest.get("boundary"), + } + + +if __name__ == "__main__": + print(json.dumps(validate(), ensure_ascii=False, indent=2, sort_keys=True)) diff --git a/scripts/rectification_three_engine_packet.py b/scripts/rectification_three_engine_packet.py index 5d4f9ebb..6f0655bb 100644 --- a/scripts/rectification_three_engine_packet.py +++ b/scripts/rectification_three_engine_packet.py @@ -51,7 +51,44 @@ def _jyotishganit_d1(case: dict[str, Any]) -> dict[str, str]: sys.path.remove(str(JYOTISHGANIT_ROOT)) -def build_packet(case: dict[str, Any]) -> dict[str, Any]: +def _gateway_job_receipt(job: dict[str, Any]) -> dict[str, Any]: + """Return only pollable, non-sensitive VedAstro job state for a packet.""" + archive = job.get("raw_response_archive") + archive = archive if isinstance(archive, dict) else {} + return { + "scope": "vedastro_gateway_job_receipt", + "status": str(job.get("status") or "blocked"), + "job_id": str(job.get("job_id") or ""), + "poll_path": str(job.get("poll_path") or ""), + "raw_response_archive": { + "status": str(archive.get("status") or "unknown"), + "official_raw_response_available": bool(archive.get("official_raw_response_available")), + }, + "boundary": "VedAstro raw response remains server-side; this receipt never returns request data or raw evidence.", + } + + +def _enqueue_vedastro_gateway_job( + case: dict[str, Any], *, question: str = "", reference_date: str = "" +) -> dict[str, Any]: + from scripts.vedastro_gateway import enqueue_gateway_job + + job = enqueue_gateway_job( + case, + question=question, + themes=["rectification"], + reference_date=reference_date, + ) + return _gateway_job_receipt(job) + + +def build_packet( + case: dict[str, Any], + *, + enqueue_vedastro_gateway: bool = False, + vedastro_question: str = "", + vedastro_reference_date: str = "", +) -> dict[str, Any]: """Compare local/PyJHora/jyotishganit D1 without persisting private input.""" required = {"year", "month", "day", "hour", "minute", "lat", "lon", "tz"} if not required <= set(case): @@ -66,6 +103,21 @@ def build_packet(case: dict[str, Any]) -> dict[str, Any]: outputs[name] = {} engine_status[name] = f"blocked:{exc.__class__.__name__}" rows = [{"planet": planet, "values": {name: data.get(planet) for name, data in outputs.items()}, "status": "match" if len({data.get(planet) for data in outputs.values()}) == 1 else "mismatch"} for planet in PLANETS] + vedastro = {"status": "requires_gateway_raw_archive"} + if enqueue_vedastro_gateway: + try: + vedastro = _enqueue_vedastro_gateway_job( + case, + question=vedastro_question, + reference_date=vedastro_reference_date, + ) + except Exception as exc: + vedastro = { + "scope": "vedastro_gateway_job_receipt", + "status": "blocked", + "reason": f"gateway_enqueue_failed:{exc.__class__.__name__}", + "boundary": "VedAstro raw response remains server-side; no raw evidence was returned.", + } return { "scope": "request_level_three_engine_d1_parity", "case_hash": case_hash(case), @@ -73,7 +125,7 @@ def build_packet(case: dict[str, Any]) -> dict[str, Any]: "match_count": sum(row["status"] == "match" for row in rows), "mismatch_count": sum(row["status"] == "mismatch" for row in rows), "rows": rows, - "vedastro": {"status": "requires_gateway_raw_archive"}, + "vedastro": vedastro, "can_confirm": False, "boundary": "D1 parity alone never confirms a rectified minute; VedAstro raw and domain-level parity remain required.", } diff --git a/tests/run_frontend_click_smoke.py b/tests/run_frontend_click_smoke.py index d79b2538..fd5e7816 100644 --- a/tests/run_frontend_click_smoke.py +++ b/tests/run_frontend_click_smoke.py @@ -10,6 +10,7 @@ import signal import socket import subprocess import sys +import tempfile import time from pathlib import Path from typing import Any @@ -18,7 +19,7 @@ from typing import Any ROOT = Path(__file__).resolve().parents[1] APP = ROOT / "jyotish-app" API_SERVER = ROOT / "scripts" / "jyotish_api_server.py" -TMP = Path(os.environ.get("TMPDIR", "/private/tmp")) +TMP = Path(os.environ.get("TMPDIR") or tempfile.gettempdir()) SYSTEM_CHROME = Path("/Applications/Google Chrome.app/Contents/MacOS/Google Chrome") OFFLINE_CONSOLE_ERROR_MARKERS = [ "ERR_CONNECTION_REFUSED", diff --git a/tests/run_frontend_runtime_smoke.py b/tests/run_frontend_runtime_smoke.py index 3cb7ae7e..749bf577 100644 --- a/tests/run_frontend_runtime_smoke.py +++ b/tests/run_frontend_runtime_smoke.py @@ -8,6 +8,7 @@ import json import os import subprocess import sys +import tempfile import time from pathlib import Path from typing import Any @@ -16,7 +17,7 @@ from typing import Any ROOT = Path(__file__).resolve().parents[1] APP = ROOT / "jyotish-app" API_SERVER = ROOT / "scripts" / "jyotish_api_server.py" -TMP = Path(os.environ.get("TMPDIR", "/private/tmp")) +TMP = Path(os.environ.get("TMPDIR") or tempfile.gettempdir()) def run(cmd: list[str], *, cwd: Path = ROOT, timeout: int = 12, check: bool = True) -> subprocess.CompletedProcess[str]: diff --git a/tests/test_active_rectification_api.py b/tests/test_active_rectification_api.py index 662171be..2e07d8c7 100644 --- a/tests/test_active_rectification_api.py +++ b/tests/test_active_rectification_api.py @@ -404,3 +404,40 @@ def test_gochara_conflict_downgrades_without_verified_timing_claim_red() -> None assert top["confidence_cap"] == "low" assert "gochara_transit" in top["downgrade_reasons"] assert scored["timing_claim_status"] == "exploratory_unvalidated" + + +def test_high_rigor_event_rectification_queues_vedastro_packet(monkeypatch) -> None: + monkeypatch.setattr( + "scripts.rectification_three_engine_packet._enqueue_vedastro_gateway_job", + lambda *_args, **_kwargs: { + "scope": "vedastro_gateway_job_receipt", + "status": "queued", + "job_id": "vgw_rectification", + "poll_path": "/api/vedastro_gateway/jobs/vgw_rectification", + "raw_response_archive": { + "status": "pending", + "official_raw_response_available": False, + }, + "boundary": "VedAstro raw response remains server-side; this receipt never returns request data or raw evidence.", + }, + ) + result = _handler()._compute_active_rectification_events( + { + "birth_date": "1993-04-17", + "start_time": "14:29", + "end_time": "14:31", + "lat": 36.683333, + "lon": 114.35, + "tz": 8, + "high_rigor": True, + "events": [ + {"id": "5cb071d6-6d99-46be-85dc-a9bf59ef6ac5", "domain": "education", "date": "2011-09", "precision": "month"}, + {"id": "0790866c-ad5e-4a45-b2b4-a5c73f6be6ea", "domain": "career", "date": "2019-07-01", "precision": "day"}, + {"id": "0ef52e51-ab5f-453b-81e5-adb44a929224", "domain": "relationship", "date": "2021", "precision": "year"}, + ], + } + ) + receipt = result["three_engine_packet"]["vedastro"] + assert receipt["status"] == "queued" + assert receipt["job_id"] == "vgw_rectification" + assert "1993" not in str(receipt) diff --git a/tests/test_frontend_productization.py b/tests/test_frontend_productization.py index 85ce4363..0d491067 100644 --- a/tests/test_frontend_productization.py +++ b/tests/test_frontend_productization.py @@ -6,6 +6,7 @@ from __future__ import annotations import re import socket import subprocess +import tempfile import time import json import os @@ -19,7 +20,7 @@ import pytest ROOT = Path(__file__).resolve().parents[1] APP = ROOT / "jyotish-app" API_SERVER = ROOT / "scripts" / "jyotish_api_server.py" -TMP = Path(os.environ.get("TMPDIR", "/private/tmp")) +TMP = Path(os.environ.get("TMPDIR") or tempfile.gettempdir()) def read(relative: str) -> str: diff --git a/tests/test_minute_rectification_holdout_validator.py b/tests/test_minute_rectification_holdout_validator.py new file mode 100644 index 00000000..455085e9 --- /dev/null +++ b/tests/test_minute_rectification_holdout_validator.py @@ -0,0 +1,8 @@ +from scripts.minute_rectification_holdout_validator import validate + + +def test_empty_public_minute_protocol_blocks_verified_claims() -> None: + report = validate() + assert report["status"] == "blocked_awaiting_public_aa_cases" + assert report["verified_minute_claim_allowed"] is False + assert report["valid_public_aa_cases"] == 0 diff --git a/tests/test_rectification_three_engine_packet.py b/tests/test_rectification_three_engine_packet.py index c471e2b7..3456fd00 100644 --- a/tests/test_rectification_three_engine_packet.py +++ b/tests/test_rectification_three_engine_packet.py @@ -13,3 +13,39 @@ def test_packet_is_private_and_never_confirms(monkeypatch) -> None: assert "year" not in str(packet) assert packet["can_confirm"] is False assert packet["vedastro"]["status"] == "requires_gateway_raw_archive" + + +def test_high_rigor_packet_queues_safe_vedastro_receipt_without_raw(monkeypatch) -> None: + monkeypatch.setattr("scripts.rectification_three_engine_packet._local_d1", lambda _: {"Sun": "Aries"}) + monkeypatch.setattr("scripts.rectification_three_engine_packet._pyjhora_d1", lambda _: {"Sun": "Aries"}) + monkeypatch.setattr("scripts.rectification_three_engine_packet._jyotishganit_d1", lambda _: {"Sun": "Aries"}) + monkeypatch.setattr( + "scripts.vedastro_gateway.enqueue_gateway_job", + lambda *_args, **_kwargs: { + "job_id": "vgw_safe_receipt", + "status": "queued", + "poll_path": "/api/vedastro_gateway/jobs/vgw_safe_receipt", + "request": CASE, + "result": {"official_raw_response": {"private": "never expose"}}, + "raw_response_archive": { + "status": "pending", + "official_raw_response_available": False, + }, + }, + ) + + packet = build_packet(CASE, enqueue_vedastro_gateway=True) + + assert packet["vedastro"] == { + "scope": "vedastro_gateway_job_receipt", + "status": "queued", + "job_id": "vgw_safe_receipt", + "poll_path": "/api/vedastro_gateway/jobs/vgw_safe_receipt", + "raw_response_archive": { + "status": "pending", + "official_raw_response_available": False, + }, + "boundary": "VedAstro raw response remains server-side; this receipt never returns request data or raw evidence.", + } + assert "year" not in str(packet) + assert "private" not in str(packet)