fix(consult): run the local skill's full technique spectrum on the web path
Web answers were thinner than a local Agent calling yinduzhanxing-skill: theme-subset vargas, no visible audit table, and a prompt that dropped the invocation contract. Bind the commercial method, compute D1–D60 plus Western layers, and deliver the same Full-Spectrum checklist. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -7,6 +7,7 @@ from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
PLAN_VERSION = "consultation-plan-v2"
|
||||
_FULL_SPECTRUM_EVIDENCE = ("natal_foundation", "domain", "timing", "validation")
|
||||
_ALLOWED_DEPTHS = {"concise", "standard", "deep", "research"}
|
||||
_ALLOWED_HORIZONS = {"next_3_months", "next_12_months", "next_24_months", "long_term"}
|
||||
_ALLOWED_PRECISION_BOUNDARIES = {"server_evidence_required", "precise_timing_blocked"}
|
||||
@@ -38,7 +39,7 @@ _ROUTE_CONTRACTS = {
|
||||
themes=("career",),
|
||||
resolved_routes=("career",),
|
||||
requested_domains=("career",),
|
||||
required_evidence_categories=("natal_foundation", "domain"),
|
||||
required_evidence_categories=_FULL_SPECTRUM_EVIDENCE,
|
||||
required_layers=("D1", "D10", "10th house/lord", "A10", "AmK", "Vimshottari", "Narayana", "Transit"),
|
||||
claim_boundary="career_direction_and_broad_timing_only",
|
||||
),
|
||||
@@ -46,7 +47,7 @@ _ROUTE_CONTRACTS = {
|
||||
themes=("marriage",),
|
||||
resolved_routes=("marriage", "relationship"),
|
||||
requested_domains=("marriage",),
|
||||
required_evidence_categories=("natal_foundation", "domain"),
|
||||
required_evidence_categories=_FULL_SPECTRUM_EVIDENCE,
|
||||
required_layers=("D1", "D9", "7th house/lord", "Venus/Jupiter", "DK", "UL", "A7", "Vimshottari", "Narayana", "Transit"),
|
||||
claim_boundary="relationship_pattern_and_broad_window_only",
|
||||
),
|
||||
@@ -54,7 +55,7 @@ _ROUTE_CONTRACTS = {
|
||||
themes=("wealth",),
|
||||
resolved_routes=("wealth", "finance"),
|
||||
requested_domains=("wealth",),
|
||||
required_evidence_categories=("natal_foundation", "domain"),
|
||||
required_evidence_categories=_FULL_SPECTRUM_EVIDENCE,
|
||||
required_layers=("D1", "D2", "D11", "2nd/11th/9th/5th houses", "Wealth Yogas", "Ashtakavarga", "Dasha"),
|
||||
claim_boundary="wealth_structure_not_financial_advice",
|
||||
),
|
||||
@@ -62,7 +63,7 @@ _ROUTE_CONTRACTS = {
|
||||
themes=("health",),
|
||||
resolved_routes=("health",),
|
||||
requested_domains=("health",),
|
||||
required_evidence_categories=("natal_foundation", "domain"),
|
||||
required_evidence_categories=_FULL_SPECTRUM_EVIDENCE,
|
||||
required_layers=("D1", "D6", "D8", "6th/8th houses", "Dasha", "non-medical boundary"),
|
||||
claim_boundary="wellbeing_pressure_patterns_not_medical_diagnosis",
|
||||
),
|
||||
@@ -70,7 +71,7 @@ _ROUTE_CONTRACTS = {
|
||||
themes=("education",),
|
||||
resolved_routes=("education",),
|
||||
requested_domains=("education",),
|
||||
required_evidence_categories=("natal_foundation", "domain"),
|
||||
required_evidence_categories=_FULL_SPECTRUM_EVIDENCE,
|
||||
required_layers=("D1", "D24", "5th/9th houses", "Mercury/Jupiter", "Dasha"),
|
||||
claim_boundary="learning_pattern_and_broad_timing_only",
|
||||
),
|
||||
@@ -78,7 +79,7 @@ _ROUTE_CONTRACTS = {
|
||||
themes=("migration",),
|
||||
resolved_routes=("migration",),
|
||||
requested_domains=("migration",),
|
||||
required_evidence_categories=("natal_foundation", "domain"),
|
||||
required_evidence_categories=_FULL_SPECTRUM_EVIDENCE,
|
||||
required_layers=("D1", "D4", "D12", "4th/12th houses", "Dasha", "Narayana"),
|
||||
claim_boundary="migration_and_home_direction_broad_window_only",
|
||||
),
|
||||
@@ -86,7 +87,7 @@ _ROUTE_CONTRACTS = {
|
||||
themes=("family",),
|
||||
resolved_routes=("family",),
|
||||
requested_domains=("family",),
|
||||
required_evidence_categories=("natal_foundation", "domain"),
|
||||
required_evidence_categories=_FULL_SPECTRUM_EVIDENCE,
|
||||
required_layers=("D1", "D7", "D12", "4th/5th/9th houses", "Dasha"),
|
||||
claim_boundary="family_pattern_not_deterministic_event_claim",
|
||||
),
|
||||
@@ -94,7 +95,7 @@ _ROUTE_CONTRACTS = {
|
||||
themes=("annual",),
|
||||
resolved_routes=("annual",),
|
||||
requested_domains=("annual",),
|
||||
required_evidence_categories=("natal_foundation", "timing", "validation"),
|
||||
required_evidence_categories=_FULL_SPECTRUM_EVIDENCE,
|
||||
required_layers=("D1", "Annual chart boundary", "Dasha", "Transit", "Tajika candidate"),
|
||||
claim_boundary="annual_report_broad_periods_only",
|
||||
),
|
||||
@@ -102,7 +103,7 @@ _ROUTE_CONTRACTS = {
|
||||
themes=("timing",),
|
||||
resolved_routes=("timing",),
|
||||
requested_domains=("timing",),
|
||||
required_evidence_categories=("natal_foundation", "timing", "validation"),
|
||||
required_evidence_categories=_FULL_SPECTRUM_EVIDENCE,
|
||||
required_layers=("Vimshottari", "Narayana", "Transit", "Varga", "negative holdout gate"),
|
||||
claim_boundary="candidate_day_month_window_only_until_holdout_passes",
|
||||
),
|
||||
@@ -110,7 +111,7 @@ _ROUTE_CONTRACTS = {
|
||||
themes=("general",),
|
||||
resolved_routes=("general",),
|
||||
requested_domains=("general",),
|
||||
required_evidence_categories=("natal_foundation", "domain"),
|
||||
required_evidence_categories=_FULL_SPECTRUM_EVIDENCE,
|
||||
required_layers=("D1", "D9", "D10", "D2", "Dasha", "Narayana", "Transit", "Functional Benefic/Malefic"),
|
||||
claim_boundary="multi_domain_summary_with_missing_layers_disclosed",
|
||||
),
|
||||
|
||||
+469
-54
@@ -62,9 +62,9 @@ except ModuleNotFoundError: # pragma: no cover - script execution path
|
||||
)
|
||||
from vedastro_runtime_context import temporary_timeout_seconds
|
||||
try:
|
||||
from scripts.unified_consultation_orchestrator import UnifiedConsultationOrchestrator
|
||||
from scripts.unified_consultation_orchestrator import FORMAL_DIVISIONS, UnifiedConsultationOrchestrator
|
||||
except ModuleNotFoundError: # pragma: no cover - script execution path
|
||||
from unified_consultation_orchestrator import UnifiedConsultationOrchestrator
|
||||
from unified_consultation_orchestrator import FORMAL_DIVISIONS, UnifiedConsultationOrchestrator
|
||||
try:
|
||||
from scripts.consultation_domain_registry import CANONICAL_DOMAINS
|
||||
except ModuleNotFoundError: # pragma: no cover - script execution path
|
||||
@@ -517,38 +517,26 @@ def _western_evidence_packet_from_body(
|
||||
timezone=body.get('western_timezone') or birth_payload['tz'],
|
||||
house_system=str(body.get('western_house_system', 'P')),
|
||||
)
|
||||
timing_request = body.get('western_timing')
|
||||
if isinstance(timing_request, dict):
|
||||
birth = {
|
||||
'year': int(birth_payload['year']), 'month': int(birth_payload['month']), 'day': int(birth_payload['day']),
|
||||
'hour': int(birth_payload['hour']), 'minute': int(birth_payload['minute']), 'second': int(birth_payload.get('second', 0)),
|
||||
'latitude': float(birth_payload['lat']), 'longitude': float(birth_payload['lon']),
|
||||
'timezone': body.get('western_timezone') or birth_payload['tz'],
|
||||
'house_system': str(body.get('western_house_system', 'P')),
|
||||
}
|
||||
timing = build_timing_techniques(
|
||||
**birth,
|
||||
transit_date=timing_request.get('transit_date'),
|
||||
solar_return_year=timing_request.get('solar_return_year'),
|
||||
secondary_progression_date=timing_request.get('secondary_progression_date'),
|
||||
solar_arc_date=timing_request.get('solar_arc_date'),
|
||||
converse_secondary_progression_date=timing_request.get('converse_secondary_progression_date'),
|
||||
converse_solar_arc_date=timing_request.get('converse_solar_arc_date'),
|
||||
midpoint_date=timing_request.get('midpoint_date'),
|
||||
lunar_return_start_date=timing_request.get('lunar_return_start_date'),
|
||||
duration_scan_start_date=timing_request.get('duration_scan_start_date'),
|
||||
duration_scan_end_date=timing_request.get('duration_scan_end_date'),
|
||||
parans_date=timing_request.get('parans_date'),
|
||||
birth = {
|
||||
'year': int(birth_payload['year']), 'month': int(birth_payload['month']), 'day': int(birth_payload['day']),
|
||||
'hour': int(birth_payload['hour']), 'minute': int(birth_payload['minute']), 'second': int(birth_payload.get('second', 0)),
|
||||
'latitude': float(birth_payload['lat']), 'longitude': float(birth_payload['lon']),
|
||||
'timezone': body.get('western_timezone') or birth_payload['tz'],
|
||||
'house_system': str(body.get('western_house_system', 'P')),
|
||||
}
|
||||
timing = build_timing_techniques(**birth, **_western_default_timing(body))
|
||||
if timing:
|
||||
duration = timing.get('transit_duration_scan')
|
||||
if isinstance(duration, dict):
|
||||
duration.pop('daily_hits', None)
|
||||
packet['timing_techniques'] = timing
|
||||
packet['sections']['timing_techniques'] = {'status': 'used', 'source_path': 'western.native_timing'}
|
||||
packet['missing_sections'] = [item for item in packet['missing_sections'] if item != 'timing_techniques']
|
||||
packet['boundary'] = (
|
||||
'Native Western natal plus implemented timing layers are attached by default on natal '
|
||||
'consultation. Outputs do not infer outcomes or interpretation. Unimplemented or failed '
|
||||
'layers are listed as blocked, never silently omitted.'
|
||||
)
|
||||
if timing:
|
||||
packet['timing_techniques'] = timing
|
||||
packet['sections']['timing_techniques'] = {'status': 'used', 'source_path': 'western.native_timing'}
|
||||
packet['missing_sections'] = [item for item in packet['missing_sections'] if item != 'timing_techniques']
|
||||
packet['boundary'] = (
|
||||
'Native calculations include only explicitly requested transit, solar-return, secondary-progression, '
|
||||
'solar-arc, midpoint, lunar-return and daily duration-scan layers; parans remain blocked until a '
|
||||
'dedicated latitude-aware event solver is implemented. Outputs do not infer outcomes or interpretation.'
|
||||
)
|
||||
return packet
|
||||
except Exception as exc: # pragma: no cover - defensive boundary
|
||||
return {
|
||||
@@ -575,6 +563,377 @@ def _consultation_reference_date(body: dict) -> datetime:
|
||||
return datetime.now()
|
||||
|
||||
|
||||
_FORMAL_DIVISION_SET = set(FORMAL_DIVISIONS)
|
||||
_RESEARCH_DN_DIVISIONS = tuple(n for n in range(2, 61) if n not in _FORMAL_DIVISION_SET)
|
||||
_WESTERN_TIMING_LAYER_NAMES = (
|
||||
'transits',
|
||||
'solar_return',
|
||||
'secondary_progressions',
|
||||
'solar_arc_directions',
|
||||
'converse_secondary_progressions',
|
||||
'converse_solar_arc_directions',
|
||||
'midpoints',
|
||||
'lunar_return',
|
||||
'transit_duration_scan',
|
||||
'parans',
|
||||
)
|
||||
|
||||
|
||||
def _western_default_timing(body: dict) -> dict:
|
||||
"""Attach every implemented Western timing layer to the consultation reference date.
|
||||
|
||||
Explicit `western_timing` keys still override the defaults; omitted keys are filled so a
|
||||
natal consultation no longer waits for the caller to name each technique.
|
||||
"""
|
||||
reference = _consultation_reference_date(body)
|
||||
iso = reference.date().isoformat()
|
||||
defaults = {
|
||||
'transit_date': iso,
|
||||
'solar_return_year': reference.year,
|
||||
'secondary_progression_date': iso,
|
||||
'solar_arc_date': iso,
|
||||
'converse_secondary_progression_date': iso,
|
||||
'converse_solar_arc_date': iso,
|
||||
'midpoint_date': iso,
|
||||
'lunar_return_start_date': iso,
|
||||
'duration_scan_start_date': iso,
|
||||
'duration_scan_end_date': (reference.date() + timedelta(days=13)).isoformat(),
|
||||
'parans_date': iso,
|
||||
}
|
||||
explicit = body.get('western_timing')
|
||||
if isinstance(explicit, dict):
|
||||
return {**defaults, **explicit}
|
||||
return defaults
|
||||
|
||||
|
||||
def _consultation_layer_present(value) -> bool:
|
||||
if not isinstance(value, dict) or not value:
|
||||
return False
|
||||
status = str(value.get('status') or '').lower()
|
||||
return status not in {'blocked', 'unavailable', 'missing'}
|
||||
|
||||
|
||||
def _planet_longitudes(planets: dict) -> dict[str, float]:
|
||||
longitudes = {}
|
||||
for name, data in planets.items():
|
||||
if isinstance(data, dict) and data.get('lon') is not None:
|
||||
try:
|
||||
longitudes[name] = float(data['lon'])
|
||||
except (TypeError, ValueError):
|
||||
continue
|
||||
return longitudes
|
||||
|
||||
|
||||
def _named_varga_chart(varga: dict, division: int):
|
||||
exact = f'D{division}'
|
||||
if exact in varga:
|
||||
return varga[exact]
|
||||
prefix = f'D{division}_'
|
||||
for key, value in varga.items():
|
||||
if not isinstance(key, str) or not key.startswith(prefix):
|
||||
continue
|
||||
rest = key[len(prefix):]
|
||||
if rest and not rest[0].isdigit():
|
||||
return value
|
||||
return None
|
||||
|
||||
|
||||
def _compact_varga_positions(chart: dict, *, name: str | None = None, boundary: str | None = None) -> dict:
|
||||
if not isinstance(chart, dict):
|
||||
return {}
|
||||
ascendant = chart.get('ascendant') if isinstance(chart.get('ascendant'), dict) else {}
|
||||
planets_in = chart.get('planets') if isinstance(chart.get('planets'), dict) else {}
|
||||
planets = {}
|
||||
for planet, data in planets_in.items():
|
||||
if isinstance(data, dict) and data.get('sign'):
|
||||
planets[planet] = data['sign']
|
||||
elif isinstance(data, str):
|
||||
planets[planet] = data
|
||||
payload = {
|
||||
'lagna': ascendant.get('sign') or chart.get('sign'),
|
||||
'planets': planets,
|
||||
}
|
||||
if name or chart.get('name'):
|
||||
payload['name'] = name or chart.get('name')
|
||||
if chart.get('meaning'):
|
||||
payload['meaning'] = chart['meaning']
|
||||
if boundary:
|
||||
payload['boundary'] = boundary
|
||||
return payload
|
||||
|
||||
|
||||
def _custom_research_dn_chart(calc, planet_lons: dict[str, float], asc_lon: float, division: int) -> dict:
|
||||
ascendant = calc.calc_custom_varga(asc_lon, division)
|
||||
planets = {name: calc.calc_custom_varga(lon, division) for name, lon in planet_lons.items()}
|
||||
return {
|
||||
'division': division,
|
||||
'ascendant': {'sign': ascendant.get('sign')},
|
||||
'planets': {name: {'sign': pos.get('sign')} for name, pos in planets.items()},
|
||||
}
|
||||
|
||||
|
||||
def _build_consultation_varga_spectrum(handler, planets: dict, ascendant: dict) -> tuple[dict | None, dict, list[dict]]:
|
||||
"""Compute formal D1–D60 named vargas plus remaining D-N research charts."""
|
||||
diagnostics: list[dict] = []
|
||||
planet_lons = _planet_longitudes(planets)
|
||||
try:
|
||||
asc_lon = float(ascendant.get('lon'))
|
||||
except (TypeError, ValueError):
|
||||
return None, {}, [{'layer': 'varga_spectrum', 'status': 'unavailable', 'reason': 'ascendant_longitude_missing'}]
|
||||
if not planet_lons:
|
||||
return None, {}, [{'layer': 'varga_spectrum', 'status': 'unavailable', 'reason': 'planet_longitudes_missing'}]
|
||||
|
||||
varga_full = None
|
||||
try:
|
||||
response = handler._compute_varga_full({'planets': planets, 'ascendant': ascendant})
|
||||
result = response.get('result') if isinstance(response, dict) else None
|
||||
if isinstance(result, dict):
|
||||
varga_full = result
|
||||
except Exception as exc:
|
||||
diagnostics.append({'layer': 'varga_full', 'status': 'unavailable', 'reason': exc.__class__.__name__})
|
||||
|
||||
formal: dict[str, dict] = {}
|
||||
extended: dict[str, dict] = {}
|
||||
blocked: list[str] = []
|
||||
if isinstance(varga_full, dict):
|
||||
for division in FORMAL_DIVISIONS:
|
||||
chart = _named_varga_chart(varga_full, division)
|
||||
code = f'D{division}'
|
||||
if isinstance(chart, dict):
|
||||
formal[code] = _compact_varga_positions(chart, name=chart.get('name'))
|
||||
else:
|
||||
blocked.append(code)
|
||||
for key, chart in varga_full.items():
|
||||
if not isinstance(key, str) or not isinstance(chart, dict):
|
||||
continue
|
||||
match = re.fullmatch(r'D(\d+)_.*', key)
|
||||
if not match:
|
||||
continue
|
||||
division = int(match.group(1))
|
||||
if division > 60:
|
||||
extended[f'D{division}'] = _compact_varga_positions(chart, name=chart.get('name'))
|
||||
else:
|
||||
blocked.extend(f'D{division}' for division in FORMAL_DIVISIONS)
|
||||
|
||||
research: dict[str, dict] = {}
|
||||
try:
|
||||
module = _load_local_module('divisional_charts_extended')
|
||||
calc = module.DivisionalChartsCalculator()
|
||||
for division in _RESEARCH_DN_DIVISIONS:
|
||||
code = f'D{division}'
|
||||
try:
|
||||
chart = _custom_research_dn_chart(calc, planet_lons, asc_lon, division)
|
||||
research[code] = _compact_varga_positions(
|
||||
chart,
|
||||
boundary='generic D-N; not a formal named varga',
|
||||
)
|
||||
except Exception as exc:
|
||||
blocked.append(code)
|
||||
diagnostics.append({'layer': code, 'status': 'unavailable', 'reason': exc.__class__.__name__})
|
||||
except Exception as exc:
|
||||
blocked.extend(f'D{n}' for n in _RESEARCH_DN_DIVISIONS)
|
||||
diagnostics.append({'layer': 'research_dn', 'status': 'unavailable', 'reason': exc.__class__.__name__})
|
||||
|
||||
spectrum = {
|
||||
'status': 'used' if formal or research else 'blocked',
|
||||
'mode': 'full_spectrum',
|
||||
'formal': formal,
|
||||
'research_dn': research,
|
||||
'extended': extended,
|
||||
'counts': {
|
||||
'formal': len(formal),
|
||||
'research_dn': len(research),
|
||||
'extended': len(extended),
|
||||
'blocked': len(blocked),
|
||||
},
|
||||
'blocked': blocked,
|
||||
'boundary': (
|
||||
'Twenty formal named Vargas are traditional charts. Remaining D-N rows through D60 are '
|
||||
'generic research divisions and must not be advertised as verified traditional vargas.'
|
||||
),
|
||||
}
|
||||
return varga_full, spectrum, diagnostics
|
||||
|
||||
|
||||
def _attach_gulika_observation(chart: dict, birth_payload: dict) -> None:
|
||||
modules = chart['modules']
|
||||
if isinstance(modules.get('gulika'), dict):
|
||||
return
|
||||
try:
|
||||
lat = float(birth_payload.get('lat') if birth_payload.get('lat') is not None else birth_payload['latitude'])
|
||||
lon = float(birth_payload.get('lon') if birth_payload.get('lon') is not None else birth_payload['longitude'])
|
||||
tz_raw = birth_payload.get('tz', birth_payload.get('timezone', 0))
|
||||
tz = float(tz_raw)
|
||||
moment = datetime(
|
||||
int(birth_payload['year']),
|
||||
int(birth_payload['month']),
|
||||
int(birth_payload['day']),
|
||||
int(float(birth_payload.get('hour', 0) or 0)),
|
||||
int(float(birth_payload.get('minute', 0) or 0)),
|
||||
int(float(birth_payload.get('second', 0) or 0)),
|
||||
)
|
||||
result = _load_local_module('gulika').calculate_gulika(moment, lat=lat, lon=lon, tz=tz)
|
||||
result['claim_boundary'] = 'observation_only; not a verified event claim'
|
||||
modules['gulika'] = result
|
||||
except Exception as exc:
|
||||
modules['gulika'] = {
|
||||
'status': 'blocked',
|
||||
'reason': exc.__class__.__name__,
|
||||
'claim_boundary': 'observation_only',
|
||||
}
|
||||
|
||||
|
||||
def _sign_of(point) -> str | None:
|
||||
if isinstance(point, dict) and point.get('sign'):
|
||||
return str(point['sign'])
|
||||
return None
|
||||
|
||||
|
||||
def _compact_western_spectrum(packet: dict | None) -> dict:
|
||||
if not isinstance(packet, dict) or not packet:
|
||||
return {
|
||||
'status': 'not_applicable',
|
||||
'techniques': {},
|
||||
'boundary': 'Western natal was not attached for this run.',
|
||||
}
|
||||
native = packet.get('native_chart') if isinstance(packet.get('native_chart'), dict) else {}
|
||||
natal_block = native.get('natal') if isinstance(native.get('natal'), dict) else packet.get('natal')
|
||||
natal_block = natal_block if isinstance(natal_block, dict) else {}
|
||||
planets = natal_block.get('planets') if isinstance(natal_block.get('planets'), dict) else {}
|
||||
timing = packet.get('timing_techniques') if isinstance(packet.get('timing_techniques'), dict) else {}
|
||||
techniques = {}
|
||||
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'),
|
||||
}
|
||||
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')
|
||||
calculation = packet.get('calculation') if isinstance(packet.get('calculation'), dict) else {}
|
||||
return {
|
||||
'status': natal_status if natal_status != 'partial' else 'executed',
|
||||
'zodiac': native.get('zodiac') or 'tropical',
|
||||
'house_system': native.get('house_system') or calculation.get('house_system'),
|
||||
'natal': {
|
||||
'sun': _sign_of(planets.get('sun')),
|
||||
'moon': _sign_of(planets.get('moon')),
|
||||
'ascendant': _sign_of(natal_block.get('ascendant')),
|
||||
},
|
||||
'techniques': techniques,
|
||||
'boundary': packet.get('boundary'),
|
||||
}
|
||||
|
||||
|
||||
def _audit_row_status(raw) -> str:
|
||||
value = str(raw or '').lower()
|
||||
if value in {'used', 'executed', 'complete', 'official_verified', 'pass', 'ready', 'ok', 'partial'}:
|
||||
return 'executed'
|
||||
if value in {'not_applicable', 'n/a', 'na', 'not applicable'}:
|
||||
return 'not_applicable'
|
||||
return 'blocked'
|
||||
|
||||
|
||||
def _audit_row(technique: str, status, *, system: str = 'jyotish', boundary: str | None = None) -> dict:
|
||||
mapped = _audit_row_status(status) if not str(status).lower() in {'executed', 'blocked', 'not_applicable'} else str(status).lower()
|
||||
row = {'technique': technique, 'status': mapped, 'system': system}
|
||||
if boundary:
|
||||
row['boundary'] = boundary
|
||||
return row
|
||||
|
||||
|
||||
def _consultation_technique_audit_table(
|
||||
*,
|
||||
chart: dict,
|
||||
western_evidence_packet: dict | None,
|
||||
quality_gate: dict,
|
||||
entry_mode: str,
|
||||
) -> list[dict]:
|
||||
rows: list[dict] = []
|
||||
for raw in quality_gate.get('technique_audit_table') or []:
|
||||
if not isinstance(raw, dict):
|
||||
continue
|
||||
rows.append(_audit_row(
|
||||
str(raw.get('technique') or raw.get('name') or 'Technique'),
|
||||
raw.get('status'),
|
||||
system=str(raw.get('system') or 'cross_system'),
|
||||
boundary=str(raw.get('effect_on_confidence') or raw.get('boundary') or ''),
|
||||
))
|
||||
modules = chart.get('modules') if isinstance(chart.get('modules'), dict) else {}
|
||||
spectrum = modules.get('varga_spectrum') if isinstance(modules.get('varga_spectrum'), dict) else {}
|
||||
counts = spectrum.get('counts') if isinstance(spectrum.get('counts'), dict) else {}
|
||||
rows.append(_audit_row(
|
||||
'Formal Vargas D1–D60',
|
||||
'executed' if counts.get('formal') else 'blocked',
|
||||
boundary=f"{counts.get('formal', 0)}/20 named traditional charts; remaining D-N are separate",
|
||||
))
|
||||
rows.append(_audit_row(
|
||||
'Research D-N through D60',
|
||||
'executed' if counts.get('research_dn') else 'blocked',
|
||||
boundary=f"{counts.get('research_dn', 0)} generic D-N charts; not formal named vargas",
|
||||
))
|
||||
if counts.get('extended'):
|
||||
rows.append(_audit_row(
|
||||
'Extended Vargas D81/D108/D144',
|
||||
'executed',
|
||||
boundary=f"{counts.get('extended')} super-divisional charts",
|
||||
))
|
||||
for name, label, boundary in (
|
||||
('arudha_padas', 'Arudha / UL / A10', 'observation for A10/UL; commercial claims stay non-deterministic'),
|
||||
('narayana_dasha', 'Narayana Dasha', None),
|
||||
('dasha_sub_periods', 'Vimshottari sub-periods', None),
|
||||
('ashtakavarga', 'Ashtakavarga', 'component parity remains partial'),
|
||||
('kp_cusps', 'KP cusps', 'KP remains non-deterministic in this product'),
|
||||
('gulika', 'Gulika / Mandi', 'observation_only'),
|
||||
):
|
||||
value = modules.get(name)
|
||||
status = 'executed' if _consultation_layer_present(value) else 'blocked'
|
||||
rows.append(_audit_row(label, status, boundary=boundary))
|
||||
shadbala = modules.get('shadbala') or chart.get('shadbala')
|
||||
rows.append(_audit_row(
|
||||
'Shadbala components',
|
||||
'executed' if _consultation_layer_present(shadbala) else 'blocked',
|
||||
boundary='Chesta/Sthana/Dig/Drik/Kala/Naisargika as components; absolute Virupa truth is not closed',
|
||||
))
|
||||
kakshya = modules.get('kakshya')
|
||||
rows.append(_audit_row(
|
||||
'Ashtakavarga Kakshya',
|
||||
'executed' if _consultation_layer_present(kakshya) else 'blocked',
|
||||
boundary='observation_only; not verified day/month timing',
|
||||
))
|
||||
if not any(row.get('technique') == 'Functional Benefic/Malefic' for row in rows):
|
||||
functional = modules.get('functional_benefic_malefic')
|
||||
rows.append(_audit_row(
|
||||
'Functional Benefic/Malefic',
|
||||
'executed' if _consultation_layer_present(functional) and str(functional.get('status') or '') != 'blocked' else 'blocked',
|
||||
boundary=str((functional or {}).get('effect_on_confidence') or '') if isinstance(functional, dict) else '',
|
||||
))
|
||||
western = _compact_western_spectrum(western_evidence_packet)
|
||||
rows.append(_audit_row(
|
||||
'Western natal (tropical)',
|
||||
western.get('status') or 'blocked',
|
||||
system='western',
|
||||
boundary=str(western.get('boundary') or ''),
|
||||
))
|
||||
for name, layer in (western.get('techniques') or {}).items():
|
||||
rows.append(_audit_row(
|
||||
f'Western {name}',
|
||||
layer.get('status') if isinstance(layer, dict) else 'blocked',
|
||||
system='western',
|
||||
boundary=str((layer or {}).get('boundary') or '') if isinstance(layer, dict) else '',
|
||||
))
|
||||
if entry_mode == 'prashna':
|
||||
rows.append(_audit_row('Prashna chart', 'executed', boundary='horary path'))
|
||||
else:
|
||||
rows.append(_audit_row('Prashna chart', 'not_applicable', boundary='this run is a natal consultation'))
|
||||
rows.append(_audit_row('Ashtakoota matching', 'not_applicable', boundary='requires a partner chart'))
|
||||
rows.append(_audit_row('Adhana / Niseka', 'not_applicable', boundary='research_only; not a natal consultation layer'))
|
||||
rows.append(_audit_row('Muhurta election', 'not_applicable', boundary='not an electional request'))
|
||||
return rows
|
||||
|
||||
|
||||
def _consultation_current_age(birth_payload: dict, body: dict) -> float:
|
||||
try:
|
||||
born = datetime(
|
||||
@@ -680,18 +1039,15 @@ def _attach_local_consultation_layers(handler, chart: dict, birth_payload: dict,
|
||||
ascendant = chart.get('ascendant') if isinstance(chart.get('ascendant'), dict) else {}
|
||||
diagnostics = []
|
||||
|
||||
if planets and ascendant and not isinstance(modules.get('varga_full'), dict):
|
||||
try:
|
||||
varga_response = handler._compute_varga_full({
|
||||
'planets': planets,
|
||||
'ascendant': ascendant,
|
||||
'divisions': ['D2', 'D4', 'D9', 'D10', 'D11'],
|
||||
})
|
||||
varga_result = varga_response.get('result') if isinstance(varga_response, dict) else None
|
||||
if isinstance(varga_result, dict):
|
||||
modules['varga_full'] = varga_result
|
||||
except Exception as exc: # optional local layer; preserve core D1 result
|
||||
diagnostics.append({'layer': 'varga_full', 'status': 'unavailable', 'reason': exc.__class__.__name__})
|
||||
if planets and ascendant:
|
||||
varga_full, varga_spectrum, varga_diagnostics = _build_consultation_varga_spectrum(
|
||||
handler, planets, ascendant,
|
||||
)
|
||||
diagnostics.extend(varga_diagnostics)
|
||||
if isinstance(varga_full, dict) and not isinstance(modules.get('varga_full'), dict):
|
||||
modules['varga_full'] = varga_full
|
||||
if varga_spectrum:
|
||||
modules['varga_spectrum'] = varga_spectrum
|
||||
|
||||
if planets and ascendant and not isinstance(modules.get('arudha_padas'), dict):
|
||||
try:
|
||||
@@ -765,13 +1121,59 @@ def _attach_local_consultation_layers(handler, chart: dict, birth_payload: dict,
|
||||
except Exception as exc:
|
||||
diagnostics.append({'layer': 'kp_cusps', 'status': 'unavailable', 'reason': exc.__class__.__name__})
|
||||
|
||||
_attach_gulika_observation(chart, birth_payload)
|
||||
|
||||
if planets and ascendant and not _consultation_layer_present(modules.get('functional_benefic_malefic')):
|
||||
try:
|
||||
functional = handler._functional_benefic_malefic_snapshot(planets, ascendant)
|
||||
if isinstance(functional, dict):
|
||||
modules['functional_benefic_malefic'] = functional
|
||||
except Exception as exc:
|
||||
diagnostics.append({'layer': 'functional_benefic_malefic', 'status': 'unavailable', 'reason': exc.__class__.__name__})
|
||||
|
||||
if planets and not _consultation_layer_present(modules.get('shadbala')):
|
||||
try:
|
||||
response = handler._compute_shadbala({
|
||||
**birth_payload,
|
||||
'planets': planets,
|
||||
'ascendant': ascendant,
|
||||
})
|
||||
result = response.get('result') if isinstance(response, dict) else None
|
||||
if isinstance(result, dict):
|
||||
modules['shadbala'] = result
|
||||
if not isinstance(chart.get('shadbala'), dict):
|
||||
chart['shadbala'] = result
|
||||
advanced = response.get('advanced_layer') if isinstance(response, dict) else None
|
||||
if isinstance(advanced, dict):
|
||||
modules['shadbala_components'] = advanced
|
||||
except Exception as exc:
|
||||
diagnostics.append({'layer': 'shadbala', 'status': 'unavailable', 'reason': exc.__class__.__name__})
|
||||
|
||||
if planets and not _consultation_layer_present(modules.get('kakshya')):
|
||||
try:
|
||||
response = handler._compute_kakshya({'planets': planets, 'ascendant': ascendant})
|
||||
result = response.get('result') if isinstance(response, dict) else None
|
||||
if isinstance(result, dict):
|
||||
modules['kakshya'] = {
|
||||
'status': 'observation_only',
|
||||
'result': result,
|
||||
'summary': response.get('summary') if isinstance(response, dict) else None,
|
||||
'boundary': 'Kakshya is an Ashtakavarga degree-layer observation; not verified day/month timing.',
|
||||
}
|
||||
except Exception as exc:
|
||||
diagnostics.append({'layer': 'kakshya', 'status': 'unavailable', 'reason': exc.__class__.__name__})
|
||||
|
||||
chart['local_consultation_layers'] = {
|
||||
'status': 'ready' if not diagnostics else 'partial',
|
||||
'source': 'repository_local_engines',
|
||||
'available': [
|
||||
name
|
||||
for name in ('varga_full', 'arudha_padas', 'narayana_dasha', 'dasha_sub_periods', 'ashtakavarga', 'kp_cusps')
|
||||
if isinstance(modules.get(name), dict) and modules.get(name)
|
||||
for name in (
|
||||
'varga_full', 'varga_spectrum', 'arudha_padas', 'narayana_dasha',
|
||||
'dasha_sub_periods', 'ashtakavarga', 'kp_cusps', 'gulika',
|
||||
'functional_benefic_malefic', 'shadbala', 'kakshya',
|
||||
)
|
||||
if _consultation_layer_present(modules.get(name))
|
||||
],
|
||||
'diagnostics': diagnostics,
|
||||
}
|
||||
@@ -787,16 +1189,17 @@ def _attach_local_consultation_layers(handler, chart: dict, birth_payload: dict,
|
||||
# The gate must also not be weaker than what the product tells the user it will use. The frontend
|
||||
# domain registry declares each domain's layers for its evidence preview, and every entry there that
|
||||
# names a real section is required here too; a test pins that intersection (BUG-270). The registry
|
||||
# additionally names human-facing items ('7th house/lord', 'negative holdout gate') and D11, which
|
||||
# the engine does not build at all — those are deliberately not requirements.
|
||||
# also names human-facing items ('7th house/lord', 'negative holdout gate') which cannot be
|
||||
# requirements. Formal Vargas the engine now materializes (D6/D7/D8/D11/D12/D24) are required
|
||||
# wherever the product already promises them.
|
||||
_ROUTE_REQUIRED_LAYERS = {
|
||||
'career': ('D1', 'D10', 'A10', 'dasha_boundaries', 'narayana_dasha'),
|
||||
'marriage': ('D1', 'D9', 'UL', 'A7', 'dasha_boundaries'),
|
||||
'wealth': ('D1', 'D2', 'ashtakavarga', 'dasha_boundaries'),
|
||||
'health': ('D1', 'D9', 'dasha_boundaries'),
|
||||
'education': ('D1', 'D9', 'dasha_boundaries'),
|
||||
'migration': ('D1', 'D4', 'dasha_boundaries'),
|
||||
'family': ('D1', 'D9', 'dasha_boundaries'),
|
||||
'wealth': ('D1', 'D2', 'D11', 'ashtakavarga', 'dasha_boundaries'),
|
||||
'health': ('D1', 'D6', 'D8', 'D9', 'dasha_boundaries'),
|
||||
'education': ('D1', 'D9', 'D24', 'dasha_boundaries'),
|
||||
'migration': ('D1', 'D4', 'D12', 'dasha_boundaries'),
|
||||
'family': ('D1', 'D7', 'D9', 'D12', 'dasha_boundaries'),
|
||||
'annual': ('D1', 'dasha_boundaries', 'narayana_dasha'),
|
||||
'timing': ('D1', 'dasha_boundaries', 'narayana_dasha'),
|
||||
'general': ('D1', 'D9', 'D10', 'D2', 'dasha_boundaries'),
|
||||
@@ -1254,6 +1657,18 @@ def execute_consultation_workflow(
|
||||
western_evidence_packet=western_evidence_packet,
|
||||
blind=bool(body.get('blind') or body.get('blind_technical_mode')),
|
||||
)
|
||||
consumer_context['varga_spectrum'] = (
|
||||
chart.get('modules', {}).get('varga_spectrum')
|
||||
if isinstance(chart.get('modules'), dict)
|
||||
else {}
|
||||
)
|
||||
consumer_context['western_spectrum'] = _compact_western_spectrum(western_evidence_packet)
|
||||
consumer_context['technique_audit_table'] = _consultation_technique_audit_table(
|
||||
chart=chart,
|
||||
western_evidence_packet=western_evidence_packet,
|
||||
quality_gate=runtime_evidence_log.get('quality_gate') if isinstance(runtime_evidence_log.get('quality_gate'), dict) else {},
|
||||
entry_mode=str(entry_mode or 'direct_chart'),
|
||||
)
|
||||
|
||||
result = {
|
||||
'success': True,
|
||||
|
||||
@@ -342,17 +342,19 @@ class UnifiedConsultationOrchestrator:
|
||||
if code not in selected:
|
||||
selected.append(code)
|
||||
formal = [f"D{division}" for division in FORMAL_DIVISIONS]
|
||||
research = [f"D{division}" for division in range(2, 61) if division not in FORMAL_DIVISIONS]
|
||||
return {
|
||||
"question": question or "",
|
||||
"themes": normalized_themes,
|
||||
"presentation_mode": "research" if is_research else "default",
|
||||
"appendix_expanded": is_research,
|
||||
"varga_dispatch": {
|
||||
"mode": "on_demand",
|
||||
"mode": "full_spectrum",
|
||||
"selected_theme_vargas": selected,
|
||||
"all_formal_vargas": formal,
|
||||
"deferred_vargas": [code for code in formal if code not in selected],
|
||||
"rule": "先调用与主题直接相关的分盘;其余正式分盘只在追问或冲突时展开。",
|
||||
"research_dn_vargas": research,
|
||||
"deferred_vargas": [],
|
||||
"rule": "主题相关分盘优先解读;D1–D60 正式分盘与其余 D-N 研究分盘均已计算,未计算的标 blocked,不得静默省略。",
|
||||
},
|
||||
}
|
||||
|
||||
@@ -566,6 +568,20 @@ class UnifiedConsultationOrchestrator:
|
||||
return "official_verified"
|
||||
return "official_blocked"
|
||||
|
||||
@staticmethod
|
||||
def _named_varga_chart(varga: dict[str, Any], division: int) -> Any:
|
||||
exact = f"D{division}"
|
||||
if exact in varga:
|
||||
return varga[exact]
|
||||
prefix = f"D{division}_"
|
||||
for key, value in varga.items():
|
||||
if not isinstance(key, str) or not key.startswith(prefix):
|
||||
continue
|
||||
rest = key[len(prefix):]
|
||||
if rest and not rest[0].isdigit():
|
||||
return value
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _section(value: Any, source_path: str) -> dict[str, Any]:
|
||||
present = bool(value)
|
||||
@@ -687,10 +703,10 @@ class UnifiedConsultationOrchestrator:
|
||||
base_chart.get("planets") and base_chart.get("ascendant"),
|
||||
"chart.planets+chart.ascendant",
|
||||
),
|
||||
"D9": self._section(varga.get("D9_Navamsa") or varga.get("D9"), "modules.varga_full.D9"),
|
||||
"D10": self._section(varga.get("D10_Dasamsa") or varga.get("D10"), "modules.varga_full.D10"),
|
||||
"D2": self._section(varga.get("D2_Hora") or varga.get("D2"), "modules.varga_full.D2"),
|
||||
"D4": self._section(varga.get("D4_Chaturthamsa") or varga.get("D4"), "modules.varga_full.D4"),
|
||||
"D9": self._section(self._named_varga_chart(varga, 9), "modules.varga_full.D9"),
|
||||
"D10": self._section(self._named_varga_chart(varga, 10), "modules.varga_full.D10"),
|
||||
"D2": self._section(self._named_varga_chart(varga, 2), "modules.varga_full.D2"),
|
||||
"D4": self._section(self._named_varga_chart(varga, 4), "modules.varga_full.D4"),
|
||||
"planet_degrees": self._section(base_chart.get("planets"), "chart.planets"),
|
||||
"house_degrees": self._section(base_chart.get("houses") or chart_data.get("houses"), "chart.houses"),
|
||||
"dasha_boundaries": self._section(modules.get("dasha") or chart_data.get("dasha"), "modules.dasha"),
|
||||
@@ -732,6 +748,20 @@ class UnifiedConsultationOrchestrator:
|
||||
"vedastro_gateway.archives",
|
||||
),
|
||||
}
|
||||
for division in FORMAL_DIVISIONS:
|
||||
if division == 1:
|
||||
continue
|
||||
key = f"D{division}"
|
||||
if key not in sections:
|
||||
sections[key] = self._section(
|
||||
self._named_varga_chart(varga, division),
|
||||
f"modules.varga_full.{key}",
|
||||
)
|
||||
spectrum = modules.get("varga_spectrum") if isinstance(modules.get("varga_spectrum"), dict) else {}
|
||||
sections["varga_spectrum"] = self._section(
|
||||
spectrum if spectrum.get("status") == "used" else None,
|
||||
"modules.varga_spectrum",
|
||||
)
|
||||
missing = [name for name, section in sections.items() if section.get("status") == "missing"]
|
||||
signals = chart_data.get("cross_system_signals")
|
||||
if not isinstance(signals, list):
|
||||
|
||||
Reference in New Issue
Block a user