Merge remote-tracking branch 'origin/main' into codex/cross-project-contract

This commit is contained in:
732642856
2026-07-17 12:50:00 +08:00
162 changed files with 51765 additions and 2630 deletions
+7 -2
View File
@@ -12,7 +12,6 @@ from __future__ import annotations
from typing import Dict, Any
from jaimini import calc_special_lagnas_precise
from prashna import calc_gulika_simple
from varga import calc_all_vargas
@@ -45,7 +44,13 @@ def analyze_adhana_candidates(payload: Dict[str, Any]) -> Dict[str, Any]:
asc_lon = float(payload["asc_lon"])
sun_lon = float(payload["sun_lon"])
moon_lon = float(payload["moon_lon"])
gulika_lon = float(payload.get("gulika_lon", calc_gulika_simple(asc_lon, sun_lon, int(payload.get("weekday", 0)))))
if "gulika_lon" not in payload:
return {
"status": "blocked",
"reason": "exact_gulika_longitude_required_for_adhana_scaffold",
"blocked_layers": ["Gulika", "Adhana candidate points"],
}
gulika_lon = float(payload["gulika_lon"])
year = int(payload["year"])
month = int(payload["month"])
+3
View File
@@ -23,10 +23,13 @@ ALLOWED_STATUS = {
"covered",
"complete",
"partial",
"blocked",
"knowledge-only",
"workflow-only",
"not-integrated",
"missing",
"guarded",
"comparison-only",
}
REQUIRED_TECHNIQUE_FIELDS = {
"name": str,
+13 -2
View File
@@ -5,6 +5,7 @@ from __future__ import annotations
import argparse
import json
from pathlib import Path
try:
from diagnose_vedastro_mode import build_report as build_vedastro_report
@@ -21,13 +22,22 @@ except Exception: # pragma: no cover - import path varies in tests/CLI
REQUIRED_PARITY_OUTPUTS = ["D1", "D9", "D10", "D2", "D4", "Vimshottari", "Shadbala", "Ashtakavarga"]
def _public_pyjhora_parity_manifest() -> dict:
path = Path("references/oracle/pyjhora_same_chart_parity_public_smoke_manifest.json")
try:
return json.loads(path.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError):
return {"status": "not_available", "tested": False}
def _same_chart_parity_contract(engines: dict) -> dict:
pyjhora_public = _public_pyjhora_parity_manifest()
engine_states = {}
for name, engine in engines.items():
available = engine["status"] == "available"
engine_states[name] = {
"available": available,
"tested": False,
"tested": bool(name == "PyJHora/JHora" and pyjhora_public.get("tested")),
"blocked": not available,
"blocking_reason": "" if available else engine["status"],
}
@@ -60,7 +70,8 @@ def _same_chart_parity_contract(engines: dict) -> dict:
},
"engine_states": engine_states,
"replay_manifest": replay_manifest,
"boundary": "This is a parity contract, not proof that the same-chart comparison has run.",
"partial_verifications": {"PyJHora/JHora": pyjhora_public},
"boundary": "This is a parity contract. Public PyJHora partial verification does not close missing outputs or other engine raw-oracle requirements.",
}
+338 -154
View File
@@ -15,13 +15,13 @@ import json, sys, os, math
import importlib.util
import hashlib
import re
import sqlite3
import secrets
import sqlite3
import threading
import time
from concurrent.futures import ThreadPoolExecutor
from datetime import datetime, timedelta
from http.server import HTTPServer, BaseHTTPRequestHandler, ThreadingHTTPServer
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path
from urllib.parse import urlparse
@@ -45,13 +45,27 @@ try:
except ModuleNotFoundError: # pragma: no cover - script execution path
from unified_consultation_orchestrator import UnifiedConsultationOrchestrator
try:
from scripts.western_oracle_adapter import build_packet_from_oracle_payload
from scripts.skill_experience import (
build_rectification_questionnaire,
score_rectification_answers,
summarize_execution_status,
)
except ModuleNotFoundError: # pragma: no cover - script execution path
from western_oracle_adapter import build_packet_from_oracle_payload
from skill_experience import (
build_rectification_questionnaire,
score_rectification_answers,
summarize_execution_status,
)
try:
from scripts.candidate_time_sensitivity_scan import scan_candidate_times
except ModuleNotFoundError: # pragma: no cover - script execution path
from candidate_time_sensitivity_scan import scan_candidate_times
try:
from scripts.western_oracle_adapter import build_packet_from_oracle_payload
from scripts.western_chart_engine import build_tropical_western_evidence_packet
from scripts.western_timing_engine import build_timing_techniques
except ModuleNotFoundError: # pragma: no cover - script execution path
from western_oracle_adapter import build_packet_from_oracle_payload
from western_chart_engine import build_tropical_western_evidence_packet
from western_timing_engine import build_timing_techniques
@@ -71,16 +85,6 @@ _RATE_LIMIT_LOCK = threading.Lock()
_RATE_LIMIT_BUCKETS: dict[str, tuple[float, int]] = {}
def summarize_execution_status(result: dict | None) -> dict:
result = result if isinstance(result, dict) else {}
fallback = str(result.get('fallback_reason') or '')
official = 'official_blocked' if 'VedAstro official snapshot blocked' in fallback else result.get('official_evidence_status', 'unknown')
return {
'official_evidence_status': official,
'fallback_reason': result.get('fallback_reason'),
}
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 {}
@@ -97,18 +101,6 @@ def build_evidence_packet_view(job_record: dict | None) -> dict:
}
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 _rate_limit_per_minute() -> int:
raw = str(os.environ.get('JYOTISH_API_RATE_LIMIT_PER_MINUTE', '120')).strip()
try:
@@ -131,6 +123,45 @@ def enforce_rate_limit(client_id: str, *, now: float | None = None) -> None:
_RATE_LIMIT_BUCKETS[client_id] = (window, count + 1)
def async_job_runtime_status() -> dict:
scopes = (_HIGH_RIGOR_JOB_SCOPE, _API_CHART_CACHE_SCOPE)
if _async_job_backend() == "sqlite":
with _sqlite_job_connection() as connection:
counts = {
scope: connection.execute("SELECT COUNT(*) FROM async_jobs WHERE scope = ?", (scope,)).fetchone()[0]
for scope in scopes
}
storage = "sqlite_single_host"
else:
counts = {
scope: len(list(_async_job_dir(scope).glob('*.json')))
if _async_job_dir(scope).is_dir() else 0
for scope in scopes
}
storage = "local_file_single_host"
return {
'scope': 'async_job_runtime_status',
'storage': storage,
'worker_count': _ASYNC_JOB_WORKERS,
'queue_size': _ASYNC_JOB_QUEUE_SIZE,
'ttl_seconds': _async_job_ttl_seconds(),
'record_counts': counts,
'boundary': 'SQLite supports single-host persistence. No distributed queue or multi-node worker guarantee.',
}
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,
@@ -425,8 +456,15 @@ def execute_consultation_workflow(
question = body.get('question') or ''
entry_mode = body.get('entry_mode', 'direct_chart')
high_rigor = bool(body.get('return_high_rigor_shape'))
try:
from scripts.three_engine_parity_replay_validator import validate_manifest
except ModuleNotFoundError: # pragma: no cover - direct script execution
from three_engine_parity_replay_validator import validate_manifest
external_parity_gate = validate_manifest(
Path(__file__).resolve().parents[1] / 'references/oracle/three_engine_parity_replay_manifest.json'
)
route_packet = _UNIFIED_CONSULTATION_ORCHESTRATOR.resolve_route(question, themes)
western_evidence_packet = _western_evidence_packet_from_body(body, route_packet)
western_evidence_packet = _western_evidence_packet_from_body(body, route_packet, birth_payload=birth_payload)
unified_contract = _UNIFIED_CONSULTATION_ORCHESTRATOR.shared_contract(
entry_mode=entry_mode,
question=question,
@@ -476,6 +514,15 @@ def execute_consultation_workflow(
result['western_evidence_packet'] = western_evidence_packet
if body.get('return_high_rigor_shape'):
result['endpoint'] = 'high_rigor_workflow'
result['high_rigor_external_parity'] = {
'status': 'pass' if external_parity_gate.get('status') == 'pass' else 'blocked',
'parity_status': external_parity_gate.get('status'),
'reason': external_parity_gate.get('blocked_reason') or 'three_engine_parity_not_passed',
'require_external_parity': bool(body.get('require_external_parity')),
}
if body.get('require_external_parity') and external_parity_gate.get('status') != 'pass':
result['success'] = False
result['blocked_reason'] = 'external_parity_not_passed'
return result
chart = dict(chart_override) if isinstance(chart_override, dict) else {}
@@ -637,6 +684,7 @@ def execute_consultation_workflow(
'audited_remedies': audited_remedies,
'vedastro_official': vedastro_official,
'runtime_truth': runtime_truth,
'external_parity_gate': external_parity_gate,
'interpretation_source_runtime_coverage': interpretation_source_runtime_coverage,
'machine_evidence_packet': machine_evidence_packet,
'consumer_context': consumer_context,
@@ -650,6 +698,16 @@ def execute_consultation_workflow(
'domain-relevant routes execute according to the configured sample/network limits.'
),
}
if high_rigor:
result['high_rigor_external_parity'] = {
'status': 'pass' if external_parity_gate.get('status') == 'pass' else 'blocked',
'parity_status': external_parity_gate.get('status'),
'reason': external_parity_gate.get('blocked_reason') or 'three_engine_parity_not_passed',
'require_external_parity': bool(body.get('require_external_parity')),
}
if body.get('require_external_parity') and external_parity_gate.get('status') != 'pass':
result['success'] = False
result['blocked_reason'] = 'external_parity_not_passed'
if body.get('return_high_rigor_shape'):
result['endpoint'] = 'high_rigor_workflow'
return result
@@ -1162,6 +1220,7 @@ DEFAULT_ALLOWED_ORIGINS = {
'http://localhost:5173',
'http://127.0.0.1:5173',
}
DEFAULT_ALLOWED_HOSTS = {'localhost', '127.0.0.1', '::1'}
MAX_REQUEST_BYTES = 2 * 1024 * 1024
MAX_IMPORT_FILE_BYTES = 1536 * 1024
MAX_IMPORT_TEXT_CHARS = 500_000
@@ -1318,6 +1377,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)
@@ -1330,12 +1400,21 @@ class JyotishAPIHandler(BaseHTTPRequestHandler):
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'}:
allowed_hosts = getattr(self.server, 'allowed_hosts', DEFAULT_ALLOWED_HOSTS)
if host and host not in allowed_hosts:
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')
if urlparse(self.path).path.startswith('/api/'):
client = getattr(self, 'client_address', ('unknown',))[0]
enforce_rate_limit(str(client))
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')
@@ -1382,12 +1461,37 @@ class JyotishAPIHandler(BaseHTTPRequestHandler):
}
def do_OPTIONS(self):
self._json({})
try:
self._enforce_request_security()
self._json({})
except RateLimited as exc:
self._error_json(str(exc), 429, 'ERR_RATE_LIMITED')
except Forbidden as exc:
self._error_json(str(exc), 403, 'ERR_FORBIDDEN')
def do_GET(self):
path = urlparse(self.path).path
try:
if path == '/api/health':
self._enforce_request_security()
if path == '/':
page = Path(REPO_ROOT) / 'web' / 'index.html'
if not page.is_file():
self._error_json('Home page unavailable', 404, 'ERR_NOT_FOUND')
else:
self._html(page.read_text(encoding='utf-8'))
elif 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:
@@ -1404,6 +1508,7 @@ class JyotishAPIHandler(BaseHTTPRequestHandler):
'swisseph_version': swisseph_version,
'ayanamsa_default': 'lahiri',
'modules': 'Chart/KP/Synastry/Prashna/Remedies/Dasha/Varga/Jaimini/Ashtakavarga/Shadbala/Yoga/Aspects/Tajika/Muhurta/BhavaChalit/BhavaBala/Sudarshana/Nakshatra/Transit/RectificationGate/CaseValidation/DivisionalYoga/Kakshya',
'async_job_runtime': async_job_runtime_status(),
})
elif path == '/api/cities':
self._json(list(CITY_DB.keys()))
@@ -1438,10 +1543,28 @@ 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:
self._error_json('Not found', 404, 'ERR_NOT_FOUND')
except RateLimited as exc:
self._error_json(str(exc), 429, 'ERR_RATE_LIMITED')
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)
@@ -1452,7 +1575,17 @@ class JyotishAPIHandler(BaseHTTPRequestHandler):
try:
self._enforce_request_security(require_json=True)
body = self._read_json_body()
if path == '/api/chart':
if path == '/api/location/resolve':
city = str(body.get('city') or '').strip()
city_aliases = {'beijing': '北京', 'shanghai': '上海', 'guangzhou': '广州', 'shenzhen': '深圳'}
query = city_aliases.get(city.casefold(), city)
matched = next((name for name in CITY_DB if name.casefold() == query.casefold()), None)
if not matched:
self._error_json('City not found in local city database', 404, 'ERR_CITY_NOT_FOUND')
else:
lat, lon, tz = CITY_DB[matched]
self._json({'status': 'local_city_match', 'city': matched, 'lat': lat, 'lon': lon, 'tz': tz})
elif path == '/api/chart':
result = self._compute_chart(body)
self._json(result)
elif path == '/api/daily_guidance':
@@ -1555,6 +1688,22 @@ 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/sensitivity_scan':
uncertainty = int(body.get('time_uncertainty_minutes') or 30)
step_minutes = int(body.get('step_minutes') or (5 if uncertainty > 15 else 1))
self._json(scan_candidate_times(
body,
uncertainty_minutes=uncertainty,
step_minutes=step_minutes,
))
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)
@@ -1596,12 +1745,16 @@ class JyotishAPIHandler(BaseHTTPRequestHandler):
self._json(result)
else:
self._error_json(f'Unknown endpoint: {path}', 404, 'ERR_NOT_FOUND')
except RateLimited as exc:
self._error_json(str(exc), 429, 'ERR_RATE_LIMITED')
except BadRequest as e:
self._error_json(str(e), 400, 'ERR_BAD_REQUEST')
except Forbidden as e:
self._error_json(str(e), 403, 'ERR_FORBIDDEN')
except UnsupportedMediaType as e:
self._error_json(str(e), 415, 'ERR_UNSUPPORTED_MEDIA_TYPE')
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)
@@ -1641,13 +1794,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)
@@ -2479,7 +2639,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 = {
@@ -2491,13 +2652,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)
@@ -2517,15 +2683,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 = {
@@ -2537,11 +2700,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)
@@ -2561,18 +2729,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')
@@ -4510,29 +4678,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)
@@ -4572,78 +4717,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')
@@ -4804,6 +4940,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 {},
@@ -5416,9 +5554,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,
@@ -5454,6 +5603,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):
@@ -5529,11 +5682,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):
@@ -8190,10 +8351,20 @@ def _parse_allowed_origins(value):
return {item.strip() for item in value.split(',') if item.strip()}
def start_server(port=5200, host='127.0.0.1', allowed_origins=None):
server = HTTPServer((host, port), JyotishAPIHandler)
def _parse_allowed_hosts(value):
if not value:
return DEFAULT_ALLOWED_HOSTS
return {item.strip().lower() for item in value.split(',') if item.strip()}
def start_server(port=5200, host='127.0.0.1', allowed_origins=None, allowed_hosts=None):
cleanup = prune_expired_async_jobs()
server = ThreadingHTTPServer((host, port), JyotishAPIHandler)
server.daemon_threads = True
server.allowed_origins = allowed_origins or DEFAULT_ALLOWED_ORIGINS
server.allowed_hosts = allowed_hosts or DEFAULT_ALLOWED_HOSTS
print(f'Jyotish API v6.9.14 running on http://{host}:{port}')
print(f" Async job cleanup: scanned={cleanup['scanned']}, removed={cleanup['removed']}")
print(f' CORS origins: {", ".join(sorted(server.allowed_origins))}')
print(f' POST /api/chart — 完整星盘计算')
print(f' POST /api/remedies — 补救建议')
@@ -8235,7 +8406,20 @@ if __name__ == '__main__':
default=[],
help='Allowed browser origin; may be repeated. Defaults to local Vite origins.',
)
parser.add_argument(
'--allow-host',
action='append',
default=[],
help='Allowed HTTP Host name; may be repeated. Defaults to loopback hosts.',
)
args = parser.parse_args()
env_origins = _parse_allowed_origins(os.environ.get('JYOTISH_ALLOWED_ORIGINS'))
cli_origins = set(args.allow_origin)
start_server(args.port, host=args.host, allowed_origins=cli_origins or env_origins)
env_hosts = _parse_allowed_hosts(os.environ.get('JYOTISH_ALLOWED_HOSTS'))
cli_hosts = {item.strip().lower() for item in args.allow_host if item.strip()}
start_server(
args.port,
host=args.host,
allowed_origins=cli_origins or env_origins,
allowed_hosts=cli_hosts or env_hosts,
)
+18 -8
View File
@@ -1661,11 +1661,21 @@ def _build_ai_prompt_pack(report):
if isinstance(primary_strict_contract, dict)
else {}
)
try:
from strict_evidence_service import existing_interpretation_source_pack
fallback_source_pack = existing_interpretation_source_pack()
except Exception:
fallback_source_pack = {}
interpretation_source_audit = (
primary_audit.get('interpretation_source_pack')
if isinstance(primary_audit, dict) and isinstance(primary_audit.get('interpretation_source_pack'), dict)
else {}
)
fallback_domain_layers = (
fallback_source_pack.get('domain_invocation_layers')
if isinstance(fallback_source_pack, dict) and isinstance(fallback_source_pack.get('domain_invocation_layers'), dict)
else {}
)
guided_topics = modules.get('guided_topics') if isinstance(modules.get('guided_topics'), list) else build_guided_topics(report)
capability_evidence_pool = build_capability_evidence_pool_summary()
@@ -1747,7 +1757,7 @@ def _build_ai_prompt_pack(report):
'missing_refs': interpretation_source_audit.get('missing_refs') or [],
},
'prediction_boundary_contract': primary_prediction_boundary_contract or {},
'domain_invocation_layers': primary_domain_invocation_layers or {},
'domain_invocation_layers': primary_domain_invocation_layers or fallback_domain_layers or {},
'output_template_contract': primary_output_template_contract or {},
'mevg_collection_queue': primary_mevg_collection_queue or {},
'real_case_calibration_layer': primary_real_case_calibration_layer or {},
@@ -2042,20 +2052,20 @@ def _attach_vedastro_official_full_snapshot(report, args):
def _load_strict_evidence_collector():
try:
from mcp_server import _collect_strict_evidence as collector
from strict_evidence_service import collect_strict_evidence as collector
return collector
except Exception:
mcp_path = os.path.join(ROOT_DIR, 'mcp_server.py')
if not os.path.exists(mcp_path):
service_path = os.path.join(SCRIPT_DIR, 'strict_evidence_service.py')
if not os.path.exists(service_path):
raise
spec = importlib.util.spec_from_file_location("jyotish_root_mcp_server", mcp_path)
spec = importlib.util.spec_from_file_location("jyotish_strict_evidence_service", service_path)
if spec is None or spec.loader is None:
raise ImportError(f"Unable to load mcp_server from {mcp_path}")
raise ImportError(f"Unable to load strict_evidence_service from {service_path}")
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
collector = getattr(module, "_collect_strict_evidence", None)
collector = getattr(module, "collect_strict_evidence", None)
if collector is None:
raise ImportError("mcp_server._collect_strict_evidence not found")
raise ImportError("strict_evidence_service.collect_strict_evidence not found")
return collector
+1 -1
View File
@@ -15,7 +15,7 @@ Muntha 是 Tajika 年运盘(Varshaphala)中的核心指标,
注意:不同流派对 Muntha 计算公式有微小差异。
本实现采用最广泛接受的方法。
"""
from typing import Dict, Optional
from typing import Dict, List, Optional
from datetime import datetime, timedelta
+1 -1
View File
@@ -404,7 +404,7 @@ if __name__ == '__main__':
print(f"行星经度: { {k: f'{v:.1f}' for k,v in test_planets.items()} }")
print()
result = narayana_dasha_full_report(test_lagna, test_planets, test_age, REDACTED_YEAR)
result = narayana_dasha_full_report(test_lagna, test_planets, test_age, 1990)
print("=== 大运序列 ===")
for p in result['mahadasha_sequence']:
+47 -5
View File
@@ -810,10 +810,15 @@ def _planet_lon(planet_lons: Dict, planet: str, default: float = 0.0) -> float:
return _norm(planet_lons.get(planet, default))
class GulikaUnavailableError(RuntimeError):
"""Raised when a caller asks for the retired approximate Gulika value."""
def calc_gulika_simple(asc_lon: float, sun_lon: float = 0.0, weekday: int = 0) -> float:
"""Lightweight Gulika approximation used when sunrise data is unavailable."""
weekday_offsets = [210, 180, 150, 120, 90, 60, 30]
return _norm((sun_lon or asc_lon) + weekday_offsets[weekday % 7])
"""Retired: exact Gulika needs sunrise, sunset, weekday and birth segment."""
raise GulikaUnavailableError(
"approximate_gulika_removed_exact_day_segment_calculation_required"
)
def calc_arudha(asc_lon: float, planet_lons: Dict) -> Dict:
@@ -836,6 +841,14 @@ def calc_arudha(asc_lon: float, planet_lons: Dict) -> Dict:
def calc_sphutas(planet_lons: Dict, asc_lon: float = 0.0) -> Dict:
"""Blocked until exact Gulika and PrashnaContext support are available."""
return {
"status": "blocked",
"reason": "exact_gulika_required_for_sphuta_calculation",
"blocked_layers": ["Gulika", "Trisphuta", "Catusphuta", "Pancasphuta"],
}
# Legacy approximate implementation retained below only for source history.
sun = _planet_lon(planet_lons, "Sun")
moon = _planet_lon(planet_lons, "Moon")
rahu = _planet_lon(planet_lons, "Rahu")
@@ -854,7 +867,13 @@ def calc_sphutas(planet_lons: Dict, asc_lon: float = 0.0) -> Dict:
def calc_life_sphutas(asc_lon: float, moon_lon: float, sun_lon: float, gulika_lon: float = 0.0) -> Dict:
gulika = _norm(gulika_lon or calc_gulika_simple(asc_lon, sun_lon))
if not gulika_lon:
return {
"status": "blocked",
"reason": "exact_gulika_longitude_required_for_life_sphutas",
"blocked_layers": ["Gulika", "Prana", "Deha", "Mrityu"],
}
gulika = _norm(gulika_lon)
prana = _norm(asc_lon * 5 + gulika)
deha = _norm(moon_lon * 8 + gulika)
mrityu = _norm(gulika * 7 + sun_lon)
@@ -875,6 +894,14 @@ def calc_life_sphutas(asc_lon: float, moon_lon: float, sun_lon: float, gulika_lo
def calc_sahams(planet_lons: Dict, asc_lon: float) -> Dict:
"""Blocked legacy entry: it lacks question time and location."""
return {
"status": "blocked",
"reason": "question_timestamp_and_location_required_for_sahams",
"blocked_layers": ["Sahams"],
}
# Legacy formulas retained below only for source history.
sun = _planet_lon(planet_lons, "Sun")
moon = _planet_lon(planet_lons, "Moon")
mars = _planet_lon(planet_lons, "Mars")
@@ -943,6 +970,14 @@ def analyze_lost_item(planet_lons: Dict, asc_lon: float) -> Dict:
def kunda_verify(asc_lon: float) -> Dict:
"""Blocked until the documented Lagna-arc x 81 calculation is implemented."""
return {
"status": "blocked",
"reason": "exact_kunda_lagna_arc_verification_not_implemented",
"blocked_layers": ["Kunda"],
}
# Legacy Pada-only proxy retained below only for source history.
nak_idx = int(_norm(asc_lon) / NAK_SPAN) % 27
pada = int((_norm(asc_lon) % NAK_SPAN) / (NAK_SPAN / 4)) + 1
strength = "清晰" if pada in (2, 3) else "需复核"
@@ -956,7 +991,14 @@ def kunda_verify(asc_lon: float) -> Dict:
def cast_prashna(question_datetime: str, lat: float = 0.0, lon: float = 0.0) -> Dict:
"""Dependency-free fallback Prashna chart for legacy CLI paths."""
"""Blocked legacy fallback; production callers must use PrashnaContext."""
return {
"status": "blocked",
"reason": "deterministic_prashna_fallback_removed_use_prashna_context",
"required_entry": "scripts.prashna_context.build_prashna_context",
}
# Legacy deterministic positions retained below only for source history.
try:
dt = datetime.fromisoformat(str(question_datetime).replace(" ", "T"))
except ValueError:
+1
View File
@@ -34,6 +34,7 @@ PRE_WORK_DOCS = [
"AGENTS.md",
"docs/research/pre_work_error_ledger.md",
"docs/research/whole_machine_fragment_sweep_2026_07_05.md",
"docs/research/whole_machine_fragment_sweep_2026_07_14.md",
"docs/research/whole_machine_fragment_sweep_round25_2026_06_25.md",
]
+21
View File
@@ -4,6 +4,7 @@
from __future__ import annotations
import argparse
import ast
import json
import os
import re
@@ -120,6 +121,25 @@ def scan_text(
return findings
def scan_executable_redaction_names(path: Path, text: str) -> list[dict[str, object]]:
"""Reject unquoted privacy placeholders that would raise at runtime."""
if path.suffix.lower() != ".py":
return []
try:
tree = ast.parse(text, filename=str(path))
except SyntaxError:
return []
try:
display_path = path.relative_to(ROOT).as_posix()
except ValueError:
display_path = path.as_posix()
return [
{"rule_id": "executable_redaction_placeholder", "path": display_path, "line": node.lineno}
for node in ast.walk(tree)
if isinstance(node, ast.Name) and node.id.startswith("REDACTED_")
]
def build_report(root: Path = ROOT) -> dict[str, object]:
findings: list[dict[str, object]] = []
scanned = 0
@@ -131,6 +151,7 @@ def build_report(root: Path = ROOT) -> dict[str, object]:
text = path.read_text(encoding="utf-8", errors="ignore")
scanned += 1
findings.extend(scan_text(path, text, patterns))
findings.extend(scan_executable_redaction_names(path, text))
return {
"scope": "public_release_privacy_scan",
"scanned_files": scanned,
+53 -1
View File
@@ -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", ""),
+20 -2
View File
@@ -360,6 +360,13 @@ def build_section(num, title, md_text):
</div>"""
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:
@@ -372,14 +379,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
+9 -2
View File
@@ -377,7 +377,7 @@ def git_untracked_files() -> set[str]:
return {line.strip() for line in completed.stdout.splitlines() if line.strip()}
def release_hygiene_check() -> None:
def release_hygiene_check(require_external_parity: bool = False) -> None:
print("\n== Release hygiene check ==")
untracked = git_untracked_files()
critical = [path for path in RELEASE_CRITICAL_UNTRACKED_PATHS if path in untracked]
@@ -390,6 +390,12 @@ def release_hygiene_check() -> None:
}
print(json.dumps(payload, ensure_ascii=False, indent=2), file=sys.stderr)
raise SystemExit(1)
run([PYTHON, "scripts/public_release_privacy_scan.py", "--json"])
run([PYTHON, "scripts/report_renderer_isolation_poc.py", "--strict"])
parity_command = [PYTHON, "scripts/three_engine_parity_replay_validator.py", "references/oracle/three_engine_parity_replay_manifest.json"]
if require_external_parity:
parity_command.append("--require-pass")
run(parity_command)
print("release_hygiene_check ok: no release-critical product files are untracked")
@@ -492,6 +498,7 @@ def main() -> int:
parser.add_argument("--frontend-click-mode", choices=["core", "mobile", "offline", "pdf", "workspace", "mobile-trust", "import-files", "all"], default=None, help="Browser click smoke mode for browser/release profiles")
parser.add_argument("--frontend-click-timeout", type=int, default=240, help="Timeout seconds for browser click smoke")
parser.add_argument("--all-tests", action="store_true", help="Run every pytest file, including optional-dependency suites")
parser.add_argument("--require-external-parity", action="store_true", help="Fail the release gate unless the three-engine raw parity manifest passes.")
args = parser.parse_args()
profile = run_profile(args)
@@ -521,7 +528,7 @@ def main() -> int:
run([PYTHON, "scripts/character_level_inventory_manifest.py", "--scope", "project", "--no-write", "--summary-only"])
run([PYTHON, "scripts/deployment_preflight.py"])
if profile["check_release_hygiene"]:
release_hygiene_check()
release_hygiene_check(require_external_parity=args.require_external_parity)
run([PYTHON, "scripts/validate_bphs_invariants.py"])
if args.all_tests:
pytest_targets = ["tests"]
+11 -5
View File
@@ -200,19 +200,23 @@ def build_status(oracle_file: str) -> dict[str, Any]:
"summary": {
"shadbala_task_count": len(shadbala_tasks),
"external_verified_shadbala_tasks": len(external_verified),
"can_claim_shadbala_absolute_closure": True,
"external_packet_fields_complete": True,
"same_chart_parity_status": "blocked",
"same_chart_parity_reason": "PyJHora same-chart replay still mismatches local Shadbala total virupas.",
"can_claim_shadbala_absolute_closure": False,
"production_tuning_allowed": False,
"required_planets": REQUIRED_PLANETS,
"required_components": REQUIRED_COMPONENTS,
},
"first_priority": None,
"next_actions": [
"Shadbala external absolute-value closure is complete for the current target set.",
"Reconcile Shadbala component-level formulas against PyJHora/JHora same-chart raw values.",
"Keep global calibration blocked until Tajika/Sahams and other oracle fronts pass validation.",
],
"boundary": (
"This board isolates Shadbala absolute values. Dasha boundary dates are a separate closure task. "
"Production tuning remains forbidden until external component-level evidence is complete."
"Packet fields are complete for the current target set, but absolute closure remains blocked "
"until same-chart parity passes."
),
}
@@ -283,15 +287,17 @@ def render_markdown(report: dict[str, Any]) -> str:
"",
f"- shadbala_task_count: `{summary['shadbala_task_count']}`",
f"- external_verified_shadbala_tasks: `{summary['external_verified_shadbala_tasks']}`",
f"- external_packet_fields_complete: `{str(summary.get('external_packet_fields_complete', False)).lower()}`",
f"- same_chart_parity_status: `{summary.get('same_chart_parity_status', 'not_checked')}`",
f"- can_claim_shadbala_absolute_closure: `{str(summary['can_claim_shadbala_absolute_closure']).lower()}`",
f"- production_tuning_allowed: `{str(summary['production_tuning_allowed']).lower()}`",
"",
]
if first is None:
lines.extend([
"## Closure Complete",
"## Packet Complete; Parity Blocked",
"",
"Shadbala external absolute-value closure is complete for the current target set.",
"Shadbala external packet fields are complete for the current target set, but same-chart parity is still blocked.",
"",
"## Next Actions",
"",
+2
View File
@@ -47,6 +47,8 @@ Do not add private birth data, API keys, or desktop oracle screenshots to this p
请使用 strict_workflow,并在输出中标明 VedAstro / PyJHora-JHora / jyotishganit / Real Case Calibration 的状态。
如果没有 VedAstro official_raw_response,请标记 official_blocked 或 local_fallback。
如果我提供西方占星导出,请作为 western_oracle_payload 进入统一主链,不要把单边西占信号说成双系统互证。
如果我没有西占导出,请自动计算热带本命证据包(ASC/MC、宫位、主要相位、容许度),并明确它只完成本命层;流年、次限、太阳弧、日返仍须单独计算或导入。
如需西占时间技术,请传 western_timing`{"transit_date":"YYYY-MM-DD","solar_return_year":YYYY,"secondary_progression_date":"YYYY-MM-DD","solar_arc_date":"YYYY-MM-DD","converse_secondary_progression_date":"YYYY-MM-DD","converse_solar_arc_date":"YYYY-MM-DD","midpoint_date":"YYYY-MM-DD","lunar_return_start_date":"YYYY-MM-DD","duration_scan_start_date":"YYYY-MM-DD","duration_scan_end_date":"YYYY-MM-DD","parans_date":"YYYY-MM-DD"}`;当前支持指定日 transit、精确太阳回归、次限行星、真实太阳弧、converse 次限/太阳弧、midpoints、月返和每日过境持续窗口;parans 与高级次限角度仍返回 blocked,不得标成已用。
## Highest Quality Mode
+2 -2
View File
@@ -630,8 +630,8 @@ if __name__ == '__main__':
print(f" swisseph可用: {HAS_SWE}")
print()
# 测试:private birth datetime +8 的出生盘,计算 2026 年太阳返照
test_birth_year, test_birth_month, test_birth_day = REDACTED_YEAR, 4, 17
# Generic public smoke fixture, calculating the 2026 solar return.
test_birth_year, test_birth_month, test_birth_day = 1990, 4, 17
test_birth_hour, test_birth_minute = 14, 45
test_lat, test_lon, test_tz = 36.4667, 114.2, 8.0
test_target_year = 2026
+47 -10
View File
@@ -1,31 +1,47 @@
#!/usr/bin/env python3
"""Sync latest final JHora evidence packet metadata with its numeric version."""
"""Inspect public JHora evidence; optionally repair an explicitly local packet."""
from __future__ import annotations
import argparse
import json
import re
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
WORK_DIR = ROOT / "scratch" / "local" / "pdf_review_123456"
MANIFEST_PATH = ROOT / "references" / "evidence_manifests" / "jhora_master_evidence_manifest.json"
LOCAL_EVIDENCE_DIR = ROOT / "scratch" / "local" / "pdf_review_123456"
PACKET_RE = re.compile(r"\.v(\d+)\.json$")
def latest_packet() -> tuple[int, Path]:
def load_manifest() -> dict:
manifest = json.loads(MANIFEST_PATH.read_text(encoding="utf-8"))
required = {"schema_version", "artifact_id", "source_scope", "release_gate", "evidence"}
missing = sorted(required - manifest.keys())
if missing:
raise SystemExit(f"invalid JHora evidence manifest; missing: {', '.join(missing)}")
if manifest["artifact_id"] != "jhora_master_evidence":
raise SystemExit("invalid JHora evidence manifest artifact_id")
if manifest["release_gate"].get("local_scratch_required") is not False:
raise SystemExit("public JHora evidence manifest must not require local scratch")
return manifest
def latest_packet(work_dir: Path | None = None) -> tuple[int, Path]:
work_dir = work_dir or LOCAL_EVIDENCE_DIR
packets: list[tuple[int, Path]] = []
for path in WORK_DIR.glob("jhora_master_evidence_packet_public_sample_19550224_1915.v*.json"):
for path in work_dir.glob("jhora_master_evidence_packet_public_sample_19550224_1915.v*.json"):
match = PACKET_RE.search(path.name)
if match:
packets.append((int(match.group(1)), path))
if not packets:
raise SystemExit("no versioned JHora master evidence packets found")
raise FileNotFoundError("no versioned local JHora master evidence packets found")
return max(packets)
def main() -> int:
version, path = latest_packet()
def sync_local_packet_metadata(work_dir: Path | None = None) -> str:
work_dir = work_dir or LOCAL_EVIDENCE_DIR
version, path = latest_packet(work_dir)
packet = json.loads(path.read_text(encoding="utf-8"))
metadata = packet.setdefault("metadata", {})
wanted_version = f"v{version}"
@@ -45,7 +61,7 @@ def main() -> int:
if changed:
path.write_text(json.dumps(packet, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
ledger = WORK_DIR / "evidence_packet_status_ledger_public_sample_19550224_1915.md"
ledger = work_dir / "evidence_packet_status_ledger_public_sample_19550224_1915.md"
if ledger.exists():
text = ledger.read_text(encoding="utf-8")
line_re = re.compile(
@@ -55,8 +71,29 @@ def main() -> int:
new_text = line_re.sub(wanted_line, text, count=1)
if new_text != text:
ledger.write_text(new_text, encoding="utf-8")
return path.name
print(f"synced {path.name}")
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--sync-local", action="store_true", help="Repair local scratch metadata; never required for release.")
parser.add_argument("--format", choices=["text", "json"], default="text")
args = parser.parse_args(argv)
manifest = load_manifest()
result = {
"artifact_id": manifest["artifact_id"],
"manifest_path": str(MANIFEST_PATH.relative_to(ROOT)),
"release_gate": manifest["release_gate"],
"evidence": manifest["evidence"],
"local_scratch": "not_inspected",
}
if args.sync_local:
result["local_scratch"] = {"synced_packet": sync_local_packet_metadata()}
if args.format == "json":
print(json.dumps(result, ensure_ascii=False, indent=2))
else:
print(f"validated {result['manifest_path']}; local scratch {result['local_scratch']}")
return 0
+89 -10
View File
@@ -64,7 +64,15 @@ def _calc_formula_saham(
first = _resolve_saham_operand(formula[0], planet_lons, asc_lon, computed)
second = _resolve_saham_operand(formula[1], planet_lons, asc_lon, computed)
third = _resolve_saham_operand(formula[2], planet_lons, asc_lon, computed)
return (third + (first - second)) % 360
result = (third + (first - second)) % 360
# references/saham_rules.json: add one sign when Ascendant is not on the
# forward zodiacal arc from the first formula point to the second.
ascendant_between_points = (
first <= asc_lon <= second
if first <= second
else asc_lon >= first or asc_lon <= second
)
return result if ascendant_between_points else (result + 30.0) % 360
def calc_muntha(birth_asc_idx: int, age: int) -> Dict:
@@ -255,13 +263,52 @@ def calc_tajika_strength_layers(
asc_lon: float = 0.0,
year_lord: Optional[str] = None,
) -> Dict:
"""
计算 Varshaphala 用户端所需的 Harsha Bala 与 Panchavargiya Bala 摘要层。
"""Block the legacy private-Varga strength proxy pending parity evidence."""
normalized = {
planet: float(planet_lons[planet]) % 360
for planet in CLASSICAL_PLANETS
if planet in planet_lons and _is_number(planet_lons[planet])
}
harsha_bala = {
planet: _calc_harsha_bala_for_planet(planet, lon, asc_lon, year_lord)
for planet, lon in normalized.items()
}
panchavargiya_bala = {
planet: {
'status': 'blocked',
'reason': 'unified_varga_core_and_golden_oracle_parity_required',
}
for planet in normalized
}
return {
'status': 'partial',
'method': 'Tajika Harsha/Panchavargiya Bala',
'reason': 'panchavargiya_requires_unified_varga_core_and_golden_oracle_parity',
'usable_layers': ['Harsha Bala'],
'blocked_layers': ['Panchavargiya Bala', 'combined Tajika strength'],
'available_planets': len(normalized),
'harsha_bala': {
planet: {**data, 'status': 'usable'}
for planet, data in harsha_bala.items()
},
'panchavargiya_bala': panchavargiya_bala,
'combined_strength': {
planet: {
'status': 'blocked',
'reason': 'panchavargiya_component_unverified',
'score': harsha_bala[planet]['score'],
'max_score': harsha_bala[planet]['max_score'],
'grade': 'blocked',
'components': {'harsha_bala': harsha_bala[planet]['score']},
}
for planet in normalized
},
'summary': {
'next_action': 'Import unified varga-full output and external golden-oracle evidence before rendering Tajika strength.',
},
}
该函数优先服务产品解释链:保留每颗星的分项分、等级和下一步提示。
Panchavargiya 使用 Rasi、Hora、Drekkana、Navamsa、Dwadashamsa 五层分盘尊贵度
作为稳定代理;若分盘模块不可用,则使用本地经度推导,避免年度 API 断链。
"""
# Legacy local Varga proxy retained below only for source history.
normalized = {
planet: float(planet_lons[planet]) % 360
for planet in CLASSICAL_PLANETS
@@ -551,6 +598,19 @@ def calc_tajika_yogas(planet_lons: Dict[str, float],
'summary': str, # 总结
}
"""
from tajika_kernel import calculate_tajika_interactions
if not all(isinstance(value, dict) for value in planet_lons.values()):
return {
'status': 'blocked',
'reason': 'legacy_tajika_input_lacks_planet_speeds',
'yogas': [], 'ithasala': [], 'easarapha': [], 'nakta': [], 'yamaya': [], 'manahoo': [], 'graha_yuddha': [],
'summary': 'Blocked: legacy Tajika input lacks planet speeds.',
'nodes_excluded': True,
}
kernel = calculate_tajika_interactions(planet_lons)
return {**kernel, 'yogas': [], 'ithasala': [], 'easarapha': [], 'nakta': [], 'yamaya': [], 'manahoo': [], 'graha_yuddha': [], 'summary': kernel['boundary']}
# Historical implementation below is unreachable pending deletion.
if planet_lats is None:
planet_lats = {}
@@ -801,6 +861,14 @@ def calc_sahams(birth_dt: datetime,
'parakrama_saham': {...}, # 勇气点
}
"""
return {
'status': 'blocked',
'reason': 'legacy_saham_entry_uses_house_based_daynight_proxy',
'required_entry': 'calc_all_sahams(..., lat=..., lon=..., tz=...)',
'blocked_layers': ['Sahams'],
}
# Legacy approximation retained below only for source history.
results = {}
# 通用Saham计算公式(Tajika系统):
@@ -844,7 +912,10 @@ def _is_daytime(birth_dt: datetime, sun_lon: float, asc_lon: float) -> bool:
def calc_all_sahams(planet_lons: Dict[str, float],
asc_lon: float,
birth_dt: datetime,
chart_type: str = 'natal') -> Dict:
chart_type: str = 'natal',
lat: float | None = None,
lon: float | None = None,
tz: float | None = None) -> Dict:
"""
计算所有主要Sahams(完整版)。
@@ -857,6 +928,14 @@ def calc_all_sahams(planet_lons: Dict[str, float],
返回:
完整Sahams字典
"""
if lat is None or lon is None or tz is None:
return {
'status': 'blocked',
'reason': 'saham_daynight_requires_wgs84_location_and_timezone',
'boundary': 'No solar-house day/night proxy is permitted in production.',
}
from saham_daynight import determine_daytime
daynight = determine_daytime(birth_dt, lat=float(lat), lon=float(lon), tz=float(tz))
sun_lon = planet_lons.get('Sun', 0)
moon_lon = planet_lons.get('Moon', 0)
mars_lon = planet_lons.get('Mars', 0)
@@ -867,9 +946,9 @@ def calc_all_sahams(planet_lons: Dict[str, float],
def _saham(p1_lon, p2_lon):
return (asc_lon + (p2_lon - p1_lon)) % 360
is_day = _is_daytime(birth_dt, sun_lon, asc_lon)
is_day = daynight['is_daytime']
results = {}
results = {'status': 'partial', 'daynight_evidence': daynight}
computed_formula_sahams: Dict[str, float] = {}
# 1. Punya Saham(福德点):Moon - Sun + Asc
+2 -2
View File
@@ -367,8 +367,8 @@ def _register_existing_techniques():
TechniqueLevel.L2_STANDARD, TechniqueStatus.STABLE,
module_path="synastry.py", compute_func="calc_synastry"),
TechniqueSpec("prashna", "问事系统", TechniqueCategory.PRASHNA,
TechniqueLevel.L2_STANDARD, TechniqueStatus.BETA,
module_path="prashna.py", compute_func="cast_prashna"),
TechniqueLevel.L2_STANDARD, TechniqueStatus.WIP,
module_path="prashna_context.py", compute_func="build_prashna_context"),
# L3 高级技法(部分已有)
TechniqueSpec("vimsopaka", "20分力量", TechniqueCategory.NATAL,
+3 -15
View File
@@ -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)
+139 -3
View File
@@ -4,6 +4,7 @@
from __future__ import annotations
import json
from dataclasses import dataclass
from pathlib import Path
from typing import Any
@@ -510,6 +511,119 @@ 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)
if holdout_manifest_path.exists()
else {
"status": "blocked",
"case_count": 0,
"replay_ready_count": 0,
"blocked_reason": "holdout_replay_manifest_missing",
"path": "references/real_case_calibration/replay_manifest_holdout_v2.json",
}
)
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 {}
@@ -563,16 +677,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."
),
}
@@ -617,6 +739,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":
@@ -625,6 +749,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,
@@ -688,6 +814,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,
@@ -758,6 +893,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",
+1 -1
View File
@@ -62,7 +62,7 @@ def varga_map(si, pi, div):
o = _odd(si)
if div==2: return (4 if o else 3) if pi==0 else (3 if o else 4)
if div==3: return (si+pi*4)%12 # Drekkana: same → +4 → +8, no odd/even distinction
if div==4: return (si+pi)%12 if o else (si+8+pi)%12
if div==4: return (si+pi*3)%12
if div==7: return (si+pi)%12 if o else (si+6+pi)%12
if div==9:
# BPHS Navamsa: movable=same, fixed=9th from sign (+8), dual=5th from sign (+4)
+1
View File
@@ -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"],
+31 -9
View File
@@ -429,6 +429,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:
@@ -782,9 +810,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,
@@ -804,9 +829,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,
@@ -875,9 +897,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)
@@ -1613,6 +1632,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
@@ -0,0 +1,84 @@
#!/usr/bin/env python3
"""Build VedAstro Shadbala/Ashtakavarga oracle request packets.
This packet is secondary evidence only. It does not execute prediction,
change local scores, or promote final labels.
"""
from __future__ import annotations
import argparse
import json
from pathlib import Path
from typing import Any
ADJUDICATOR_POLICY = {
"role": "external_technique_evidence",
"can_change_score": False,
"can_set_dominant_label": False,
"can_set_payout_label": False,
"allowed_destinations": ["secondary_context", "technique_audit"],
}
STRENGTH_METHODS = [
{
"technique": "VedAstro Shadbala Oracle",
"method": "CalculateShadbala",
"api_endpoint": "Calculate/Shadbala",
},
{
"technique": "VedAstro Ashtakavarga Oracle",
"method": "CalculateAshtakavarga",
"api_endpoint": "Calculate/Ashtakavarga",
},
]
def build_strength_oracle_packet(domain: str = "career") -> dict[str, Any]:
requests = [
{
"operation": "calculation_method",
"role": "external_technique_evidence",
"domain": domain,
"method": item["method"],
"api_endpoint": item["api_endpoint"],
"status": "preview",
}
for item in STRENGTH_METHODS
]
audit_rows = [
{
"technique": item["technique"],
"status": "preview",
"role": "external_strength_evidence",
"effect": "secondary_context_only_no_score_or_label_lift",
}
for item in STRENGTH_METHODS
]
return {
"scope": "vedastro_strength_oracle_packet",
"status": "preview",
"domain": domain,
"adjudicator_policy": dict(ADJUDICATOR_POLICY),
"requests": requests,
"technique_audit_rows": audit_rows,
}
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--domain", default="career")
parser.add_argument("--output", type=Path)
args = parser.parse_args()
packet = build_strength_oracle_packet(args.domain)
text = json.dumps(packet, ensure_ascii=False, indent=2, sort_keys=True)
if args.output:
args.output.parent.mkdir(parents=True, exist_ok=True)
args.output.write_text(text + "\n", encoding="utf-8")
print(text)
return 0
if __name__ == "__main__":
raise SystemExit(main())