fix(consult): run the local skill's full technique spectrum on the web path
Independent Staging Quality Gate / validate (push) Successful in 10m59s
Independent Staging Quality Gate / publish (push) Successful in 9m7s

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:
Jesse_Chen
2026-08-18 23:30:36 +08:00
co-authored by Cursor
parent d04fc30b63
commit 2bf7472645
29 changed files with 1176 additions and 191 deletions
+11 -10
View File
@@ -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
View File
@@ -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,
+37 -7
View File
@@ -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):