feat: complete minute birth-time rectification flow

This commit is contained in:
Jesse_Chen
2026-07-25 01:15:48 +08:00
parent f90aba217f
commit 3ca30ed7de
93 changed files with 8347 additions and 1646 deletions
+32 -3
View File
@@ -88,6 +88,7 @@ class CandidateResult(TypedDict):
calculation_contract: dict[str, Any]
stability_diagnostics: dict[str, Any]
missing_layers: list[str]
candidate_ranking_summary: NotRequired[list[dict[str, Any]]]
def precision_weight(precision: EventPrecision) -> float:
@@ -270,12 +271,39 @@ def adjudicate_candidate_rows(
reasons.append("neighbor_stability_not_passed")
if (leave_one_event_out or {}).get("status") != "pass":
reasons.append("leave_one_event_out_not_passed")
# Minute confirmation remains release-gated until the frozen public AA holdout passes.
reasons.append("minute_holdout_not_ready")
can_apply = (
confidence == "high"
and segment is not None
and neighbor_stability["all_required_passed"]
and (leave_one_event_out or {}).get("status") == "pass"
and not missing_layers
and not any(reason in reasons for reason in (
"tied_leader",
"insufficient_events",
"insufficient_domains",
"winning_interval_too_wide",
"lead_margin_below_medium_threshold",
))
)
ranking_summary: list[dict[str, Any]] = []
for score in ranked_scores[:3]:
score_rows = sorted(
(row for row in rows if row["score"] == score),
key=lambda row: _minute_value(row["time"]),
)
if not score_rows:
continue
representative = score_rows[(len(score_rows) - 1) // 2]
ranking_summary.append({
"rank": len(ranking_summary) + 1,
"time": representative["time"],
"score": score,
"tied_minute_count": len(score_rows),
})
return {
"result_id": str(uuid5(NAMESPACE_URL, f"{ALGORITHM_VERSION}:{request_fingerprint}")),
"confidence": confidence,
"can_apply": False,
"can_apply": can_apply,
"winning_segment": segment,
"event_count": event_count,
"domain_count": domain_count,
@@ -292,6 +320,7 @@ def adjudicate_candidate_rows(
"leave_one_event_out": leave_one_event_out or {"status": "not_evaluated", "runs": []},
},
"missing_layers": missing_layers,
"candidate_ranking_summary": ranking_summary,
}
+19 -2
View File
@@ -4,7 +4,10 @@
from __future__ import annotations
import argparse
import contextlib
import importlib
import importlib.util
import io
import json
import os
from pathlib import Path
@@ -19,11 +22,22 @@ def build_report() -> dict:
module_name = os.environ.get("PYJHORA_MODULE_NAME", "jhora").strip() or "jhora"
adapter_exists = adapter.exists()
module_available = importlib.util.find_spec(module_name) is not None
import_error = None
if module_available:
try:
with contextlib.redirect_stdout(io.StringIO()), contextlib.redirect_stderr(io.StringIO()):
importlib.import_module(f"{module_name}.utils")
importlib.import_module(f"{module_name}.horoscope.chart.charts")
importlib.import_module(f"{module_name}.panchanga.drik")
except Exception as exc:
import_error = f"{exc.__class__.__name__}: {exc}"
if not adapter_exists:
status = "missing_adapter"
elif not module_available:
status = "missing_dependency"
elif import_error:
status = "dependency_import_failed"
else:
status = "available"
@@ -35,14 +49,17 @@ def build_report() -> dict:
"dependency_module": module_name,
"dependency_available": module_available,
"missing_dependency": None if module_available else module_name,
"dependency_import_error": import_error,
"install_hint": {
"package": "PyJHora",
"commands": ["pip install PyJHora"],
"commands": [
"pip install -r requirements.txt -r requirements-reference-engines.txt"
],
"note": "Install in an isolated optional benchmark environment, not as a hard runtime dependency.",
},
"license_boundary": "AGPL external benchmark only; do not vendor or make it a runtime dependency.",
"ephemeris_data_note": "Recent PyJHora releases may require separate Swiss Ephemeris data download/configuration before full chart comparison can run.",
"boundary": "This is an adapter readiness smoke check only; it does not run PyJHora chart comparison.",
"boundary": "This verifies required PyJHora modules import successfully, but does not run chart comparison.",
}
+67 -6
View File
@@ -7,7 +7,7 @@ import hashlib
import json
import math
import threading
from datetime import datetime
from datetime import datetime, timezone
from typing import Any
from zoneinfo import ZoneInfo
@@ -53,18 +53,66 @@ def _lookup_timezone_name(lat: float, lon: float) -> str | None:
def infer_timezone_offset(*, lat: float, lon: float, local_datetime: datetime) -> float:
timezone_context = resolve_timezone_context(
lat=lat,
lon=lon,
local_datetime=local_datetime,
)
offset = timezone_context["timezone_offset"]
if offset is None:
raise TimezoneInferenceError(
f"local time is {timezone_context['local_time_status']} in IANA zone"
)
return float(offset)
def resolve_timezone_context(
*, lat: float, lon: float, local_datetime: datetime | None = None
) -> dict[str, Any]:
"""Resolve an IANA zone and, when safe, its historical local UTC offset.
A missing local time still permits timezone identification. DST folds and
gaps deliberately return no offset instead of silently choosing one.
"""
if not (-90 <= lat <= 90 and -180 <= lon <= 180):
raise TimezoneInferenceError("timezone inference received invalid coordinates")
tz_name = _lookup_timezone_name(lat, lon)
if not tz_name:
raise TimezoneInferenceError("timezone inference returned no IANA zone")
if local_datetime is None:
return {
"timezone_id": tz_name,
"timezone_offset": None,
"local_time_status": "not_provided",
}
try:
offset = local_datetime.replace(tzinfo=ZoneInfo(tz_name)).utcoffset()
zone = ZoneInfo(tz_name)
valid_offsets: set[float] = set()
for fold in (0, 1):
aware = local_datetime.replace(tzinfo=zone, fold=fold)
round_trip = aware.astimezone(timezone.utc).astimezone(zone).replace(tzinfo=None)
offset = aware.utcoffset()
if round_trip == local_datetime and offset is not None:
valid_offsets.add(offset.total_seconds() / 3600.0)
except Exception as exc:
raise TimezoneInferenceError("timezone inference failed for IANA zone") from exc
if offset is None:
raise TimezoneInferenceError("timezone inference returned no UTC offset")
return offset.total_seconds() / 3600.0
if not valid_offsets:
return {
"timezone_id": tz_name,
"timezone_offset": None,
"local_time_status": "nonexistent",
}
if len(valid_offsets) > 1:
return {
"timezone_id": tz_name,
"timezone_offset": None,
"local_time_status": "ambiguous",
}
return {
"timezone_id": tz_name,
"timezone_offset": valid_offsets.pop(),
"local_time_status": "resolved",
}
def _normalized_request(payload: dict[str, Any]) -> dict[str, Any]:
@@ -83,9 +131,16 @@ def _normalized_request(payload: dict[str, Any]) -> dict[str, Any]:
lat = float(payload["lat"])
lon = float(payload["lon"])
tz_requested = payload.get("tz")
timezone_id = payload.get("timezone_id", payload.get("timezoneId"))
timezone_source = "explicit_offset"
if tz_requested in {None, ""}:
tz = infer_timezone_offset(lat=lat, lon=lon, local_datetime=local_dt)
timezone_context = resolve_timezone_context(lat=lat, lon=lon, local_datetime=local_dt)
timezone_id = timezone_context["timezone_id"]
tz = timezone_context["timezone_offset"]
if tz is None:
raise TimezoneInferenceError(
f"local time is {timezone_context['local_time_status']} in IANA zone"
)
timezone_source = "iana_inferred"
else:
tz = float(tz_requested)
@@ -101,6 +156,7 @@ def _normalized_request(payload: dict[str, Any]) -> dict[str, Any]:
"lat": lat,
"lon": lon,
"tz": tz,
"timezone_id": str(timezone_id).strip() if timezone_id else None,
"timezone_source": timezone_source,
"ayanamsa": ayanamsa,
"node_mode": requested_node,
@@ -151,11 +207,16 @@ def compute_chart(payload: dict[str, Any]) -> dict[str, Any]:
"ephemeris_source": "swisseph_calc_ut",
"ephemeris_flags_verified": False,
}
if request["timezone_id"]:
effective["timezone_id"] = request["timezone_id"]
requested = {
"ayanamsa": payload.get("ayanamsa", "lahiri"),
"node_mode": payload.get("node_mode", payload.get("nodeMode", "mean")),
"timezone_offset": payload.get("tz"),
}
requested_timezone_id = payload.get("timezone_id", payload.get("timezoneId"))
if requested_timezone_id:
requested["timezone_id"] = requested_timezone_id
contract = _contract(requested, effective, algorithm="sidereal_natal_chart")
hash_payload = {
"contract": contract,
+451 -21
View File
@@ -90,6 +90,246 @@ _RATE_LIMIT_LOCK = threading.Lock()
_RATE_LIMIT_BUCKETS: dict[str, tuple[float, int]] = {}
def resolve_location_timezone_payload(body):
"""Resolve a global coordinate to IANA timezone and safe historical offset."""
calculation_service = _load_local_module('domain_calculation_service')
lat = float(body['latitude'])
lon = float(body['longitude'])
birth_date = str(body.get('birthDate') or '').strip()
birth_time = str(body.get('birthTime') or '').strip()
local_datetime = None
if birth_time and not birth_date:
raise ValueError('birthDate is required when birthTime is supplied')
if birth_date:
if not re.fullmatch(r'\d{4}-\d{2}-\d{2}', birth_date):
raise ValueError('birthDate must be YYYY-MM-DD')
if birth_time:
if not re.fullmatch(r'(?:[01]\d|2[0-3]):[0-5]\d', birth_time):
raise ValueError('birthTime must be HH:MM')
local_datetime = datetime.fromisoformat(f'{birth_date}T{birth_time}:00')
else:
datetime.fromisoformat(birth_date)
context = calculation_service.resolve_timezone_context(
lat=lat,
lon=lon,
local_datetime=local_datetime,
)
return {
'available': True,
'timezoneId': context['timezone_id'],
'timezoneOffset': context['timezone_offset'],
'timezoneSource': 'iana_historical',
'localTimeStatus': context['local_time_status'],
}
_VEDASTRO_RECTIFICATION_DOMAIN_MAP = {
'career': ('career', 'direct'),
'finance': ('wealth', 'direct'),
'relationship': ('marriage', 'direct'),
'education': ('career', 'proxy'),
'relocation': ('career', 'proxy'),
}
def _rectification_event_date_range(event):
value = str(event.get('date') or '')
precision = str(event.get('precision') or '')
if precision == 'day':
return value, value
if precision == 'month':
start = datetime.strptime(value, '%Y-%m')
next_month = (
datetime(start.year + 1, 1, 1)
if start.month == 12
else datetime(start.year, start.month + 1, 1)
)
return start.strftime('%Y-%m-%d'), (next_month - timedelta(days=1)).strftime('%Y-%m-%d')
if precision == 'year':
year = int(value)
return f'{year:04d}-01-01', f'{year:04d}-12-31'
raise ValueError(f'Unsupported event precision: {precision}')
def _rectification_event_representative_date(start_date, end_date):
start = datetime.strptime(start_date, '%Y-%m-%d')
end = datetime.strptime(end_date, '%Y-%m-%d')
return (start + (end - start) / 2).strftime('%Y-%m-%d')
def _select_vedastro_rectification_events(events):
precision_rank = {'year': 1, 'month': 2, 'day': 3}
selected_by_domain = {}
eligible_event_count = 0
unsupported_events = []
for event in events:
mapping = _VEDASTRO_RECTIFICATION_DOMAIN_MAP.get(event['domain'])
if mapping is None:
unsupported_events.append({
'event_id': event['id'],
'ui_domain': event['domain'],
'status': 'unsupported_range_scan_domain',
'reason': 'VedAstro SearchEvents does not expose a supported health-pressure range-scan domain.',
})
continue
eligible_event_count += 1
adapter_domain, mapping_mode = mapping
start_date, end_date = _rectification_event_date_range(event)
rank = (
1 if mapping_mode == 'direct' else 0,
precision_rank.get(str(event.get('precision') or ''), 0),
end_date,
)
current = selected_by_domain.get(adapter_domain)
if current is None or rank > current[0]:
representative_date = _rectification_event_representative_date(start_date, end_date)
selected_by_domain[adapter_domain] = (
rank,
event,
adapter_domain,
mapping_mode,
representative_date,
representative_date,
)
selected_events = [
selected_by_domain[domain][1:]
for domain in sorted(selected_by_domain)
]
return selected_events, eligible_event_count, unsupported_events
def _safe_vedastro_range_scan_summary(event, adapter_domain, mapping_mode, start_date, end_date, report):
ledger = report.get('evidence_ledger') if isinstance(report.get('evidence_ledger'), list) else []
signal_lift = round(sum(
float(item.get('signal_lift') or 0)
for item in ledger
if isinstance(item, dict) and isinstance(item.get('signal_lift'), (int, float))
), 6)
event_count = int(report.get('event_count') or 0)
return {
'event_id': str(event.get('id') or ''),
'ui_domain': str(event.get('domain') or ''),
'adapter_domain': adapter_domain,
'mapping_mode': mapping_mode,
'start_date': start_date,
'end_date': end_date,
'status': str(report.get('status') or 'unknown'),
'available': bool(report.get('available')),
'event_count': event_count,
'matched': event_count > 0,
'signal_lift': signal_lift,
'top_event_id': str((report.get('top_event') or {}).get('event_id') or ''),
}
def _vedastro_candidate_metric(event_scans):
successful = [item for item in event_scans if item.get('status') == 'ok' and item.get('available')]
return {
'requested_event_count': len(event_scans),
'successful_event_count': len(successful),
'matched_event_count': sum(1 for item in successful if item.get('matched')),
'event_hit_count': sum(int(item.get('event_count') or 0) for item in successful),
'signal_lift': round(sum(float(item.get('signal_lift') or 0) for item in successful), 6),
}
def _vedastro_metric_key(metric):
return (
int(metric.get('matched_event_count') or 0),
float(metric.get('signal_lift') or 0),
int(metric.get('event_hit_count') or 0),
)
_VEDASTRO_MINUTE_SENSITIVE_LAYERS = (
'ascendant_house_boundaries',
'D9',
'D10',
'dasha_boundaries',
)
def _safe_vedastro_minute_snapshot_summary(candidate_time, report):
layers = report.get('layers') if isinstance(report.get('layers'), dict) else {}
safe_layers = {}
for name in (*_VEDASTRO_MINUTE_SENSITIVE_LAYERS, 'kp_cusp_sub_lord'):
layer = layers.get(name) if isinstance(layers.get(name), dict) else {}
safe_layer = {
'status': str(layer.get('status') or 'missing'),
}
if name in _VEDASTRO_MINUTE_SENSITIVE_LAYERS:
safe_layer['fingerprint'] = layer.get('fingerprint')
if name == 'ascendant_house_boundaries':
safe_layer['ascendant'] = layer.get('ascendant')
safe_layer['house_count'] = len(layer.get('houses') or {})
elif name in {'D9', 'D10'}:
safe_layer['house_count'] = len(layer.get('houses') or {})
safe_layer['planet_count'] = len(layer.get('planets') or {})
elif name == 'dasha_boundaries':
safe_layer['boundary_count'] = int(layer.get('boundary_count') or 0)
elif name == 'kp_cusp_sub_lord':
safe_layer['reason'] = str(layer.get('reason') or '')
safe_layers[name] = safe_layer
return {
'candidate_time': candidate_time,
'status': str(report.get('status') or 'blocked'),
'available': bool(report.get('available')),
'source': str(report.get('source') or 'vedastro_official'),
'layers': safe_layers,
}
def _compare_vedastro_minute_snapshots(candidate_snapshots):
comparison_ready = len(candidate_snapshots) == 2 and all(
item.get('available') for item in candidate_snapshots
)
differences = {}
discriminated_layers = []
for layer_name in _VEDASTRO_MINUTE_SENSITIVE_LAYERS:
left = (candidate_snapshots[0].get('layers') or {}).get(layer_name, {}) if candidate_snapshots else {}
right = (candidate_snapshots[1].get('layers') or {}).get(layer_name, {}) if len(candidate_snapshots) > 1 else {}
both_available = left.get('status') == 'ok' and right.get('status') == 'ok'
differs = bool(
both_available
and left.get('fingerprint')
and right.get('fingerprint')
and left.get('fingerprint') != right.get('fingerprint')
)
differences[layer_name] = {
'status': 'different' if differs else 'same' if both_available else 'unavailable',
'discriminated': differs,
}
if differs:
discriminated_layers.append(layer_name)
return {
'comparison_ready': comparison_ready,
'discriminated': comparison_ready and bool(discriminated_layers),
'discriminated_layers': discriminated_layers,
'differences': differences,
}
def _rectification_candidate_ready_for_external_validation(result):
"""Allow external validation once local scoring has a narrow, auditable lead."""
segment = result.get('winning_segment')
ranking = result.get('candidate_ranking_summary')
if not isinstance(segment, dict) or not isinstance(ranking, list) or len(ranking) < 2:
return False
try:
width_minutes = int(segment.get('width_minutes') or 0)
top_score = float(result.get('top_score'))
second_score = float(result.get('second_score'))
except (TypeError, ValueError):
return False
return (
int(result.get('event_count') or 0) >= 3
and int(result.get('domain_count') or 0) >= 2
and 1 <= width_minutes <= 15
and bool(segment.get('representative_time'))
and top_score > second_score
and 'missing_mandatory_layers' not in (result.get('reasons') or [])
)
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 {}
@@ -1693,6 +1933,11 @@ class JyotishAPIHandler(BaseHTTPRequestHandler):
else:
lat, lon, tz = CITY_DB[matched]
self._json({'status': 'local_city_match', 'city': matched, 'lat': lat, 'lon': lon, 'tz': tz})
elif path == '/api/location/timezone':
try:
self._json(resolve_location_timezone_payload(body))
except (KeyError, TypeError, ValueError) as exc:
self._error_json(str(exc), 400, 'ERR_INVALID_LOCATION_TIMEZONE_REQUEST')
elif path == '/api/chart':
result = self._compute_chart(body)
self._json(result)
@@ -6938,8 +7183,8 @@ class JyotishAPIHandler(BaseHTTPRequestHandler):
lon = self._get_float(body, 'lon', 0, -180, 180)
tz = self._get_float(body, 'tz', 0, -14, 14)
events = body.get('events')
if not isinstance(events, list) or not 3 <= len(events) <= 8:
raise BadRequest('events must contain between 3 and 8 items')
if not isinstance(events, list) or len(events) < 3:
raise BadRequest('events must contain at least 3 items')
normalized_events = []
allowed_domains = {'education', 'relocation', 'relationship', 'career', 'finance', 'health_pressure'}
formats = {'year': '%Y', 'month': '%Y-%m', 'day': '%Y-%m-%d'}
@@ -6986,35 +7231,220 @@ class JyotishAPIHandler(BaseHTTPRequestHandler):
'tz': tz,
'events': normalized_events,
})
external_validation = {
'status': 'not_evaluated',
'reason': 'local_candidate_not_ready_for_external_validation',
'vedastro_status': 'not_evaluated',
'vedastro_reason': 'official_vedastro_runs_after_local_scoring_produces_a_narrow_candidate',
'blockers': [],
}
local_candidate_ready = _rectification_candidate_ready_for_external_validation(result)
if high_rigor and local_candidate_ready:
from scripts.rectification_three_engine_packet import build_packet
representative_time = result['winning_segment']['representative_time']
representative_hour, representative_minute = representative_time.split(':', 1)
engine_case = {
'year': parsed_birth_date.year,
'month': parsed_birth_date.month,
'day': parsed_birth_date.day,
'hour': int(representative_hour),
'minute': int(representative_minute),
'lat': lat,
'lon': lon,
'tz': tz,
}
three_engine_packet = build_packet(engine_case)
vedastro_events = [{
'date': event['date'],
'domain': event['domain'],
'summary': event.get('summary', ''),
} for event in normalized_events]
vedastro_result = self._compute_vedastro_gateway_run({
**engine_case,
'question': (
'Validate this rectified birth-minute candidate against the dated life events '
'and minute-sensitive chart boundaries. Events: '
+ json.dumps(vedastro_events, ensure_ascii=False, sort_keys=True)
),
'theme': sorted({
_VEDASTRO_RECTIFICATION_DOMAIN_MAP[event['domain']][0]
for event in normalized_events
if event['domain'] in _VEDASTRO_RECTIFICATION_DOMAIN_MAP
}),
'reference_date': datetime.now().strftime('%Y-%m-%d'),
})
vedastro_status = str(
vedastro_result.get('official_closure_state')
or vedastro_result.get('status')
or 'blocked'
)
parity_passed = (
all(status == 'ok' for status in three_engine_packet.get('engine_status', {}).values())
and three_engine_packet.get('mismatch_count') == 0
)
official_response_present = vedastro_status == 'official_verified'
ranking = result.get('candidate_ranking_summary') or []
candidate_times = [
str(item.get('time') or '')
for item in ranking[:2]
if isinstance(item, dict) and item.get('time')
]
supported_events, eligible_event_count, unsupported_events = (
_select_vedastro_rectification_events(normalized_events)
)
vedastro_adapter = _load_local_module('vedastro_service_adapter')
candidate_validations = []
minute_candidate_snapshots = []
range_scan_cache = {}
for candidate_time in candidate_times:
candidate_hour, candidate_minute = candidate_time.split(':', 1)
candidate_case = {
**engine_case,
'hour': int(candidate_hour),
'minute': int(candidate_minute),
}
minute_snapshot = vedastro_adapter.run_rectification_minute_snapshot_for_case(
candidate_case,
case_id=f'user_rectification_{candidate_time.replace(":", "")}',
)
minute_candidate_snapshots.append(
_safe_vedastro_minute_snapshot_summary(candidate_time, minute_snapshot)
)
event_scans = []
for event, adapter_domain, mapping_mode, event_start, event_end in supported_events:
cache_key = (candidate_time, adapter_domain, event_start, event_end)
if cache_key not in range_scan_cache:
range_scan_cache[cache_key] = vedastro_adapter.run_range_scan_for_case(
candidate_case,
adapter_domain,
event_start,
event_end,
case_id=f'user_rectification_{candidate_time.replace(":", "")}',
)
event_scans.append(_safe_vedastro_range_scan_summary(
event,
adapter_domain,
mapping_mode,
event_start,
event_end,
range_scan_cache[cache_key],
))
candidate_validations.append({
'candidate_time': candidate_time,
'metric': _vedastro_candidate_metric(event_scans),
'events': event_scans,
})
minute_comparison = _compare_vedastro_minute_snapshots(minute_candidate_snapshots)
minute_snapshots_verified = bool(
minute_comparison['comparison_ready']
and all(item.get('source') == 'vedastro_official' for item in minute_candidate_snapshots)
)
official_response_present = official_response_present or minute_snapshots_verified
if minute_snapshots_verified:
vedastro_status = 'official_verified'
background_comparison_ready = len(candidate_validations) == 2 and bool(supported_events)
scans_succeeded = background_comparison_ready and all(
item['metric']['successful_event_count'] == item['metric']['requested_event_count']
for item in candidate_validations
)
semantic_validation_passed = (
official_response_present
and minute_snapshots_verified
and minute_comparison['discriminated']
)
external_blockers = []
if not parity_passed:
external_blockers.append('three_engine_parity_not_passed')
if not official_response_present:
external_blockers.append('vedastro_official_response_missing')
if len(candidate_times) < 2:
external_blockers.append('vedastro_runner_up_candidate_missing')
if len(candidate_times) >= 2 and not minute_snapshots_verified:
external_blockers.append('vedastro_minute_snapshot_failed')
if minute_snapshots_verified and not minute_comparison['discriminated']:
external_blockers.append('vedastro_minute_sensitive_layers_not_discriminated')
background_status = (
'pass'
if scans_succeeded
else 'partial'
if candidate_validations
else 'not_evaluated'
)
external_validation = {
'status': 'pass' if parity_passed and semantic_validation_passed else 'fail',
'reason': (
'winner_must_pass_three_engine_parity_and_official_vedastro_minute_sensitive_candidate_identity_checks; '
'SearchEvents_is_background_only'
),
'vedastro_status': vedastro_status,
'vedastro_reason': str(
vedastro_result.get('official_closure_reason')
or 'official_minute_snapshot_required'
),
'blockers': external_blockers,
'candidate_time': representative_time,
'engine_status': three_engine_packet.get('engine_status', {}),
'match_count': three_engine_packet.get('match_count', 0),
'mismatch_count': three_engine_packet.get('mismatch_count', 0),
'minute_sensitive_validation': {
'status': 'pass' if semantic_validation_passed else 'fail',
'comparison_contract': (
'official_ascendant_house_boundaries_plus_D9_plus_D10_plus_dasha_boundary_identity; '
'local_event_scoring_keeps_candidate_order'
),
'candidate_order_source': 'local_dated_event_scoring',
'candidates': minute_candidate_snapshots,
**minute_comparison,
'kp_cusp_sub_lord': {
'status': 'unsupported_by_verified_official_interface',
'used_for_decision': False,
},
},
'event_background_validation': {
'status': background_status,
'used_for_decision': False,
'comparison_contract': 'SearchEvents provides background event context and never selects the final minute',
'eligible_event_count': eligible_event_count,
'supported_event_count': len(supported_events),
'selection_policy': (
'one_strongest_event_per_native_adapter_domain; '
'direct_mapping_before_proxy; day_before_month_before_year; '
'newest_on_equal_precision; month_or_year_uses_midpoint_day'
),
'unsupported_events': unsupported_events,
'candidates': candidate_validations,
},
}
result['three_engine_packet'] = {
**three_engine_packet,
'vedastro': {
'status': vedastro_status,
'official_closure_reason': vedastro_result.get('official_closure_reason'),
'minute_sensitive_layers': list(_VEDASTRO_MINUTE_SENSITIVE_LAYERS),
'search_events_role': 'background_only',
},
'can_confirm': parity_passed and semantic_validation_passed,
}
from scripts.rectification_technique_contract import build_rectification_technique_contract
result['technique_contract'] = build_rectification_technique_contract(
event_count=result.get('event_count', 0),
domain_count=result.get('domain_count', 0),
high_rigor=high_rigor,
local_candidate_ready=local_candidate_ready,
stability_diagnostics=result.get('stability_diagnostics'),
required_layers_complete='missing_mandatory_layers' not in result.get('reasons', []),
canonical_input_hash=result.get('canonical_input_hash', ''),
missing_required_layers=result.get('missing_layers', []),
external_validation=external_validation,
)
result['can_apply'] = bool(result['technique_contract']['confirmation_allowed'])
if high_rigor:
from scripts.rectification_three_engine_packet import build_packet
result['three_engine_packet'] = build_packet({
'year': parsed_birth_date.year,
'month': parsed_birth_date.month,
'day': parsed_birth_date.day,
'hour': int(start_time.split(':', 1)[0]),
'minute': int(start_time.split(':', 1)[1]),
'lat': lat,
'lon': lon,
'tz': tz,
},
enqueue_vedastro_gateway=True,
vedastro_question='High-rigor birth-time rectification evidence packet',
vedastro_reference_date=datetime.now().strftime('%Y-%m-%d'),
)
result['can_apply'] = False
result.setdefault('reasons', []).append('three_engine_parity_not_passed')
result['reasons'] = list(dict.fromkeys(
result.get('reasons', []) + result['technique_contract']['hard_blockers']
))
return {
'success': True,
'endpoint': 'active_rectification_events',
@@ -7147,7 +7577,7 @@ class JyotishAPIHandler(BaseHTTPRequestHandler):
except (KeyError, TypeError, ValueError) as exc:
raise BadRequest(str(exc)) from exc
result['can_apply'] = False
result.setdefault('reasons', []).append('minute_holdout_not_ready')
result.setdefault('reasons', []).append('vedastro_validation_required')
return {'success': True, 'endpoint': 'dynamic_rectification_score', **result}
def _compute_case_validation(self, body):
+46 -16
View File
@@ -13,35 +13,57 @@ def build_rectification_technique_contract(
event_count: int,
domain_count: int,
high_rigor: bool = False,
local_candidate_ready: bool = False,
stability_diagnostics: dict[str, Any] | None = None,
required_layers_complete: bool = False,
canonical_input_hash: str = "",
missing_required_layers: list[str] | None = None,
external_validation: dict[str, Any] | None = None,
) -> dict[str, Any]:
blockers: list[str] = []
if event_count < 3:
blockers.append("insufficient_events")
if domain_count < 2:
blockers.append("insufficient_domains")
if high_rigor:
blockers.append("three_engine_parity_not_passed")
neighbor = (stability_diagnostics or {}).get("neighbor_stability") or {}
leave_one_out = (stability_diagnostics or {}).get("leave_one_event_out") or {}
if not neighbor.get("all_required_passed"):
blockers.append("neighbor_stability_not_passed")
if leave_one_out.get("status") != "pass":
blockers.append("leave_one_event_out_not_passed")
external = external_validation or {}
external_status = str(external.get("status") or "not_evaluated")
if not local_candidate_ready:
blockers.append("local_candidate_not_ready")
if not required_layers_complete:
blockers.append("required_layers_incomplete")
blockers.append("minute_holdout_not_ready")
if not high_rigor:
blockers.append("vedastro_validation_required")
elif external_status != "pass":
blockers.extend(external.get("blockers") or ["vedastro_validation_not_passed"])
confirmation_allowed = (
event_count >= 3
and domain_count >= 2
and local_candidate_ready
and required_layers_complete
and high_rigor
and external_status == "pass"
)
gates = {
"event_quality": _gate("pass" if event_count >= 3 else "fail", "requires_at_least_three_dated_events"),
"cross_domain_coverage": _gate("pass" if domain_count >= 2 else "fail", "requires_at_least_two_event_domains"),
"local_candidate": _gate("pass" if local_candidate_ready else "fail", "requires_a_unique_leading_candidate_range_no_wider_than_fifteen_minutes"),
"required_layers": _gate("pass" if required_layers_complete else "fail", "all_event_required_layers_must_compute"),
"neighbor_stability": _gate("pass" if neighbor.get("all_required_passed") else "fail", "requires_unique_lead_at_plus_minus_1_2_5_minutes"),
"leave_one_event_out": _gate("pass" if leave_one_out.get("status") == "pass" else "fail", "leader_must_survive_removing_each_event"),
"three_engine_input_parity": _gate("fail" if high_rigor else "not_evaluated", "same_normalized_input_and_domain_parity_required"),
"public_holdout_release": _gate("blocked", "frozen_public_AA_minute_holdout_is_below_20_cases"),
"neighbor_stability": _gate("pass" if neighbor.get("all_required_passed") else "diagnostic_fail", "diagnostic_only_unique_lead_at_plus_minus_1_2_5_minutes"),
"leave_one_event_out": _gate("pass" if leave_one_out.get("status") == "pass" else "diagnostic_fail", "diagnostic_only_leader_survival_after_removing_each_event"),
"three_engine_input_parity": _gate(
"pass" if external.get("mismatch_count") == 0 and external.get("engine_status") else "fail" if high_rigor and external_status != "not_evaluated" else "not_evaluated",
"local_pyjhora_and_jyotishganit_must_match_the_same_candidate_input",
),
"vedastro_official_response": _gate(
"pass" if external.get("vedastro_status") == "official_verified" else "fail" if high_rigor and external_status != "not_evaluated" else "not_evaluated",
str(external.get("vedastro_reason") or "official_vedastro_response_required_before_minute_sensitive_validation"),
),
"vedastro_minute_sensitive_validation": _gate(
"pass" if (external.get("minute_sensitive_validation") or {}).get("status") == "pass" else "fail" if high_rigor and external_status != "not_evaluated" else "not_evaluated",
"official_ascendant_house_D9_D10_and_dasha_candidate_identity_must_discriminate_the_local_winner_from_runner_up",
),
}
reported_missing_layers = list(dict.fromkeys([
*(missing_required_layers or []),
@@ -56,12 +78,20 @@ def build_rectification_technique_contract(
"missing_layers": reported_missing_layers,
"partial_layers": ["shadbala_sthana_drik_naisargika"],
"auxiliary_layers": ["functional_benefic_malefic", "controlled_transit", "ashtakavarga", "shadbala_verified_components"],
"external_engines": {"status": "required_not_run" if high_rigor else "not_run", "providers": ["pyjhora", "jyotishganit", "vedastro"]},
"external_engines": {
"status": external_status,
"providers": ["pyjhora", "jyotishganit", "vedastro"],
"validation": external,
},
"canonical_input_hash": canonical_input_hash,
"gates": gates,
"hard_blockers": list(dict.fromkeys(blockers)),
"decision": "continue_rectification",
"confirmation_allowed": False,
"can_narrow_to_minute": False,
"boundary": "A candidate range is not a confirmed birth minute. Minute confirmation is disabled until the frozen public AA holdout release gate passes.",
"decision": "confirm_minute" if confirmation_allowed else "continue_rectification",
"confirmation_allowed": confirmation_allowed,
"can_narrow_to_minute": confirmation_allowed,
"boundary": (
"The narrow local candidate passed required-layer, three-engine, and official VedAstro minute-sensitive identity validation. SearchEvents is background evidence only. Neighbor and leave-one-event-out stability remain diagnostic confidence indicators. Explicit user confirmation is still required before replacing the stored birth time."
if confirmation_allowed
else "A candidate range is not a confirmed birth minute. Continue until local candidate calculation, required layers, three-engine parity, and official VedAstro minute-sensitive identity validation pass. SearchEvents remains background evidence only; neighbor and leave-one-event-out results are diagnostic rather than hard blockers."
),
}
@@ -2,6 +2,7 @@
from __future__ import annotations
import importlib
import os
import sys
from datetime import datetime
from pathlib import Path
@@ -24,6 +25,7 @@ except ModuleNotFoundError: # pragma: no cover - direct script execution
ROOT = Path(__file__).resolve().parents[1]
JYOTISHGANIT_ROOT = ROOT / "references" / "open_source_sources" / "jyotishganit"
JYOTISHGANIT_DATA_DIR = ROOT / ".cache" / "jyotishganit"
PLANETS = ("Sun", "Moon", "Mars", "Mercury", "Jupiter", "Venus", "Saturn")
SIGNS = ("Aries", "Taurus", "Gemini", "Cancer", "Leo", "Virgo", "Libra", "Scorpio", "Sagittarius", "Capricorn", "Aquarius", "Pisces")
@@ -54,7 +56,12 @@ def _pyjhora_d1(case: dict[str, Any]) -> dict[str, str]:
return {index_to_planet[body]: SIGNS[int(position[0])] for body, position in charts.rasi_chart(jd, place) if body in index_to_planet}
def _ensure_jyotishganit_data_dir() -> str:
return os.environ.setdefault("JYOTISHGANIT_DATA_DIR", str(JYOTISHGANIT_DATA_DIR))
def _jyotishganit_d1(case: dict[str, Any]) -> dict[str, str]:
_ensure_jyotishganit_data_dir()
sys.path.insert(0, str(JYOTISHGANIT_ROOT))
try:
from jyotishganit import calculate_birth_chart, get_birth_chart_json
+120 -11
View File
@@ -2748,6 +2748,126 @@ def run_official_full_snapshot_for_case(
return _store_official_full_snapshot_semantic_cache(case, case_id, result)
def _rectification_position(value: Any) -> dict[str, Any] | None:
if not isinstance(value, dict) or not value.get("sign"):
return None
position = {"sign": str(value["sign"])}
degree = value.get("degree_in_sign")
if isinstance(degree, (int, float)):
position["degree_in_sign"] = round(float(degree), 6)
return position
def _rectification_varga_positions(
chart: dict[str, Any],
varga: str,
) -> tuple[dict[str, Any], dict[str, Any]]:
houses = chart.get("houses") if isinstance(chart.get("houses"), dict) else {}
planets = chart.get("planets") if isinstance(chart.get("planets"), dict) else {}
house_positions = {
name: position
for name, item in sorted(houses.items())
if isinstance(item, dict)
and (position := _rectification_position((item.get("vargas") or {}).get(varga)))
}
planet_positions = {
name: position
for name, item in sorted(planets.items())
if isinstance(item, dict)
and (position := _rectification_position((item.get("vargas") or {}).get(varga)))
}
return house_positions, planet_positions
def _rectification_timeline_item_count(value: Any) -> int:
if isinstance(value, list):
return len(value)
if isinstance(value, dict):
return max(
(_rectification_timeline_item_count(item) for item in value.values()),
default=0,
)
return 0
def run_rectification_minute_snapshot_for_case(
case: dict[str, Any],
*,
case_id: str = "user_chart",
) -> dict[str, Any]:
"""Return a safe official fingerprint for minute-candidate comparison.
The summary intentionally excludes raw official responses. SearchEvents is
not part of this fingerprint; it remains a separate background check.
"""
snapshot = run_official_full_snapshot_for_case(case, case_id=case_id)
chart = snapshot.get("official_chart") if isinstance(snapshot.get("official_chart"), dict) else {}
houses = chart.get("houses") if isinstance(chart.get("houses"), dict) else {}
ascendant = _rectification_position(chart.get("ascendant"))
house_positions = {
name: position
for name, item in sorted(houses.items())
if (position := _rectification_position(item))
}
d9_houses, d9_planets = _rectification_varga_positions(chart, "D9")
d10_houses, d10_planets = _rectification_varga_positions(chart, "D10")
sections = snapshot.get("snapshot_sections") if isinstance(snapshot.get("snapshot_sections"), dict) else {}
section_statuses = snapshot.get("section_statuses") if isinstance(snapshot.get("section_statuses"), dict) else {}
dasha_payload = sections.get("dasha_all")
ascendant_house_payload = {
"ascendant": ascendant,
"houses": house_positions,
}
d9_payload = {"houses": d9_houses, "planets": d9_planets}
d10_payload = {"houses": d10_houses, "planets": d10_planets}
dasha_available = section_statuses.get("dasha_all") == "ok" and isinstance(dasha_payload, dict)
layers = {
"ascendant_house_boundaries": {
"status": "ok" if ascendant and house_positions else "missing",
**ascendant_house_payload,
"fingerprint": _hash_payload(ascendant_house_payload) if ascendant and house_positions else None,
},
"D9": {
"status": "ok" if d9_houses or d9_planets else "missing",
**d9_payload,
"fingerprint": _hash_payload(d9_payload) if d9_houses or d9_planets else None,
},
"D10": {
"status": "ok" if d10_houses or d10_planets else "missing",
**d10_payload,
"fingerprint": _hash_payload(d10_payload) if d10_houses or d10_planets else None,
},
"dasha_boundaries": {
"status": "ok" if dasha_available else "missing",
"boundary_count": _rectification_timeline_item_count(dasha_payload) if dasha_available else 0,
"fingerprint": _hash_payload(dasha_payload) if dasha_available else None,
},
"kp_cusp_sub_lord": {
"status": "unsupported_by_verified_official_interface",
"reason": "The verified VedAstro interface exposes KP house-membership helpers, not an auditable cusp/sub-lord result.",
},
}
required_statuses = [layers[name]["status"] for name in (
"ascendant_house_boundaries", "D9", "D10", "dasha_boundaries"
)]
available = bool(snapshot.get("available")) and all(status == "ok" for status in required_statuses)
source_metadata = snapshot.get("source_metadata") if isinstance(snapshot.get("source_metadata"), dict) else {}
return {
"available": available,
"status": "ok" if available else "partial" if any(status == "ok" for status in required_statuses) else "blocked",
"source": "vedastro_official",
"operation": "rectification_minute_snapshot",
"candidate_time": f'{int(case.get("hour") or 0):02d}:{int(case.get("minute") or 0):02d}',
"layers": layers,
"kp_cusp_sub_lord": layers["kp_cusp_sub_lord"],
"source_metadata": {
"response_hash": source_metadata.get("response_hash"),
"official_snapshot_status": snapshot.get("status"),
},
}
def _run_range_scan_case(case: dict[str, Any], domain: str, start_date: str, end_date: str) -> dict[str, Any]:
if domain not in SUPPORTED_RANGE_SCAN_DOMAINS:
return {
@@ -2776,17 +2896,6 @@ def _run_range_scan_case(case: dict[str, Any], domain: str, start_date: str, end
"source_metadata": _source_metadata(endpoint),
}
range_scan_enabled = os.environ.get("VEDASTRO_RANGE_SCAN_NETWORK_ENABLED", "1").strip().lower() in {"1", "true", "yes", "on"}
if not range_scan_enabled:
return {
"backend": "vedastro_service_adapter_candidate",
"available": False,
"status": "network_execution_disabled",
"reason": "VEDASTRO_RANGE_SCAN_NETWORK_ENABLED is disabled for the interactive chat path.",
"request_preview": request_preview,
"source_metadata": _source_metadata(endpoint),
}
sample_dates = _iter_sample_dates(start_date, end_date)
reports: list[dict[str, Any]] = []
for sample_date in sample_dates: