fix(consult): check the evidence gate against the route the answer is on
Independent Staging Quality Gate / validate (push) Successful in 7m41s
Independent Staging Quality Gate / publish (push) Successful in 9m39s

七条路由的证据门都不是自己的: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:
Jesse_Chen
2026-08-18 11:50:28 +08:00
parent 5687182980
commit 10ae149c58
9 changed files with 347 additions and 46 deletions
+77 -34
View File
@@ -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