37, 'diagnosis_date' => '2026-10-26', 'chief_complaint' => '未记录', 'tongue' => '未记录', 'pulse' => '脉细', 'allergy_history' => '不详', 'current_medications' => '二甲双胍500mg,每日2次。']; $source = AiRequiredSource::submission(['diagnoses' => [$current, ['id' => 12, 'diagnosis_date' => '2020-02-01', 'tongue' => '舌红', 'pulse' => '脉弦', 'current_medications' => '格列美脲已停用。']]]); $frozen = serialize([$current, $source]); $guard = TcmEvidenceGuard::assess($current, $source); guardR37($guard['missing'] === ['舌象', '本次症状病史'], 'Independent gaps are preserved; pulse never becomes a gap.'); guardR37($guard['restricted'] && $guard['analysis_allowed'], 'Completeness marker does not disable selected-task analysis.'); guardR37($guard['pulse_assessment'] === 'excluded_by_workflow', 'Current pulse stays excluded from diagnosis.'); guardR37(str_ends_with($guard['version'], '-r37'), 'Changed evidence contract has a new version.'); guardR37(serialize([$current, $source]) === $frozen, 'Raw dates, polarity, historical records and current source are immutable.'); $limits = TcmEvidenceGuard::limitationsPrompt($guard); guardR37(str_contains($limits, '继续完成指定任务') && str_contains($limits, '只限制依赖这些证据的判断'), 'Missing evidence qualifies specific judgments while useful analysis continues.'); guardR37(str_contains($limits, '未知不写成正常或不存在') && str_contains($limits, '历史记录保留日期'), 'Unknown state and historical identity remain explicit.'); guardR37(str_contains($limits, '关键方证依据不足时不新增具体方药或剂量'), 'New prescription safety remains explicit.'); guardR37(!str_contains($limits, '只返回一个JSON对象') && !str_contains($limits, 'fact_ids'), 'No source-selection output protocol remains.'); $full = json_encode($source, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); $question = '请分析现有用药风险 13800138000'; $query = TcmEvidenceGuard::prompt($full, $guard, $question); guardR37(str_contains($query, '请分析现有用药风险') && str_contains($query, '</QUESTION_DATA>') && !str_contains($query, '13800138000'), 'Question is preserved with data-boundary escaping and identity redaction.'); guardR37(str_contains($query, '二甲双胍500mg') && str_contains($query, '格列美脲已停用') && str_contains($query, '2020-02-01'), 'Full clinical source and dates remain in the analysis prompt.'); $answer = "当前用药\n病历记录“二甲双胍500mg,每日2次。”可供核对。\n\n局限\n实际服用情况及肾功能资料需进一步核实,不能据已有药名认定适用性;由执业医师结合检查复核。"; foreach ([false, true] as $structured) { $review = TcmEvidenceGuard::review($guard, $answer, $structured); guardR37($review['valid'] && $review['answer'] === $answer, 'Accepted model bytes are retained exactly; mode=' . (int) $structured); guardR37($review['answer_mode'] === 'clinical_analysis' && $review['selected_fact_count'] === 0 && $review['selection_valid'] === null && !$review['format_normalized'], 'No local fact-selection success is claimed; mode=' . (int) $structured); } foreach (['', '{"fact_ids":["f1"]}', '模型拒绝后只返回{"fact_ids":[]}。', '本次舌红,苔黄腻。', '本次无过敏史。', '请补采脉象。', '脉细提示血虚。'] as $invalid) { $review = TcmEvidenceGuard::review($guard, $invalid); guardR37(!$review['valid'] && $review['answer'] === '' && $review['issues'] !== [], 'Invalid/obsolete/unsafe reply is honestly rejected without any local fallback: ' . $invalid); guardR37(!str_contains($review['answer'], '四诊资料核对') && !str_contains($review['answer'], '八纲'), 'No canned report is synthesized for rejected input: ' . $invalid); } $withImages = TcmEvidenceGuard::assess($current + ['tongue_images' => ['/offline/tongue.png']]); $imageLimits = TcmEvidenceGuard::limitationsPrompt($withImages); guardR37($withImages['current_tongue_images']['reference_count'] === 1 && !$withImages['current_tongue_images']['content_read'] && !$withImages['current_tongue_images']['tongue_appearance_verified'], 'Image references never certify access or visual conclusions.'); guardR37(str_contains($imageLimits, '请直接观察图片') && str_contains($imageLimits, '不因此把全部望诊判为未知'), 'Attached tongue images are observed directly, never dismissed as unread.'); guardR37(TcmEvidenceGuard::assess(['chief_complaint' => '口干。', 'tongue' => '舌淡红,苔薄白。'])['missing'] === [], 'Recorded tongue and symptoms remain complete without pulse.'); guardR37(serialize([$current, $source]) === $frozen, 'Prompt/review never changes source facts after processing.'); $failed = array_values(array_filter($checks, static fn (array $check): bool => !$check['passed'])); echo json_encode(['suite' => 'TcmEvidenceGuardTaskAnalysisR37Test', 'checks' => count($checks), 'passed' => count($checks) - count($failed), 'failed' => count($failed), 'failures' => $failed], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) . "\n"; exit($failed === [] ? 0 : 1);