push($chunk)) break; $expect($stream->finish() === $valid && $stream->rejected() === !$valid, 'Stream verdict at chunk ' . $size . ': ' . $label); $expect($visible === ($valid ? $wire : $safePrefix), 'Original accepted bytes or only the safe heading may be visible at chunk ' . $size . ': ' . $label); $expect($stream->acceptedContent() === $visible, 'Accepted and emitted bytes must match: ' . $label); } }; $unknown = Guard::build(['present_illness' => '胸痛、心悸、固定刺痛未记录。']); $known = Guard::build([ 'present_illness' => '口干;无心悸;无明显胸痛;未记录固定刺痛。', 'sleep_condition' => '易醒。', 'allergy_history' => '本次询问无已知药物过敏史。', 'tongue' => '舌质淡红,苔薄白。', 'pulse' => '脉细。', ]); // A matching pair may wrap one unknown field value, including after a heading. foreach ([['(', ')'], ['(', ')']] as [$open, $close]) { foreach (['未记录', '不详', '待补充', '尚未采集'] as $state) { foreach (['睡眠', '过敏史', '舌象', '脉象'] as $field) { $value = $open . $state . $close; $check($unknown, $field . ':' . $value . '。', true); $check($unknown, $field . ":\n\n" . $value . '。', true); $check($known, $field . ':' . $value . '。', false, 'RECORDED_TO_UNKNOWN'); $check($known, $field . ":\n\n" . $value . '。', false, 'RECORDED_TO_UNKNOWN', $field . ":\n\n"); } } } $check($unknown, '睡眠:( 未记录 )。', true); $check($unknown, '睡眠:(不详)。', false, 'UNKNOWN_TO_POSITIVE'); $check($unknown, '睡眠:(无异常)。', false); $check($unknown, '睡眠:(未记录);患者无固定刺痛。', false, 'UNKNOWN_TO_NEGATIVE'); $check($unknown, '睡眠:(未记录,患者有固定刺痛)。', false, 'UNKNOWN_TO_POSITIVE'); $check($known, '睡眠:(未记录)但舌质红绛。', false, 'TONGUE_FEATURE_NOT_IN_CURRENT_SOURCE'); // The collection action scopes its whether-object even when it names the patient. foreach ([ '需核实患者是否存在心悸。', '补问患者是否有胸痛?', '需询问患者有无胸痛。', '请询问患者有无胸痛。', '仍需进一步询问患者有无胸痛、心悸。', '请再次复核本例是否存在固定刺痛。', '应核实:患者是否存在舌质红绛、脉滑。', ] as $text) { $check($unknown, $text, true); $check($known, $text, true); } // True claims, record gaps and qualifiers remain checked after the bounded object. $assertions = [ ['患者有心悸', 'NEGATIVE_TO_POSITIVE'], ['患者有固定刺痛', 'UNKNOWN_TO_POSITIVE'], ['患者无固定刺痛', 'UNKNOWN_TO_NEGATIVE'], ['患者无胸痛', 'NEGATION_SCOPE_WIDENED'], ['心悸未记录', 'NEGATIVE_TO_UNKNOWN'], ['口干未记录', 'POSITIVE_TO_UNKNOWN'], ['舌质红绛', 'TONGUE_FEATURE_NOT_IN_CURRENT_SOURCE'], ['脉滑', 'PULSE_FEATURE_NOT_IN_CURRENT_SOURCE'], ]; foreach ([',', '且', '并且', '但', '同时', '还', '又', '另'] as $joiner) { foreach ($assertions as [$assertion, $code]) { $check($known, '需核实患者是否存在感染' . $joiner . $assertion . '。', false, $code); } } foreach ([ ['需核实患者有心悸。', 'NEGATIVE_TO_POSITIVE'], ['已核实患者是否存在心悸。', 'NEGATIVE_TO_POSITIVE'], ['需核实患者是否存在患者有固定刺痛。', 'UNKNOWN_TO_POSITIVE'], ['需核实患者是否心悸未记录。', 'NEGATIVE_TO_UNKNOWN'], ['需核实患者是否存在已记录的固定刺痛。', 'UNKNOWN_TO_POSITIVE'], ['需核实患者是否存在感染,需关注患者有固定刺痛。', 'UNKNOWN_TO_POSITIVE'], ] as [$text, $code]) $check($known, $text, false, $code); // Do not relax the independent negative-qualifier or concatenated-topic policy. $check($known, '无胸痛。', false, 'NEGATION_SCOPE_WIDENED'); $check($known, '无明显胸痛。', true); $compound = Guard::build(['present_illness' => '无胸痛心悸。']); $check($compound, '无胸痛、心悸。', false, 'NEGATION_SCOPE_WIDENED'); $check($compound, '无胸痛心悸。', true); echo "Clinical fact state natural language: {$checks} assertions passed (synthetic, exact streaming bytes).\n";