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:
@@ -11,6 +11,9 @@ if SCRIPTS not in sys.path:
|
||||
sys.path.insert(0, SCRIPTS)
|
||||
|
||||
from jyotish_api_server import ( # noqa: E402
|
||||
_PRECISE_BIRTH_TIME_ACCURACY,
|
||||
_ROUTE_DOMAIN_CONTEXT,
|
||||
_ROUTE_REQUIRED_LAYERS,
|
||||
JyotishAPIHandler,
|
||||
_attach_local_consultation_layers,
|
||||
_build_consumer_context,
|
||||
@@ -263,6 +266,181 @@ def test_consumer_context_does_not_surface_optional_provider_as_user_limitation(
|
||||
assert context['optional_unavailable_layers'][0]['layer'] == 'vedastro_official_cross_check'
|
||||
|
||||
|
||||
def _relationship_shaped_question() -> str:
|
||||
"""A question that is unmistakably about relationships and matches no marriage keyword.
|
||||
|
||||
The old keyword list held 感情; this says 情感. That one transposition was enough to lose the
|
||||
marriage domain's boundary on a marriage-route answer.
|
||||
"""
|
||||
|
||||
return '我情感上遇到合适的人了吗现在这个阶段'
|
||||
|
||||
|
||||
def _marriage_route() -> dict:
|
||||
return UnifiedConsultationOrchestrator().resolve_route(
|
||||
_relationship_shaped_question(), ['marriage'], declared_route='marriage',
|
||||
)
|
||||
|
||||
|
||||
def _sections(**overrides: str) -> dict:
|
||||
base = {
|
||||
'D1': 'used',
|
||||
'D2': 'used',
|
||||
'D4': 'used',
|
||||
'D9': 'used',
|
||||
'D10': 'used',
|
||||
'A10': 'used',
|
||||
'UL': 'used',
|
||||
'dasha_boundaries': 'used',
|
||||
'narayana_dasha': 'used',
|
||||
'external_oracle_status': 'official_blocked',
|
||||
}
|
||||
base.update(overrides)
|
||||
return {'sections': {name: {'status': status} for name, status in base.items()}}
|
||||
|
||||
|
||||
def _confirmed_birth_time() -> dict:
|
||||
return {
|
||||
'effective_accuracy': 'minute',
|
||||
'lagna_boundary': {'is_sensitive': False},
|
||||
'summary': {'warned': ['D7'], 'disabled': ['D30', 'D60']},
|
||||
}
|
||||
|
||||
|
||||
def test_every_orchestrator_route_declares_its_own_evidence_gate() -> None:
|
||||
"""The guard that would have caught this: no route may inherit `general`'s gate by accident.
|
||||
|
||||
`route_requirements` was keyed `relationship`/`finance` while the routes are named
|
||||
`marriage`/`wealth`, so `.get(route, general)` silently downgraded both gates and nothing failed.
|
||||
"""
|
||||
|
||||
routes = set(UnifiedConsultationOrchestrator._ROUTE_DEFINITIONS)
|
||||
assert routes <= set(_ROUTE_REQUIRED_LAYERS), sorted(routes - set(_ROUTE_REQUIRED_LAYERS))
|
||||
assert set(_ROUTE_DOMAIN_CONTEXT) <= routes, sorted(set(_ROUTE_DOMAIN_CONTEXT) - routes)
|
||||
|
||||
packet = UnifiedConsultationOrchestrator().machine_evidence_packet(
|
||||
chart=_base_chart(),
|
||||
route_packet={'question_type': 'general', 'primary_theme': 'general'},
|
||||
vedastro_official={'status': 'blocked'},
|
||||
)
|
||||
buildable = set(packet['sections'])
|
||||
for route, layers in _ROUTE_REQUIRED_LAYERS.items():
|
||||
unbuildable = sorted(set(layers) - buildable)
|
||||
assert not unbuildable, f'{route} requires layers the evidence packet never builds: {unbuildable}'
|
||||
|
||||
|
||||
def test_marriage_route_requires_upapada_however_the_question_is_worded() -> None:
|
||||
context = _build_consumer_context(
|
||||
question=_relationship_shaped_question(),
|
||||
route_packet=_marriage_route(),
|
||||
chart={'success': True},
|
||||
rectification=_confirmed_birth_time(),
|
||||
machine_evidence_packet=_sections(UL='missing'),
|
||||
vedastro_official={'status': 'blocked'},
|
||||
)
|
||||
|
||||
assert context['route'] == 'marriage'
|
||||
assert context['missing_route_layers'] == ['UL']
|
||||
assert context['core_status'] == 'degraded'
|
||||
|
||||
|
||||
def test_wealth_route_requires_the_hora_chart() -> None:
|
||||
context = _build_consumer_context(
|
||||
question='接下来的收入结构会怎么变',
|
||||
route_packet={'question_type': 'wealth', 'primary_theme': 'wealth'},
|
||||
chart={'success': True},
|
||||
rectification=_confirmed_birth_time(),
|
||||
machine_evidence_packet=_sections(D2='missing'),
|
||||
vedastro_official={'status': 'blocked'},
|
||||
)
|
||||
|
||||
assert context['missing_route_layers'] == ['D2']
|
||||
assert context['core_status'] == 'degraded'
|
||||
|
||||
|
||||
def test_domain_boundary_follows_the_route_not_the_wording() -> None:
|
||||
context = _build_consumer_context(
|
||||
question=_relationship_shaped_question(),
|
||||
route_packet=_marriage_route(),
|
||||
chart={'success': True},
|
||||
rectification=_confirmed_birth_time(),
|
||||
machine_evidence_packet=_sections(),
|
||||
vedastro_official={'status': 'blocked'},
|
||||
)
|
||||
|
||||
assert 'gender_interpretation_boundary' in context['domain_boundaries']
|
||||
assert 'gender interpretation boundary' in context['domain_context_layers']
|
||||
# A marriage answer must not pick up unrelated domains' boundaries.
|
||||
assert 'health_non_medical_boundary' not in context['domain_boundaries']
|
||||
|
||||
|
||||
def test_birth_time_boundary_reads_rectification_state_instead_of_the_question() -> None:
|
||||
uncertain = _build_consumer_context(
|
||||
question='我的事业接下来怎么走',
|
||||
route_packet={'question_type': 'career', 'primary_theme': 'career'},
|
||||
chart={'success': True},
|
||||
rectification={'effective_accuracy': '1hour', 'lagna_boundary': {'is_sensitive': False}},
|
||||
machine_evidence_packet=_sections(),
|
||||
vedastro_official={'status': 'blocked'},
|
||||
)
|
||||
confirmed = _build_consumer_context(
|
||||
question='我的事业接下来怎么走',
|
||||
route_packet={'question_type': 'career', 'primary_theme': 'career'},
|
||||
chart={'success': True},
|
||||
rectification=_confirmed_birth_time(),
|
||||
machine_evidence_packet=_sections(),
|
||||
vedastro_official={'status': 'blocked'},
|
||||
)
|
||||
|
||||
assert 'birth_time_uncertainty_boundary' in uncertain['domain_boundaries']
|
||||
assert 'birth_time_uncertainty_boundary' not in confirmed['domain_boundaries']
|
||||
assert 'minute' in _PRECISE_BIRTH_TIME_ACCURACY
|
||||
|
||||
|
||||
def test_unknown_birth_time_precision_counts_as_uncertain() -> None:
|
||||
context = _build_consumer_context(
|
||||
question='我的事业接下来怎么走',
|
||||
route_packet={'question_type': 'career', 'primary_theme': 'career'},
|
||||
chart={'success': True},
|
||||
rectification={},
|
||||
machine_evidence_packet=_sections(),
|
||||
vedastro_official={'status': 'blocked'},
|
||||
)
|
||||
|
||||
assert 'birth_time_uncertainty_boundary' in context['domain_boundaries']
|
||||
|
||||
|
||||
def test_sensitive_lagna_keeps_the_boundary_at_minute_accuracy() -> None:
|
||||
context = _build_consumer_context(
|
||||
question='我的事业接下来怎么走',
|
||||
route_packet={'question_type': 'career', 'primary_theme': 'career'},
|
||||
chart={'success': True},
|
||||
rectification={'effective_accuracy': 'minute', 'lagna_boundary': {'is_sensitive': True}},
|
||||
machine_evidence_packet=_sections(),
|
||||
vedastro_official={'status': 'blocked'},
|
||||
)
|
||||
|
||||
assert 'birth_time_uncertainty_boundary' in context['domain_boundaries']
|
||||
|
||||
|
||||
def test_precise_timing_needs_narayana_dasha_on_a_route_that_does_not_require_it() -> None:
|
||||
"""`timing_layers_ready` used to read `missing_route_layers`, which never lists a layer the route
|
||||
does not require. Precise timing was therefore granted on marriage/wealth without the layer."""
|
||||
|
||||
context = _build_consumer_context(
|
||||
question=_relationship_shaped_question(),
|
||||
route_packet=_marriage_route(),
|
||||
chart={'success': True},
|
||||
rectification=_confirmed_birth_time(),
|
||||
machine_evidence_packet=_sections(narayana_dasha='missing'),
|
||||
vedastro_official={'status': 'blocked'},
|
||||
)
|
||||
|
||||
assert context['missing_route_layers'] == []
|
||||
assert context['core_status'] == 'ready'
|
||||
assert context['answer_policy']['can_answer_precise_timing'] is False
|
||||
|
||||
|
||||
def test_failed_vedastro_raw_packet_is_not_marked_as_used() -> None:
|
||||
packet = UnifiedConsultationOrchestrator().machine_evidence_packet(
|
||||
chart=_base_chart(),
|
||||
|
||||
Reference in New Issue
Block a user