fix(consult): attach transit windows and VedAstro minute snapshots
Consult only copied Sade Sati and never searched trigger dates; V9 score left official minute identity unevaluated. Search a 90-day slow-planet window and run two-candidate snapshots with timeout staying not_evaluated. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+329
-18
@@ -352,6 +352,151 @@ def _compare_vedastro_minute_snapshots(candidate_snapshots):
|
||||
}
|
||||
|
||||
|
||||
def _vedastro_minute_sensitive_candidate_times(result) -> list[str]:
|
||||
decisions = result.get('candidate_decisions') if isinstance(result, dict) else None
|
||||
times = []
|
||||
if not isinstance(decisions, list):
|
||||
return times
|
||||
for item in decisions:
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
time_value = item.get('time')
|
||||
if (
|
||||
isinstance(time_value, str)
|
||||
and re.fullmatch(r'(?:[01]\d|2[0-3]):[0-5]\d', time_value)
|
||||
and time_value not in times
|
||||
):
|
||||
times.append(time_value)
|
||||
if len(times) == 2:
|
||||
break
|
||||
return times
|
||||
|
||||
|
||||
def _unevaluated_vedastro_minute_sensitive(reason: str) -> dict:
|
||||
return {
|
||||
'status': 'not_evaluated',
|
||||
'summary': {
|
||||
'reason': reason,
|
||||
'discriminated': False,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def _evaluate_vedastro_minute_sensitive_pair(request, times: list[str]) -> dict:
|
||||
if not isinstance(request, dict) or len(times) != 2:
|
||||
return _unevaluated_vedastro_minute_sensitive('vedastro_minute_pair_unavailable')
|
||||
try:
|
||||
birth_day = datetime.strptime(str(request.get('birth_date') or ''), '%Y-%m-%d')
|
||||
lat = float(request['lat'])
|
||||
lon = float(request['lon'])
|
||||
tz = float(request['tz'])
|
||||
except (KeyError, TypeError, ValueError):
|
||||
return _unevaluated_vedastro_minute_sensitive('vedastro_minute_request_incomplete')
|
||||
try:
|
||||
adapter = _load_local_module('vedastro_service_adapter')
|
||||
except Exception:
|
||||
return _unevaluated_vedastro_minute_sensitive('vedastro_minute_adapter_unavailable')
|
||||
|
||||
def snapshot_for(candidate_time: str):
|
||||
hour, minute = candidate_time.split(':', 1)
|
||||
case = {
|
||||
'year': birth_day.year,
|
||||
'month': birth_day.month,
|
||||
'day': birth_day.day,
|
||||
'hour': int(hour),
|
||||
'minute': int(minute),
|
||||
'second': 0,
|
||||
'lat': lat,
|
||||
'lon': lon,
|
||||
'tz': tz,
|
||||
}
|
||||
return _safe_vedastro_adapter_call(
|
||||
adapter.run_rectification_minute_snapshot_for_case,
|
||||
case,
|
||||
case_id=f'rectification_v5_minute_{candidate_time.replace(":", "")}',
|
||||
)
|
||||
|
||||
timeout = _foreground_vedastro_budget_seconds()
|
||||
futures = [_FOREGROUND_VEDASTRO_EXECUTOR.submit(snapshot_for, candidate_time) for candidate_time in times]
|
||||
reports = []
|
||||
deadline = time.monotonic() + timeout
|
||||
for future in futures:
|
||||
remaining = max(0.0, deadline - time.monotonic())
|
||||
try:
|
||||
reports.append(future.result(timeout=remaining))
|
||||
except FuturesTimeoutError:
|
||||
return _unevaluated_vedastro_minute_sensitive('vedastro_minute_snapshot_timeout')
|
||||
except Exception:
|
||||
return _unevaluated_vedastro_minute_sensitive('vedastro_minute_snapshot_error')
|
||||
summaries = [
|
||||
_safe_vedastro_minute_snapshot_summary(candidate_time, report)
|
||||
for candidate_time, report in zip(times, reports)
|
||||
]
|
||||
if any(
|
||||
report.get('status') == 'timeout' or report.get('_failure_kind') == 'timeout'
|
||||
for report in reports
|
||||
):
|
||||
return _unevaluated_vedastro_minute_sensitive('vedastro_minute_snapshot_timeout')
|
||||
complete = all(
|
||||
_vedastro_minute_snapshot_is_complete(report, summary)
|
||||
for report, summary in zip(reports, summaries)
|
||||
)
|
||||
if not complete:
|
||||
return _unevaluated_vedastro_minute_sensitive('vedastro_minute_snapshot_incomplete')
|
||||
comparison = _compare_vedastro_minute_snapshots(summaries)
|
||||
discriminated_layers = [
|
||||
str(name) for name in (comparison.get('discriminated_layers') or [])[:6] if name
|
||||
]
|
||||
if comparison.get('discriminated'):
|
||||
return {
|
||||
'status': 'passed',
|
||||
'summary': {
|
||||
'reason': 'minute_sensitive_layers_discriminated',
|
||||
'discriminated': True,
|
||||
'discriminated_layers': discriminated_layers,
|
||||
},
|
||||
}
|
||||
return {
|
||||
'status': 'failed',
|
||||
'summary': {
|
||||
'reason': 'vedastro_minute_sensitive_layers_not_discriminated',
|
||||
'discriminated': False,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def _attach_vedastro_minute_sensitive_to_score(result, request):
|
||||
if not isinstance(result, dict):
|
||||
return result
|
||||
receipt = result.get('decision_receipt')
|
||||
if not isinstance(receipt, dict):
|
||||
receipt = result.get('candidate_decision_receipt')
|
||||
if not isinstance(receipt, dict):
|
||||
return result
|
||||
exact = receipt.setdefault('gates', {}).setdefault('exact_confirmation', {})
|
||||
if not isinstance(exact, dict):
|
||||
exact = {}
|
||||
receipt.setdefault('gates', {})['exact_confirmation'] = exact
|
||||
times = _vedastro_minute_sensitive_candidate_times(result)
|
||||
if len(times) < 2 or receipt.get('acceptance_allowed') is not True:
|
||||
exact.setdefault('external_validation_status', 'not_evaluated')
|
||||
exact.setdefault('vedastro_minute_sensitive', {
|
||||
'reason': 'local_candidate_not_ready_for_minute_sensitive_check',
|
||||
'discriminated': False,
|
||||
})
|
||||
result['decision_receipt'] = receipt
|
||||
if isinstance(result.get('candidate_decision_receipt'), dict):
|
||||
result['candidate_decision_receipt'] = receipt
|
||||
return result
|
||||
evaluation = _evaluate_vedastro_minute_sensitive_pair(request, times)
|
||||
exact['external_validation_status'] = evaluation['status']
|
||||
exact['vedastro_minute_sensitive'] = evaluation['summary']
|
||||
result['decision_receipt'] = receipt
|
||||
if 'candidate_decision_receipt' in result:
|
||||
result['candidate_decision_receipt'] = receipt
|
||||
return result
|
||||
|
||||
|
||||
def _safe_vedastro_adapter_call(call, *args, **kwargs):
|
||||
try:
|
||||
result = call(*args, **kwargs)
|
||||
@@ -582,6 +727,10 @@ _WESTERN_TIMING_LAYER_NAMES = (
|
||||
'transit_duration_scan',
|
||||
'parans',
|
||||
)
|
||||
_CONSULTATION_TRANSIT_WINDOW_DAYS = 90
|
||||
_CONSULTATION_TRANSIT_PLANETS = ('Saturn', 'Jupiter', 'Rahu', 'Ketu')
|
||||
_WESTERN_ASPECT_KEYS = ('transit_planet', 'natal_point', 'aspect', 'orb')
|
||||
_WESTERN_WINDOW_KEYS = ('transit_planet', 'natal_point', 'aspect', 'start_date', 'end_date', 'min_orb')
|
||||
|
||||
|
||||
def _western_default_timing(body: dict) -> dict:
|
||||
@@ -880,6 +1029,159 @@ def _sign_of(point) -> str | None:
|
||||
return None
|
||||
|
||||
|
||||
def _iso_date_only(value) -> str | None:
|
||||
if hasattr(value, 'strftime'):
|
||||
try:
|
||||
return value.strftime('%Y-%m-%d')
|
||||
except (TypeError, ValueError, OverflowError):
|
||||
return None
|
||||
if isinstance(value, str) and re.match(r'\d{4}-\d{2}-\d{2}', value):
|
||||
return value[:10]
|
||||
return None
|
||||
|
||||
|
||||
def _consultation_point_lon(point) -> float | None:
|
||||
if not isinstance(point, dict):
|
||||
return None
|
||||
raw = point.get('lon')
|
||||
if raw is None:
|
||||
raw = point.get('longitude')
|
||||
if raw is None:
|
||||
raw = point.get('degree_raw')
|
||||
try:
|
||||
return float(raw)
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
|
||||
|
||||
def _compact_consultation_trigger(item) -> dict | None:
|
||||
if not isinstance(item, dict):
|
||||
return None
|
||||
date = _iso_date_only(item.get('date') or item.get('start_date'))
|
||||
end = _iso_date_only(item.get('end_date'))
|
||||
planet = item.get('planet')
|
||||
target = item.get('target') or item.get('sensitive_point')
|
||||
kind = item.get('kind') or item.get('type') or item.get('event')
|
||||
compact = {}
|
||||
if date:
|
||||
compact['date'] = date
|
||||
if isinstance(planet, str) and planet.strip():
|
||||
compact['planet'] = planet.strip()
|
||||
if isinstance(target, str) and target.strip():
|
||||
compact['target'] = target.strip()
|
||||
if isinstance(kind, str) and kind.strip():
|
||||
compact['kind'] = kind.strip()[:80]
|
||||
orb = item.get('orb')
|
||||
if isinstance(orb, (int, float)):
|
||||
compact['orb'] = round(float(orb), 2)
|
||||
if date and end and end != date:
|
||||
compact['window'] = f'{date}..{end}'
|
||||
return compact or None
|
||||
|
||||
|
||||
def _consultation_transit_search_window(body: dict) -> tuple[datetime, datetime]:
|
||||
start = _consultation_reference_date(body).replace(tzinfo=None, hour=0, minute=0, second=0, microsecond=0)
|
||||
return start, start + timedelta(days=_CONSULTATION_TRANSIT_WINDOW_DAYS)
|
||||
|
||||
|
||||
def _search_consultation_transit_triggers(chart: dict, body: dict, *, ayanamsa: str) -> tuple[list, dict]:
|
||||
start, end = _consultation_transit_search_window(body)
|
||||
period = {'start': start.strftime('%Y-%m-%d'), 'end': end.strftime('%Y-%m-%d')}
|
||||
existing = chart.get('transit_triggers')
|
||||
if isinstance(existing, list) and existing:
|
||||
compact = [row for row in (_compact_consultation_trigger(item) for item in existing[:6]) if row]
|
||||
if compact:
|
||||
return compact, period
|
||||
planets = chart.get('planets') if isinstance(chart.get('planets'), dict) else {}
|
||||
ascendant = chart.get('ascendant') if isinstance(chart.get('ascendant'), dict) else {}
|
||||
points = []
|
||||
asc_lon = _consultation_point_lon(ascendant)
|
||||
if asc_lon is not None:
|
||||
points.append(('Ascendant', asc_lon))
|
||||
moon = planets.get('Moon') if isinstance(planets.get('Moon'), dict) else {}
|
||||
moon_lon = _consultation_point_lon(moon)
|
||||
if moon_lon is not None:
|
||||
points.append(('Moon', moon_lon))
|
||||
if not points:
|
||||
return [], period
|
||||
transit_mod = _load_local_module('transit_trigger')
|
||||
found = []
|
||||
for name, degree in points:
|
||||
for planet in _CONSULTATION_TRANSIT_PLANETS:
|
||||
rows = transit_mod.search_transit_triggers(
|
||||
planet,
|
||||
degree,
|
||||
start,
|
||||
end,
|
||||
ayanamsa_name=ayanamsa,
|
||||
)
|
||||
for row in rows:
|
||||
if not isinstance(row, dict):
|
||||
continue
|
||||
payload = dict(row)
|
||||
payload.setdefault('planet', planet)
|
||||
payload.setdefault('sensitive_point', name)
|
||||
compact = _compact_consultation_trigger(payload)
|
||||
if compact:
|
||||
found.append(compact)
|
||||
if len(found) >= 6:
|
||||
found.sort(key=lambda item: item.get('date') or '9999-12-31')
|
||||
return found[:6], period
|
||||
found.sort(key=lambda item: item.get('date') or '9999-12-31')
|
||||
return found[:6], period
|
||||
|
||||
|
||||
def _compact_scalar_rows(rows, keys, limit=6) -> list:
|
||||
compact = []
|
||||
if not isinstance(rows, list):
|
||||
return compact
|
||||
for item in rows:
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
row = {}
|
||||
for key in keys:
|
||||
value = item.get(key)
|
||||
if key in {'start_date', 'end_date', 'target_date'} or str(key).endswith('_date'):
|
||||
value = _iso_date_only(value)
|
||||
if value is None or value == '':
|
||||
continue
|
||||
if isinstance(value, float):
|
||||
row[key] = round(value, 4)
|
||||
elif isinstance(value, (str, int, bool)):
|
||||
row[key] = value
|
||||
if row:
|
||||
compact.append(row)
|
||||
if len(compact) >= limit:
|
||||
break
|
||||
return compact
|
||||
|
||||
|
||||
def _compact_western_timing_layer(layer: dict) -> dict:
|
||||
compact = {
|
||||
'status': _audit_row_status(layer.get('status') or 'executed'),
|
||||
'boundary': layer.get('boundary') or layer.get('reason'),
|
||||
}
|
||||
for key in ('target_date', 'start_date', 'end_date'):
|
||||
date = _iso_date_only(layer.get(key))
|
||||
if date:
|
||||
compact[key] = date
|
||||
year = layer.get('target_year')
|
||||
if isinstance(year, int):
|
||||
compact['target_year'] = year
|
||||
return_date = _iso_date_only(layer.get('return_local_time'))
|
||||
if return_date:
|
||||
compact['return_date'] = return_date
|
||||
aspects = _compact_scalar_rows(layer.get('aspects'), _WESTERN_ASPECT_KEYS)
|
||||
if aspects:
|
||||
compact['aspects'] = aspects
|
||||
windows = _compact_scalar_rows(layer.get('windows'), _WESTERN_WINDOW_KEYS)
|
||||
if windows:
|
||||
compact['windows'] = windows
|
||||
if isinstance(layer.get('event_count'), int):
|
||||
compact['event_count'] = layer['event_count']
|
||||
return compact
|
||||
|
||||
|
||||
def _compact_western_spectrum(packet: dict | None) -> dict:
|
||||
if not isinstance(packet, dict) or not packet:
|
||||
return {
|
||||
@@ -896,10 +1198,7 @@ def _compact_western_spectrum(packet: dict | None) -> dict:
|
||||
for name in _WESTERN_TIMING_LAYER_NAMES:
|
||||
layer = timing.get(name)
|
||||
if isinstance(layer, dict):
|
||||
techniques[name] = {
|
||||
'status': _audit_row_status(layer.get('status') or 'executed'),
|
||||
'boundary': layer.get('boundary') or layer.get('reason'),
|
||||
}
|
||||
techniques[name] = _compact_western_timing_layer(layer)
|
||||
else:
|
||||
techniques[name] = {'status': 'blocked', 'boundary': 'layer_not_materialized'}
|
||||
natal_status = 'executed' if packet.get('status') != 'blocked' and natal_block else str(packet.get('status') or 'blocked')
|
||||
@@ -1358,21 +1657,31 @@ def _attach_local_consultation_layers(handler, chart: dict, birth_payload: dict,
|
||||
if not compact_sade:
|
||||
compact_sade = None
|
||||
triggers = []
|
||||
raw_triggers = chart.get('transit_triggers') or (modules.get('transit_triggers') if isinstance(modules.get('transit_triggers'), list) else [])
|
||||
if isinstance(raw_triggers, list):
|
||||
for item in raw_triggers[:6]:
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
triggers.append({
|
||||
key: item[key]
|
||||
for key in ('date', 'planet', 'target', 'kind', 'orb')
|
||||
if key in item and item[key] is not None
|
||||
})
|
||||
search_period = None
|
||||
try:
|
||||
triggers, search_period = _search_consultation_transit_triggers(
|
||||
chart,
|
||||
body,
|
||||
ayanamsa=str(body.get('ayanamsa') or birth_payload.get('ayanamsa') or 'lahiri'),
|
||||
)
|
||||
except Exception as trigger_exc:
|
||||
start, end = _consultation_transit_search_window(body)
|
||||
search_period = {'start': start.strftime('%Y-%m-%d'), 'end': end.strftime('%Y-%m-%d')}
|
||||
diagnostics.append({
|
||||
'layer': 'transit_triggers',
|
||||
'status': 'unavailable',
|
||||
'reason': trigger_exc.__class__.__name__,
|
||||
})
|
||||
modules['transits'] = {
|
||||
'status': 'executed' if compact_sade or triggers else 'blocked',
|
||||
'status': 'executed' if compact_sade or triggers or search_period else 'blocked',
|
||||
'sade_sati': compact_sade,
|
||||
'triggers': triggers,
|
||||
'boundary': 'observation windows, not guaranteed events',
|
||||
'trigger_count': len(triggers),
|
||||
'search_period': search_period,
|
||||
'boundary': (
|
||||
'observation windows, not guaranteed events; '
|
||||
'empty triggers means none in the searched window, not that transits were skipped'
|
||||
),
|
||||
}
|
||||
except Exception as exc:
|
||||
modules['transits'] = {
|
||||
@@ -8691,18 +9000,20 @@ class JyotishAPIHandler(BaseHTTPRequestHandler):
|
||||
|
||||
def _compute_rectification_v5_score(self, body):
|
||||
from scripts.rectification.api_service import score_candidates
|
||||
request = self._rectification_v5_request(body)
|
||||
return {
|
||||
'success': True,
|
||||
'endpoint': 'rectification_v5_score',
|
||||
**score_candidates(self._rectification_v5_request(body)),
|
||||
**_attach_vedastro_minute_sensitive_to_score(score_candidates(request), request),
|
||||
}
|
||||
|
||||
def _compute_rectification_v5_diagnostics(self, body):
|
||||
from scripts.rectification.api_service import diagnostics
|
||||
request = self._rectification_v5_request(body)
|
||||
return {
|
||||
'success': True,
|
||||
'endpoint': 'rectification_v5_diagnostics',
|
||||
**diagnostics(self._rectification_v5_request(body)),
|
||||
**_attach_vedastro_minute_sensitive_to_score(diagnostics(request), request),
|
||||
}
|
||||
|
||||
def _compute_rectification_v5_vedastro_validate(self, body):
|
||||
|
||||
Reference in New Issue
Block a user