fix(consult): compute the antardasha boundaries answers need, and stop billing an apology for an unwritten answer
The model packet read chart.modules.dasha_boundaries, a key the engine never wrote, so no answer ever had sub-period boundaries while the receipt still reported precise timing as allowed. The server now cuts the running mahadasha into antardashas out of the periods the packet already shows, exposes them as their own evidence section, and precise timing requires that section. A run whose calculation succeeded and whose model then wrote nothing was answered with a fixed apology and billed as completed. It now asks once more against the cached calculation, and fails with empty_answer—no charge—if that attempt is silent too. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -590,6 +590,79 @@ def _consultation_current_age(birth_payload: dict, body: dict) -> float:
|
||||
return max(0.0, (reference.replace(tzinfo=None) - born).total_seconds() / (365.2425 * 86400))
|
||||
|
||||
|
||||
def _consultation_period_datetime(value) -> datetime | None:
|
||||
if isinstance(value, datetime):
|
||||
return value.replace(tzinfo=None)
|
||||
if isinstance(value, str) and value.strip():
|
||||
try:
|
||||
return datetime.fromisoformat(value.strip().replace('Z', '+00:00')).replace(tzinfo=None)
|
||||
except ValueError:
|
||||
return None
|
||||
return None
|
||||
|
||||
|
||||
def _consultation_dasha_sub_periods(chart: dict, body: dict) -> dict | None:
|
||||
"""Cut the running mahadasha into its antardashas.
|
||||
|
||||
A mahadasha is six to twenty years wide, so mahadasha boundaries alone cannot place an answer in
|
||||
a month or a year; the packet has to carry the sub-period boundaries whose presence it claims
|
||||
precise timing from. They are cut out of the periods the packet already shows rather than from a
|
||||
second timeline built from the moon's longitude: both would come from the same engine, but only
|
||||
this way are the sub-period boundaries guaranteed to sit inside the mahadasha boundaries the
|
||||
model reads, instead of handing it two nearly-identical sets of dasha dates to choose between.
|
||||
"""
|
||||
|
||||
dasha = chart.get('dasha') if isinstance(chart.get('dasha'), dict) else {}
|
||||
periods = dasha.get('periods') if isinstance(dasha.get('periods'), list) else []
|
||||
parsed = []
|
||||
for period in periods:
|
||||
if not isinstance(period, dict) or not period.get('lord'):
|
||||
continue
|
||||
start = _consultation_period_datetime(period.get('start'))
|
||||
end = _consultation_period_datetime(period.get('end'))
|
||||
if start is None or end is None or end <= start:
|
||||
continue
|
||||
parsed.append({'lord': str(period['lord']), 'start': start, 'end': end})
|
||||
|
||||
reference = _consultation_reference_date(body).replace(tzinfo=None)
|
||||
current = next((period for period in parsed if period['start'] <= reference < period['end']), None)
|
||||
if current is None:
|
||||
return None
|
||||
|
||||
analyzer = _load_local_module('dasha_analyzer')
|
||||
sub_periods = analyzer.build_antardasha(current)
|
||||
if not sub_periods:
|
||||
return None
|
||||
current_sub = analyzer.find_current_sub(sub_periods, reference)
|
||||
following = next(
|
||||
(sub_periods[index + 1] for index, period in enumerate(sub_periods)
|
||||
if period is current_sub and index + 1 < len(sub_periods)),
|
||||
None,
|
||||
)
|
||||
|
||||
def boundary(period: dict) -> dict:
|
||||
return {
|
||||
'lord': period['lord'],
|
||||
'start': period['start'].strftime('%Y-%m-%d'),
|
||||
'end': period['end'].strftime('%Y-%m-%d'),
|
||||
}
|
||||
|
||||
return {
|
||||
'status': 'ready',
|
||||
'source': 'chart.dasha.periods + dasha_analyzer.build_antardasha',
|
||||
'method': 'vimshottari_antardasha_proportional',
|
||||
'current': {'mahadasha': boundary(current), 'antardasha': boundary(current_sub)},
|
||||
**({'next': boundary(following)} if following else {}),
|
||||
'boundaries': [boundary(period) for period in sub_periods],
|
||||
'boundary_count': len(sub_periods),
|
||||
'summary': (
|
||||
f"当前 {current['lord']} 大运下的小运为 {current_sub['lord']},"
|
||||
f"边界 {boundary(current_sub)['start']} 至 {boundary(current_sub)['end']}。"
|
||||
'更细的 Pratyantardasha 与行运触发不在本层计算范围内。'
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def _attach_local_consultation_layers(handler, chart: dict, birth_payload: dict, body: dict) -> dict:
|
||||
"""Attach locally-computable consultation layers before reports/evidence are assembled.
|
||||
|
||||
@@ -666,6 +739,20 @@ def _attach_local_consultation_layers(handler, chart: dict, birth_payload: dict,
|
||||
except Exception as exc:
|
||||
diagnostics.append({'layer': 'ashtakavarga', 'status': 'unavailable', 'reason': exc.__class__.__name__})
|
||||
|
||||
if not isinstance(modules.get('dasha_sub_periods'), dict):
|
||||
try:
|
||||
sub_periods = _consultation_dasha_sub_periods(chart, body)
|
||||
if isinstance(sub_periods, dict):
|
||||
modules['dasha_sub_periods'] = sub_periods
|
||||
else:
|
||||
diagnostics.append({
|
||||
'layer': 'dasha_sub_periods',
|
||||
'status': 'unavailable',
|
||||
'reason': 'no_mahadasha_period_covers_reference_date',
|
||||
})
|
||||
except Exception as exc:
|
||||
diagnostics.append({'layer': 'dasha_sub_periods', 'status': 'unavailable', 'reason': exc.__class__.__name__})
|
||||
|
||||
if planets and ascendant and not isinstance(modules.get('kp_cusps'), dict):
|
||||
try:
|
||||
normalized, _, asc_sign_idx = handler._normalized_planets_from_body({
|
||||
@@ -683,7 +770,7 @@ def _attach_local_consultation_layers(handler, chart: dict, birth_payload: dict,
|
||||
'source': 'repository_local_engines',
|
||||
'available': [
|
||||
name
|
||||
for name in ('varga_full', 'arudha_padas', 'narayana_dasha', 'ashtakavarga', 'kp_cusps')
|
||||
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)
|
||||
],
|
||||
'diagnostics': diagnostics,
|
||||
@@ -831,9 +918,14 @@ def _build_consumer_context(
|
||||
# Read the sections directly. Gating on `missing_route_layers` made this vacuously true for every
|
||||
# route that does not require narayana_dasha, so precise timing was granted without that layer
|
||||
# ever being checked.
|
||||
#
|
||||
# `dasha_boundaries` is the mahadasha list, and a six-to-twenty-year period cannot place an
|
||||
# answer in a month, so it alone never justified precise timing: the antardasha boundaries have
|
||||
# to be there too. The gate has to name them itself, because a section that is missing is a
|
||||
# section no route requirement can speak for (BUG-279).
|
||||
timing_layers_ready = all(
|
||||
isinstance(sections.get(name), dict) and sections[name].get('status') == 'used'
|
||||
for name in ('dasha_boundaries', 'narayana_dasha')
|
||||
for name in ('dasha_boundaries', 'dasha_sub_periods', 'narayana_dasha')
|
||||
)
|
||||
precision_allows_timing = not any(name in disabled_vargas for name in ('D9', 'D10'))
|
||||
can_answer_precise_timing = d1_ready and timing_layers_ready and precision_allows_timing and not missing_route_layers
|
||||
|
||||
@@ -694,6 +694,10 @@ class UnifiedConsultationOrchestrator:
|
||||
"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"),
|
||||
# The mahadasha list above and the antardasha cut below are different claims: one says
|
||||
# which decade, the other which months. They are separate sections so that an answer
|
||||
# policy can require the second without the first standing in for it.
|
||||
"dasha_sub_periods": self._section(modules.get("dasha_sub_periods"), "modules.dasha_sub_periods"),
|
||||
"narayana_dasha": self._section(modules.get("narayana_dasha"), "modules.narayana_dasha"),
|
||||
"shadbala": self._section(modules.get("shadbala") or chart_data.get("shadbala"), "modules.shadbala"),
|
||||
"ashtakavarga": self._section(modules.get("ashtakavarga") or chart_data.get("ashtakavarga"), "modules.ashtakavarga"),
|
||||
|
||||
Reference in New Issue
Block a user