Merge remote-tracking branch 'origin/main' into codex/cross-project-contract
This commit is contained in:
+7
-2
@@ -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"])
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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
@@ -15,7 +15,7 @@ Muntha 是 Tajika 年运盘(Varshaphala)中的核心指标,
|
||||
注意:不同流派对 Muntha 计算公式有微小差异。
|
||||
本实现采用最广泛接受的方法。
|
||||
"""
|
||||
from typing import Dict, Optional
|
||||
from typing import Dict, List, Optional
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
|
||||
|
||||
@@ -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
@@ -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:
|
||||
|
||||
@@ -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",
|
||||
]
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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", ""),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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",
|
||||
"",
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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"],
|
||||
|
||||
@@ -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())
|
||||
Reference in New Issue
Block a user