fix(consult): check the evidence gate against the route the answer is on
七条路由的证据门都不是自己的:route_requirements 的键写成 relationship/finance,而 路由名是 marriage/wealth,另有 5 条路由压根没有条目,全部静默落到 general 的门。 missingLayers: [] 因此不表示证据齐备,只表示没检查过——婚姻的 UL 与财富的 D2 从未 进入检查。 同一函数另有两处判据也没接到权威来源。7 块正则用问题文本重猜领域,而领域早已由模型 声明并写进 route_packet,一句写作「情感」而非表里「感情」的提问在 marriage 路由上完全 拿不到性别解读边界。timing_layers_ready 读的是 missing_route_layers,该列表只装本路由 要求的层,于是对任何不要求 narayana_dasha 的路由恒为真,精确应期在该层根本没算出来时 也照样放行。三处的失败方向都是静默放宽,因此没有任何人报错。 三处都接回权威来源:10 条路由逐条显式列出必需层(层名限定为证据包真实构建的 section, 所以 wealth 不要求引擎不产出的 D11)、领域边界按 route 查表、出生时间边界从矫正闸门的 effective_accuracy 与 Lagna 敏感度派生、就绪判断直接读 section 状态。唯一保留文本探测 的是「用户有没有要一个具体日期」——服务端对此没有权威来源,改为 timing/annual 路由结构 性携带、文本仅作叠加,一次措辞漏判不再能把信号清零。 另外把 skillReferenceReadCount 暴露为回执的 skill.referenceReads(必填)与可观测日志的 skillReferenceReads。它此前数完即丢,而 skill_read 按设计不记成 runtime step,因此「模型 有没有真的翻开方法文档」在运行结束后无处可查。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -691,6 +691,55 @@ def _attach_local_consultation_layers(handler, chart: dict, birth_payload: dict,
|
||||
return chart
|
||||
|
||||
|
||||
# Keyed by the route names in UnifiedConsultationOrchestrator._ROUTE_DEFINITIONS. Every layer named
|
||||
# here must be a section the evidence packet actually builds, otherwise the route could never reach
|
||||
# `ready`. A route that falls through to `general`'s gate is checked against a weaker evidence set
|
||||
# without anything failing, which is how `marriage` went unchecked for UL and `wealth` for D2; the
|
||||
# table therefore covers every defined route and a test pins that coverage (BUG-267).
|
||||
_ROUTE_REQUIRED_LAYERS = {
|
||||
'career': ('D1', 'D10', 'A10', 'dasha_boundaries', 'narayana_dasha'),
|
||||
'marriage': ('D1', 'D9', 'UL', 'dasha_boundaries'),
|
||||
'wealth': ('D1', 'D2', 'dasha_boundaries'),
|
||||
'health': ('D1', 'D9', 'dasha_boundaries'),
|
||||
'education': ('D1', 'D9', 'dasha_boundaries'),
|
||||
'migration': ('D1', 'D4', 'dasha_boundaries'),
|
||||
'family': ('D1', 'D9', 'dasha_boundaries'),
|
||||
'annual': ('D1', 'dasha_boundaries', 'narayana_dasha'),
|
||||
'timing': ('D1', 'dasha_boundaries', 'narayana_dasha'),
|
||||
'general': ('D1', 'D9', 'dasha_boundaries'),
|
||||
}
|
||||
|
||||
# The context layers and interpretation boundaries a domain carries. These follow the resolved route
|
||||
# because the route is what the answer is about. Deriving them from question wording missed every
|
||||
# phrasing outside the keyword list, so a plainly relationship-shaped question could be answered on
|
||||
# the marriage route with no gender-interpretation boundary attached.
|
||||
_ROUTE_DOMAIN_CONTEXT = {
|
||||
'marriage': {
|
||||
'layers': ('gender interpretation boundary',),
|
||||
'boundary_key': 'gender_interpretation_boundary',
|
||||
'boundary': 'Gender can affect interpretation wording and spouse-role overlays, but it must not change astronomical calculation facts.',
|
||||
},
|
||||
'health': {
|
||||
'layers': ('D6', 'D8', '6th/8th/12th house', 'health non_medical boundary'),
|
||||
'boundary_key': 'health_non_medical_boundary',
|
||||
'boundary': 'Health reading is non_medical guidance only; no diagnosis, guaranteed disease event, or treatment instruction.',
|
||||
},
|
||||
'migration': {'layers': ('12th house', 'twelfth house', 'house_12')},
|
||||
'family': {'layers': ('4th/5th/9th house',)},
|
||||
'education': {'layers': ('5th/9th house',)},
|
||||
'annual': {
|
||||
'layers': ('annual forecast boundary',),
|
||||
'boundary_key': 'annual_forecast_boundary',
|
||||
'boundary': 'Annual forecast is a bounded trend reading; no full-year certainty or exact day/month guarantee.',
|
||||
},
|
||||
}
|
||||
|
||||
# Birth-time precision tiers that need no uncertainty boundary. Anything else counts as uncertain,
|
||||
# including an absent tier and the '5min' value `get_effective_accuracy` can return but
|
||||
# `ACCURACY_MATRIX` has no row for.
|
||||
_PRECISE_BIRTH_TIME_ACCURACY = ('minute', 'rectified')
|
||||
|
||||
|
||||
def _build_consumer_context(
|
||||
*,
|
||||
question: str,
|
||||
@@ -701,21 +750,13 @@ def _build_consumer_context(
|
||||
vedastro_official: dict,
|
||||
) -> dict:
|
||||
"""Build a chat-facing truth contract without exposing provider noise as a fatal error."""
|
||||
question_text = (question or '').lower()
|
||||
sections = (
|
||||
machine_evidence_packet.get('sections')
|
||||
if isinstance(machine_evidence_packet.get('sections'), dict)
|
||||
else {}
|
||||
)
|
||||
route = str(route_packet.get('question_type') or route_packet.get('primary_theme') or 'general')
|
||||
route_requirements = {
|
||||
'career': ['D1', 'D10', 'A10', 'dasha_boundaries', 'narayana_dasha'],
|
||||
'relationship': ['D1', 'D9', 'UL', 'dasha_boundaries'],
|
||||
'finance': ['D1', 'D2', 'dasha_boundaries'],
|
||||
'timing': ['D1', 'dasha_boundaries', 'narayana_dasha'],
|
||||
'general': ['D1', 'D9', 'dasha_boundaries'],
|
||||
}
|
||||
required = route_requirements.get(route, route_requirements['general'])
|
||||
required = _ROUTE_REQUIRED_LAYERS.get(route, _ROUTE_REQUIRED_LAYERS['general'])
|
||||
available_layers = sorted(
|
||||
name for name, section in sections.items()
|
||||
if isinstance(section, dict) and section.get('status') == 'used'
|
||||
@@ -734,36 +775,27 @@ def _build_consumer_context(
|
||||
if boundary_key and boundary:
|
||||
domain_boundaries[boundary_key] = boundary
|
||||
|
||||
if re.search(r'(婚|结婚|感情|关系|配偶|伴侣|spouse|marriage|relationship)', question_text):
|
||||
domain_context = _ROUTE_DOMAIN_CONTEXT.get(route)
|
||||
if domain_context:
|
||||
add_domain_layer(
|
||||
'gender interpretation boundary',
|
||||
boundary_key='gender_interpretation_boundary',
|
||||
boundary='Gender can affect interpretation wording and spouse-role overlays, but it must not change astronomical calculation facts.',
|
||||
*domain_context['layers'],
|
||||
boundary_key=domain_context.get('boundary_key'),
|
||||
boundary=domain_context.get('boundary'),
|
||||
)
|
||||
if re.search(r'(健康|疾病|身体|病|医疗|health)', question_text):
|
||||
add_domain_layer(
|
||||
'D6', 'D8', '6th/8th/12th house', 'health non_medical boundary',
|
||||
boundary_key='health_non_medical_boundary',
|
||||
boundary='Health reading is non_medical guidance only; no diagnosis, guaranteed disease event, or treatment instruction.',
|
||||
)
|
||||
if re.search(r'(出国|移民|迁移|异地|海外|foreign|migration|relocation)', question_text):
|
||||
add_domain_layer('12th house', 'twelfth house', 'house_12')
|
||||
if re.search(r'(家庭|房产|子女|父母|家|children|family|property)', question_text):
|
||||
add_domain_layer('4th/5th/9th house')
|
||||
if re.search(r'(教育|学习|读书|考试|考证|education|study|exam)', question_text):
|
||||
add_domain_layer('5th/9th house')
|
||||
if re.search(r'(出生时间|时间不准|校正|矫正|rectification|birth time)', question_text):
|
||||
|
||||
# Birth-time uncertainty is something the server already knows from the rectification gate, so
|
||||
# read that state instead of waiting for the user to phrase it. An absent tier counts as
|
||||
# uncertain: not knowing the precision is not the same as having confirmed it.
|
||||
lagna_boundary = rectification.get('lagna_boundary') if isinstance(rectification.get('lagna_boundary'), dict) else {}
|
||||
if (
|
||||
str(rectification.get('effective_accuracy') or '') not in _PRECISE_BIRTH_TIME_ACCURACY
|
||||
or bool(lagna_boundary.get('is_sensitive'))
|
||||
):
|
||||
add_domain_layer(
|
||||
'birth_time uncertain boundary', 'not_auto_rectified',
|
||||
boundary_key='birth_time_uncertainty_boundary',
|
||||
boundary='Birth-time uncertainty requires candidate windows and event evidence; the workflow is not_auto_rectified.',
|
||||
)
|
||||
if re.search(r'(年度|一年|未来一年|年运|annual|year)', question_text):
|
||||
add_domain_layer(
|
||||
'annual forecast boundary',
|
||||
boundary_key='annual_forecast_boundary',
|
||||
boundary='Annual forecast is a bounded trend reading; no full-year certainty or exact day/month guarantee.',
|
||||
)
|
||||
d1_ready = 'D1' in available_layers and bool(chart.get('success', True))
|
||||
hard_blockers = [] if d1_ready else ['core_chart_unavailable']
|
||||
|
||||
@@ -784,8 +816,19 @@ def _build_consumer_context(
|
||||
warned_vargas = rect_summary.get('warned') if isinstance(rect_summary.get('warned'), list) else []
|
||||
disabled_vargas = rect_summary.get('disabled') if isinstance(rect_summary.get('disabled'), list) else []
|
||||
relevant_warned_vargas = [name for name in warned_vargas if name in required]
|
||||
precise_timing_requested = route == 'timing' or bool(re.search(r'(具体|精确|哪一|几月|月份|日期|何时|什么时候|时间点|年份)', question or ''))
|
||||
timing_layers_ready = all(name not in missing_route_layers for name in ('dasha_boundaries', 'narayana_dasha'))
|
||||
# Whether precision was asked for is the one signal with no server-side source: it is about the
|
||||
# request, not about our own data. The timing-family routes carry it structurally; the wording
|
||||
# probe stays only as an additive hint, so a phrasing it misses can no longer zero the signal.
|
||||
precise_timing_requested = route in ('timing', 'annual') or bool(
|
||||
re.search(r'(具体|精确|哪一|几月|月份|日期|何时|什么时候|时间点|年份)', question or '')
|
||||
)
|
||||
# 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.
|
||||
timing_layers_ready = all(
|
||||
isinstance(sections.get(name), dict) and sections[name].get('status') == 'used'
|
||||
for name in ('dasha_boundaries', '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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user