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
This commit is contained in:
732642856
2026-07-20 01:28:57 +08:00
committed by GitHub
parent b4fee10439
commit 64fcb62f83
12 changed files with 228 additions and 7 deletions
+4
View File
@@ -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. `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. 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.
@@ -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."
}
+10 -2
View File
@@ -20,6 +20,7 @@ import secrets
import sqlite3 import sqlite3
import threading import threading
import time import time
import tempfile
import uuid import uuid
from concurrent.futures import ThreadPoolExecutor from concurrent.futures import ThreadPoolExecutor
from datetime import datetime, timedelta from datetime import datetime, timedelta
@@ -1312,7 +1313,10 @@ MAX_IMPORT_FILE_BYTES = 1536 * 1024
MAX_IMPORT_TEXT_CHARS = 500_000 MAX_IMPORT_TEXT_CHARS = 500_000
MAX_REPORT_HTML_CHARS = 1_200_000 MAX_REPORT_HTML_CHARS = 1_200_000
MAX_REPORT_BASE64_BYTES = 8 * 1024 * 1024 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 = { API_COMMAND_MAP = {
'chart': '/api/chart', 'chart': '/api/chart',
@@ -6986,7 +6990,11 @@ class JyotishAPIHandler(BaseHTTPRequestHandler):
'lat': lat, 'lat': lat,
'lon': lon, 'lon': lon,
'tz': tz, '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['can_apply'] = False
result.setdefault('reasons', []).append('three_engine_parity_not_passed') result.setdefault('reasons', []).append('three_engine_parity_not_passed')
return { return {
+11
View File
@@ -4923,6 +4923,17 @@ def cmd_full_reading(args):
) )
except Exception as d11_error: except Exception as d11_error:
varga_result["D11_Rudramsa"] = {"error": str(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 report['modules']['varga_full'] = varga_result
# v6.1.7: Re-run Yoga with D9/D60 context after varga-full is available. # v6.1.7: Re-run Yoga with D9/D60 context after varga-full is available.
@@ -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))
+54 -2
View File
@@ -51,7 +51,44 @@ def _jyotishganit_d1(case: dict[str, Any]) -> dict[str, str]:
sys.path.remove(str(JYOTISHGANIT_ROOT)) 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.""" """Compare local/PyJHora/jyotishganit D1 without persisting private input."""
required = {"year", "month", "day", "hour", "minute", "lat", "lon", "tz"} required = {"year", "month", "day", "hour", "minute", "lat", "lon", "tz"}
if not required <= set(case): if not required <= set(case):
@@ -66,6 +103,21 @@ def build_packet(case: dict[str, Any]) -> dict[str, Any]:
outputs[name] = {} outputs[name] = {}
engine_status[name] = f"blocked:{exc.__class__.__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] 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 { return {
"scope": "request_level_three_engine_d1_parity", "scope": "request_level_three_engine_d1_parity",
"case_hash": case_hash(case), "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), "match_count": sum(row["status"] == "match" for row in rows),
"mismatch_count": sum(row["status"] == "mismatch" for row in rows), "mismatch_count": sum(row["status"] == "mismatch" for row in rows),
"rows": rows, "rows": rows,
"vedastro": {"status": "requires_gateway_raw_archive"}, "vedastro": vedastro,
"can_confirm": False, "can_confirm": False,
"boundary": "D1 parity alone never confirms a rectified minute; VedAstro raw and domain-level parity remain required.", "boundary": "D1 parity alone never confirms a rectified minute; VedAstro raw and domain-level parity remain required.",
} }
+2 -1
View File
@@ -10,6 +10,7 @@ import signal
import socket import socket
import subprocess import subprocess
import sys import sys
import tempfile
import time import time
from pathlib import Path from pathlib import Path
from typing import Any from typing import Any
@@ -18,7 +19,7 @@ from typing import Any
ROOT = Path(__file__).resolve().parents[1] ROOT = Path(__file__).resolve().parents[1]
APP = ROOT / "jyotish-app" APP = ROOT / "jyotish-app"
API_SERVER = ROOT / "scripts" / "jyotish_api_server.py" 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") SYSTEM_CHROME = Path("/Applications/Google Chrome.app/Contents/MacOS/Google Chrome")
OFFLINE_CONSOLE_ERROR_MARKERS = [ OFFLINE_CONSOLE_ERROR_MARKERS = [
"ERR_CONNECTION_REFUSED", "ERR_CONNECTION_REFUSED",
+2 -1
View File
@@ -8,6 +8,7 @@ import json
import os import os
import subprocess import subprocess
import sys import sys
import tempfile
import time import time
from pathlib import Path from pathlib import Path
from typing import Any from typing import Any
@@ -16,7 +17,7 @@ from typing import Any
ROOT = Path(__file__).resolve().parents[1] ROOT = Path(__file__).resolve().parents[1]
APP = ROOT / "jyotish-app" APP = ROOT / "jyotish-app"
API_SERVER = ROOT / "scripts" / "jyotish_api_server.py" 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]: def run(cmd: list[str], *, cwd: Path = ROOT, timeout: int = 12, check: bool = True) -> subprocess.CompletedProcess[str]:
+37
View File
@@ -404,3 +404,40 @@ def test_gochara_conflict_downgrades_without_verified_timing_claim_red() -> None
assert top["confidence_cap"] == "low" assert top["confidence_cap"] == "low"
assert "gochara_transit" in top["downgrade_reasons"] assert "gochara_transit" in top["downgrade_reasons"]
assert scored["timing_claim_status"] == "exploratory_unvalidated" 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)
+2 -1
View File
@@ -6,6 +6,7 @@ from __future__ import annotations
import re import re
import socket import socket
import subprocess import subprocess
import tempfile
import time import time
import json import json
import os import os
@@ -19,7 +20,7 @@ import pytest
ROOT = Path(__file__).resolve().parents[1] ROOT = Path(__file__).resolve().parents[1]
APP = ROOT / "jyotish-app" APP = ROOT / "jyotish-app"
API_SERVER = ROOT / "scripts" / "jyotish_api_server.py" 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: def read(relative: str) -> str:
@@ -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
@@ -13,3 +13,39 @@ def test_packet_is_private_and_never_confirms(monkeypatch) -> None:
assert "year" not in str(packet) assert "year" not in str(packet)
assert packet["can_confirm"] is False assert packet["can_confirm"] is False
assert packet["vedastro"]["status"] == "requires_gateway_raw_archive" 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)