push($chunk)) break; roleGrammarExpect($stream->finish() === $expected && $stream->rejected() === !$expected, 'Stream decision differs at byte chunk size ' . $size . ': ' . $text); roleGrammarExpect($expected ? $visible === $wire : $visible === '', 'Accepted bytes changed or a rejected unit leaked at byte chunk size ' . $size . ': ' . $text); } } $record = [ 'id' => 1, 'diagnosis_date' => '2026-10-23', 'present_illness' => '口干;无心悸;无明显胸痛;未记录固定刺痛或腰酸耳鸣。', 'past_history' => '无已知冠心病。', 'allergy_history' => '无已知药物过敏史。', 'tongue' => '舌质淡红,舌面少津,苔少;无黄腻苔、紫暗或瘀斑。', 'pulse' => '脉细弱,按之无力;不浮、不滑、不紧。', ]; $ledger = Guard::build($record); $ids = []; foreach ($ledger['sources'] as $source) $ids[$source['field']] = $source['id']; foreach (['属', '属于', '为'] as $copula) foreach (['待补', '未记录'] as $state) foreach (['依据', '证据', '资料'] as $noun) { roleGrammarCheck($ledger, '固定刺痛' . $copula . $state . $noun . '。', true); roleGrammarCheck($ledger, '口干' . $copula . $state . $noun . '。', false, 'POSITIVE_TO_UNKNOWN'); roleGrammarCheck($ledger, '心悸' . $copula . $state . $noun . '。', false, 'NEGATIVE_TO_UNKNOWN'); } roleGrammarCheck($ledger, '固定刺痛属待补依据,且舌无紫暗瘀斑。', true); roleGrammarCheck($ledger, '固定刺痛属待补依据,舌象未记录。', false, 'RECORDED_TO_UNKNOWN'); foreach (['患者有固定刺痛,属待补依据。', '患者有固定刺痛属待补依据。', '固定刺痛属待补依据且患者有固定刺痛。', '固定刺痛属待补依据,患者有固定刺痛。'] as $text) { roleGrammarCheck($ledger, $text, false, 'UNKNOWN_TO_POSITIVE'); } roleGrammarCheck($ledger, '固定刺痛属待补依据,患者无固定刺痛。', false, 'UNKNOWN_TO_NEGATIVE'); roleGrammarCheck($ledger, '固定刺痛属待补依据,舌红绛。', false, 'TONGUE_FEATURE_NOT_IN_CURRENT_SOURCE'); roleGrammarCheck($ledger, '固定刺痛属待补依据,脉滑。', false, 'PULSE_FEATURE_NOT_IN_CURRENT_SOURCE'); // P2 audit follow-up: a pending record role cannot retract a self-report, // chief complaint, or an occurrence already asserted by its own subject. foreach (['自述固定刺痛', '主诉固定刺痛', '今晨固定刺痛发作', '固定刺痛发作', '今晨自述固定刺痛', '昨日固定刺痛持续了3小时'] as $assertion) { roleGrammarCheck($ledger, $assertion . '。', false, 'UNKNOWN_TO_POSITIVE'); foreach (['属待补依据', '为待补资料', '属于未记录证据'] as $recordTail) { roleGrammarCheck($ledger, $assertion . $recordTail . '。', false, 'UNKNOWN_TO_POSITIVE'); } roleGrammarCheck($ledger, '固定刺痛属待补依据,' . $assertion . '为待补资料。', false, 'UNKNOWN_TO_POSITIVE'); roleGrammarCheck($ledger, $assertion . '属待补依据,舌淡红苔少。', false, 'UNKNOWN_TO_POSITIVE'); } foreach (['自述固定刺痛属未记录固定刺痛。', '主诉固定刺痛为未记录固定刺痛。', '今晨固定刺痛发作属未记录固定刺痛。', '自述固定刺痛属未记录而非否认。', '主诉:固定刺痛为待补资料。', '肾系定位因自述固定刺痛未记录而待定。'] as $assertedRecord) { roleGrammarCheck($ledger, $assertedRecord, false, 'UNKNOWN_TO_POSITIVE'); } foreach (['固定刺痛发作时段属待补依据。', '今晨固定刺痛发作频次为待补资料。', '固定刺痛持续时间属于未记录信息。', '自述内容属待补资料。', '主诉内容为待补资料。', '固定刺痛、腰酸耳鸣属待补依据。', '本次固定刺痛属待补依据。'] as $neutralRecord) { roleGrammarCheck($ledger, $neutralRecord, true); } roleGrammarCheck($ledger, '口干属待补依据,自述固定刺痛为待补资料。', false, 'POSITIVE_TO_UNKNOWN'); roleGrammarCheck($ledger, '心悸为待补资料,主诉固定刺痛属待补依据。', false, 'NEGATIVE_TO_UNKNOWN'); roleGrammarCheck($ledger, '固定刺痛属待补依据,舌象未记录。', false, 'RECORDED_TO_UNKNOWN'); $assertedSource = Guard::build(['present_illness' => '自述固定刺痛属待补依据。']); roleGrammarCheck($assertedSource, '固定刺痛未记录。', false, 'POSITIVE_TO_UNKNOWN'); foreach (['肾系定位', '证名判断', '脏腑归属'] as $conclusion) foreach (['未记录', '尚未记录', '未采集'] as $state) { roleGrammarCheck($ledger, $conclusion . '因腰酸耳鸣' . $state . '而待定。', true); roleGrammarCheck($ledger, $conclusion . '因口干' . $state . '而待定。', false, 'POSITIVE_TO_UNKNOWN'); roleGrammarCheck($ledger, $conclusion . '因心悸' . $state . '而待定。', false, 'NEGATIVE_TO_UNKNOWN'); } roleGrammarCheck($ledger, '腰酸耳鸣未记录而待定。', true); roleGrammarCheck($ledger, '肾系定位因舌象未记录而待定。', false, 'RECORDED_TO_UNKNOWN'); foreach (['患者有腰酸耳鸣,肾系定位因未记录而待定。', '患者有腰酸耳鸣未记录而待定。', '肾系定位因腰酸耳鸣未记录而待定且患者有固定刺痛。'] as $text) { roleGrammarCheck($ledger, $text, false, 'UNKNOWN_TO_POSITIVE'); } roleGrammarCheck($ledger, '肾系定位因腰酸耳鸣未记录而待定,口干未记录。', false, 'POSITIVE_TO_UNKNOWN'); roleGrammarCheck($ledger, '肾系定位因腰酸耳鸣未记录而待定,舌红绛。', false, 'TONGUE_FEATURE_NOT_IN_CURRENT_SOURCE'); foreach (['每次', '每日', '每餐'] as $iteration) foreach (['饮水', '喝水', '进食'] as $measure) { roleGrammarCheck($ledger, $iteration . $measure . '不多,舌淡红苔少。', true); } foreach (['、', ',', '且', '但'] as $boundary) { roleGrammarCheck($ledger, '每次饮水不多' . $boundary . '患者无固定刺痛。', false, 'UNKNOWN_TO_NEGATIVE'); roleGrammarCheck($ledger, '每次饮水不多' . $boundary . '患者有心悸。', false, 'NEGATIVE_TO_POSITIVE'); roleGrammarCheck($ledger, '每次饮水不多' . $boundary . '口干未记录。', false, 'POSITIVE_TO_UNKNOWN'); roleGrammarCheck($ledger, '每次饮水不多' . $boundary . '舌红绛。', false, 'TONGUE_FEATURE_NOT_IN_CURRENT_SOURCE'); roleGrammarCheck($ledger, '每次饮水不多' . $boundary . '脉滑。', false, 'PULSE_FEATURE_NOT_IN_CURRENT_SOURCE'); } roleGrammarCheck(Guard::build(['water_intake' => '饮水未记录。']), '每次饮水不多。', false, 'UNKNOWN_TO_POSITIVE'); roleGrammarCheck($ledger, '每次腰酸耳鸣不多。', false, 'UNKNOWN_TO_POSITIVE'); roleGrammarCheck($ledger, '每次饮水不多舌红绛。', false, 'TONGUE_FEATURE_NOT_IN_CURRENT_SOURCE'); foreach (['', '或', '及', '和', '、'] as $join) foreach (['舌', '舌质', '舌象', '舌体', '舌色'] as $subject) { roleGrammarCheck($ledger, $subject . '无紫暗' . $join . '瘀斑。', true); } $qualified = Guard::build(array_replace($record, ['tongue' => '舌质淡红;无明显紫暗或瘀斑。'])); $partial = Guard::build(array_replace($record, ['tongue' => '舌质淡红;无紫暗。'])); $otherField = Guard::build(array_replace($record, ['tongue' => '舌质淡红;无紫暗。', 'tongue_coating' => '无瘀斑。'])); foreach (['', '或', '及', '和', '、'] as $join) { roleGrammarCheck($qualified, '舌无紫暗' . $join . '瘀斑。', false, 'TONGUE_FEATURE_NOT_IN_CURRENT_SOURCE'); roleGrammarCheck($qualified, '舌无明显紫暗' . $join . '瘀斑。', true); roleGrammarCheck($partial, '舌无紫暗' . $join . '瘀斑。', false, 'TONGUE_FEATURE_NOT_IN_CURRENT_SOURCE'); roleGrammarCheck($otherField, '舌无紫暗' . $join . '瘀斑。', false, 'TONGUE_FEATURE_NOT_IN_CURRENT_SOURCE'); } roleGrammarCheck($ledger, '舌无紫。', false, 'TONGUE_FEATURE_NOT_IN_CURRENT_SOURCE'); roleGrammarCheck($ledger, '舌无紫暗或瘀斑,舌裂纹。', false, 'TONGUE_FEATURE_NOT_IN_CURRENT_SOURCE'); roleGrammarCheck($ledger, '舌无紫暗或瘀斑,脉涩。', false, 'PULSE_FEATURE_NOT_IN_CURRENT_SOURCE'); roleGrammarCheck($ledger, '2026-10-22舌无紫暗或瘀斑。', false, 'TONGUE_FEATURE_NOT_IN_CURRENT_SOURCE'); roleGrammarCheck($ledger, '舌无紫暗或瘀斑(2026-10-22,' . $ids['tongue'] . ')。', false, 'SOURCE_ANNOTATION_INVALID'); roleGrammarCheck($ledger, '舌无紫暗或瘀斑(s999)。', false, 'SOURCE_ANNOTATION_INVALID'); foreach (['红绛', '绛红', '紫暗', '淡红'] as $phenotype) { $matching = Guard::build(['tongue' => '舌' . $phenotype . '。']); roleGrammarCheck($matching, '舌' . $phenotype . '。', true); roleGrammarCheck($matching, '舌无' . $phenotype . '。', false, 'TONGUE_FEATURE_NOT_IN_CURRENT_SOURCE'); } foreach ([',舌', ',且舌', ',舌'] as $boundary) { roleGrammarCheck($ledger, '无心悸' . $boundary . '淡红苔少,脉细弱。', true); roleGrammarCheck($ledger, '无心悸' . $boundary . '红绛。', false, 'TONGUE_FEATURE_NOT_IN_CURRENT_SOURCE'); roleGrammarCheck($ledger, '无心悸' . $boundary . '无淡红。', false, 'TONGUE_FEATURE_NOT_IN_CURRENT_SOURCE'); roleGrammarCheck($ledger, '无固定刺痛' . $boundary . '淡红苔少。', false, 'UNKNOWN_TO_NEGATIVE'); } roleGrammarCheck($ledger, '无明显胸痛,舌淡红苔少。', true); roleGrammarCheck($ledger, '无胸痛,舌淡红苔少。', false, 'NEGATION_SCOPE_WIDENED'); roleGrammarCheck($ledger, '无药物过敏史,舌淡红苔少。', false, 'NEGATION_SCOPE_WIDENED'); foreach (['舌脉未呈黄腻、滑、紧等所见。', '舌未呈紫暗或瘀斑。', '脉未呈滑、紧等所见。', '舌象未呈黄腻苔。', '本次脉象未呈浮。', '舌脉未呈黄腻、滑、紧等所见,但不能仅凭这些否定完全排除夹实。'] as $text) { roleGrammarCheck($ledger, $text, true); } roleGrammarCheck($qualified, '舌未呈紫暗或瘀斑。', false, 'TONGUE_FEATURE_NOT_IN_CURRENT_SOURCE'); roleGrammarCheck($qualified, '舌未呈明显紫暗或瘀斑。', true); roleGrammarCheck($partial, '舌脉未呈黄腻、滑、紧等所见。', false, 'TONGUE_FEATURE_NOT_IN_CURRENT_SOURCE'); roleGrammarCheck($ledger, '舌脉未呈黄腻、滑、涩等所见。', false, 'PULSE_FEATURE_NOT_IN_CURRENT_SOURCE'); roleGrammarCheck($ledger, '舌脉未呈红绛、滑、紧等所见。', false, 'TONGUE_FEATURE_NOT_IN_CURRENT_SOURCE'); roleGrammarCheck($ledger, '舌未呈滑。', false, 'TONGUE_FEATURE_NOT_IN_CURRENT_SOURCE'); roleGrammarCheck($ledger, '脉未呈黄腻。', false, 'PULSE_FEATURE_NOT_IN_CURRENT_SOURCE'); roleGrammarCheck($ledger, '舌脉未呈黄腻、滑、紧等所见,患者有固定刺痛。', false, 'UNKNOWN_TO_POSITIVE'); roleGrammarCheck($ledger, '舌脉未呈黄腻、滑、紧等所见且患者有固定刺痛。', false, 'UNKNOWN_TO_POSITIVE'); roleGrammarCheck($ledger, '舌脉未呈黄腻、滑、紧等所见,舌红绛。', false, 'TONGUE_FEATURE_NOT_IN_CURRENT_SOURCE'); roleGrammarCheck($ledger, '舌脉未呈黄腻、滑、紧等所见,脉涩。', false, 'PULSE_FEATURE_NOT_IN_CURRENT_SOURCE'); roleGrammarCheck($ledger, '舌象未记录。', false, 'RECORDED_TO_UNKNOWN'); roleGrammarCheck($ledger, '未记录紫暗或瘀斑。', false, 'NEGATIVE_TO_UNKNOWN'); roleGrammarCheck($ledger, '舌未呈紫暗或瘀斑(s999)。', false, 'SOURCE_ANNOTATION_INVALID'); $parsed = (new ReflectionMethod(Guard::class, 'parts'))->invoke(null, '每次饮水不多,舌淡红苔少'); roleGrammarExpect(!in_array('negative', array_column($parsed, 'state'), true), 'Quantity 多 is not an absent clinical topic'); $features = (new ReflectionMethod(Guard::class, 'features'))->invoke(null, 'tongue', '舌紫暗或瘀斑'); roleGrammarExpect($features === ['紫暗', '瘀斑'], 'The optional tongue subject cannot consume the start of 紫暗'); $mixed = (new ReflectionMethod(Guard::class, 'parts'))->invoke(null, '舌脉未呈黄腻、滑、紧等所见'); roleGrammarExpect(array_column($mixed, 'topic') === ['舌黄腻', '脉滑、紧'] && array_column($mixed, 'state') === ['negative', 'negative'], 'Mixed examinations retain separate typed negative parts'); // v37: discomfort is a complete symptom lexeme; an outer negative survives. $gutPositive = Guard::build(array_replace($record, ['present_illness' => '胃肠不适;口干;无心悸;未记录固定刺痛。'])); $gutUnknown = Guard::build(array_replace($record, ['present_illness' => '口干;无心悸;未记录胃肠不适或固定刺痛。'])); $gutNegative = Guard::build(array_replace($record, ['present_illness' => '口干;无胃肠不适;未记录固定刺痛。'])); $gutQualified = Guard::build(array_replace($record, ['present_illness' => '口干;无明显胃肠不适。'])); roleGrammarCheck($gutPositive, '胃肠不适,口干。', true); roleGrammarCheck($gutPositive, '用后监测胃肠不适、汗出、口干及气短。', true); foreach (['无胃肠不适。', '患者无胃肠不适。', '未见胃肠不适。'] as $negative) { roleGrammarCheck($gutPositive, $negative, false, 'POSITIVE_TO_NEGATIVE'); roleGrammarCheck($gutUnknown, $negative, false, 'UNKNOWN_TO_NEGATIVE'); roleGrammarCheck($gutNegative, $negative, true); } roleGrammarCheck($gutUnknown, '患者有胃肠不适。', false, 'UNKNOWN_TO_POSITIVE'); roleGrammarCheck($gutUnknown, '胃肠不适每日1次。', false, 'UNKNOWN_TO_POSITIVE'); roleGrammarCheck($gutUnknown, '胃肠不适未记录。', true); roleGrammarCheck($gutPositive, '胃肠不适未记录。', false, 'POSITIVE_TO_UNKNOWN'); roleGrammarCheck($gutNegative, '胃肠不适未记录。', false, 'NEGATIVE_TO_UNKNOWN'); roleGrammarCheck($gutNegative, '患者有胃肠不适。', false, 'NEGATIVE_TO_POSITIVE'); roleGrammarCheck($gutQualified, '无胃肠不适。', false, 'NEGATION_SCOPE_WIDENED'); roleGrammarCheck($gutQualified, '无明显胃肠不适。', true); roleGrammarCheck($gutPositive, '胃肠不适,舌红绛。', false, 'TONGUE_FEATURE_NOT_IN_CURRENT_SOURCE'); roleGrammarCheck($gutPositive, '胃肠不适,脉滑。', false, 'PULSE_FEATURE_NOT_IN_CURRENT_SOURCE'); $gutParts = (new ReflectionMethod(Guard::class, 'parts'))->invoke(null, '胃肠不适、汗出、口干'); roleGrammarExpect(array_column($gutParts, 'state') === ['positive'], 'Discomfort cannot negate following positive symptoms'); $negativeGutParts = (new ReflectionMethod(Guard::class, 'parts'))->invoke(null, '无胃肠不适'); roleGrammarExpect(array_column($negativeGutParts, 'state') === ['negative'] && array_column($negativeGutParts, 'topic') === ['胃肠不适'], 'An outer absence retains the complete discomfort object'); $pendingReview = '新方各成分既往耐受、保健品及其他未记录药物仍待核'; foreach ([$pendingReview, '新药既往耐受、合并用药尚待复核', '药物相互作用、肝肾功能仍需核查', '三味药耐受或保健品待核实'] as $review) { roleGrammarCheck($ledger, $review . '。', true); roleGrammarCheck($ledger, '本次无已知药物过敏史,但' . $review . '。', true); } foreach ([',', '且', '但', '而', ''] as $join) { roleGrammarCheck($ledger, $pendingReview . $join . '患者无固定刺痛。', false, 'UNKNOWN_TO_NEGATIVE'); roleGrammarCheck($ledger, $pendingReview . $join . '患者有心悸。', false, 'NEGATIVE_TO_POSITIVE'); roleGrammarCheck($ledger, $pendingReview . $join . '自述固定刺痛属待补依据。', false, 'UNKNOWN_TO_POSITIVE'); } foreach ([',', '且', '但', '而'] as $join) { roleGrammarCheck($ledger, $pendingReview . $join . '口干未记录。', false, 'POSITIVE_TO_UNKNOWN'); roleGrammarCheck($ledger, $pendingReview . $join . '心悸未记录。', false, 'NEGATIVE_TO_UNKNOWN'); roleGrammarCheck($ledger, $pendingReview . $join . '舌红绛。', false, 'TONGUE_FEATURE_NOT_IN_CURRENT_SOURCE'); roleGrammarCheck($ledger, $pendingReview . $join . '脉滑。', false, 'PULSE_FEATURE_NOT_IN_CURRENT_SOURCE'); } roleGrammarCheck($ledger, '患者无固定刺痛,' . $pendingReview . '。', false, 'UNKNOWN_TO_NEGATIVE'); roleGrammarCheck($ledger, '本次无药物过敏史,但' . $pendingReview . '。', false, 'NEGATION_SCOPE_WIDENED'); roleGrammarCheck($ledger, $pendingReview . '(s999)。', false, 'SOURCE_ANNOTATION_INVALID'); roleGrammarCheck($ledger, $pendingReview . '今晨固定刺痛发作。', false, 'UNKNOWN_TO_POSITIVE'); roleGrammarCheck($ledger, $pendingReview . '2026-10-23患者无固定刺痛。', false, 'UNKNOWN_TO_NEGATIVE'); $safetyQuantity = Guard::build(array_replace($record, ['water_intake' => '饮水量未记录。'])); roleGrammarCheck($safetyQuantity, $pendingReview . '每次饮水量不多。', false, 'UNKNOWN_TO_POSITIVE'); roleGrammarCheck($safetyQuantity, $pendingReview . '且每次饮水量不多。', false, 'UNKNOWN_TO_POSITIVE'); roleGrammarCheck($ledger, '固定刺痛仍待核。', false, 'UNKNOWN_TO_POSITIVE'); roleGrammarCheck($ledger, '固定刺痛耐受、患者无固定刺痛仍待核。', false, 'UNKNOWN_TO_NEGATIVE'); // Existing applicability and no-self-changing-medication instructions did not // cause the original rejection. They receive no new blanket exemption. foreach (['孕育相关不适用于男性', '不擅停换'] as $instruction) { roleGrammarCheck($ledger, $instruction . '。', true); roleGrammarCheck($ledger, $instruction . ',患者无固定刺痛。', false, 'UNKNOWN_TO_NEGATIVE'); roleGrammarCheck($ledger, $instruction . '且患者有心悸。', false, 'NEGATIVE_TO_POSITIVE'); roleGrammarCheck($ledger, $instruction . ',口干未记录。', false, 'POSITIVE_TO_UNKNOWN'); roleGrammarCheck($ledger, $instruction . ',舌红绛。', false, 'TONGUE_FEATURE_NOT_IN_CURRENT_SOURCE'); } $applicabilityParts = (new ReflectionMethod(Guard::class, 'parts'))->invoke(null, '孕育相关不适用于男性'); roleGrammarExpect(in_array('negative', array_column($applicabilityParts, 'state'), true), 'The discomfort lexeme rule does not reinterpret the applicability predicate'); foreach (['不适用', '不适于', '不适合', '不适应', '不适宜', '不适当'] as $predicate) { $predicateParts = (new ReflectionMethod(Guard::class, 'parts'))->invoke(null, '该药物' . $predicate . '此情形'); roleGrammarExpect(in_array('negative', array_column($predicateParts, 'state'), true), 'A complete applicability/suitability predicate is not the discomfort lexeme: ' . $predicate); } // Portable complete safety units, rebuilt against the actual production // source projection. A lexical pass does not certify the safety advice. $safetySnapshot = (new ReflectionMethod(PatientAiReportLogic::class, 'buildSourceSnapshotFromRows'))->invoke(null, CompleteTcmCase::rows()); $safetySubmission = (new ReflectionMethod(PatientAiReportLogic::class, 'sanitizeSnapshotForUpstream'))->invoke(null, AiRequiredSource::submission($safetySnapshot)); $safetyLedger = Guard::build(CompleteTcmCase::current(), $safetySubmission); $safetyUnits = [ '禁忌与用药复核:本次询问为“无已知药物过敏史”(s5),但新方各成分既往耐受、保健品及其他未记录药物仍待核。' . '现有ALT 22U/L、肌酐78μmol/L、eGFR 96mL/min/1.73m²、尿ACR 12mg/g,可作启用前基线,不能替代后续监测。' . '孕育相关不适用于男性,但胃肠耐受、肝肾变化及过敏反应仍需核查。', '禁忌待核:虽本次询问“无已知药物过敏史”(s5),仍须核实三味药既往耐受、其他药物及保健品;用后监测皮疹、胃肠不适、汗出、口干、气短及肝肾功能。' . '现用二甲双胍0.5g每日2次,维持既有医嘱,不擅停换;联用期间继续监测空腹、餐后血糖及低血糖相关症状。' . '2020-05-02藿香正气散系胃肠不适旧方,早已停用,不续用。', ]; foreach ($safetyUnits as $safetyUnit) roleGrammarCheck($safetyLedger, $safetyUnit, true); // v38: absence of a symptom record, classified unknown, remains a data gap. foreach (['无', '没有', '未见'] as $missing) foreach (['记录', '资料'] as $recordNoun) foreach (['属', '为'] as $copula) { $tail = $recordNoun . $copula . '未知。'; roleGrammarCheck($ledger, $missing . '固定刺痛' . $tail, true); roleGrammarCheck($ledger, $missing . '口干' . $tail, false, 'POSITIVE_TO_UNKNOWN'); roleGrammarCheck($ledger, $missing . '心悸' . $tail, false, 'NEGATIVE_TO_UNKNOWN'); roleGrammarCheck($ledger, $missing . '舌象' . $tail, false, 'RECORDED_TO_UNKNOWN'); } roleGrammarCheck($ledger, '无固定刺痛的记录属于不详。', true); roleGrammarCheck($ledger, '无口干信息属未知。', false, 'POSITIVE_TO_UNKNOWN'); roleGrammarCheck($ledger, '无心悸描述属未知。', false, 'NEGATIVE_TO_UNKNOWN'); foreach ([',', '且'] as $join) { roleGrammarCheck($ledger, '无固定刺痛记录属未知' . $join . '患者有固定刺痛。', false, 'UNKNOWN_TO_POSITIVE'); roleGrammarCheck($ledger, '无固定刺痛记录属未知' . $join . '患者无固定刺痛。', false, 'UNKNOWN_TO_NEGATIVE'); roleGrammarCheck($ledger, '无固定刺痛记录属未知' . $join . '口干未记录。', false, 'POSITIVE_TO_UNKNOWN'); roleGrammarCheck($ledger, '无固定刺痛记录属未知' . $join . '舌红绛。', false, 'TONGUE_FEATURE_NOT_IN_CURRENT_SOURCE'); } roleGrammarCheck($ledger, '患者有固定刺痛,无固定刺痛记录属未知。', false, 'UNKNOWN_TO_POSITIVE'); roleGrammarCheck($ledger, '自述固定刺痛属待补依据,无固定刺痛记录属未知。', false, 'UNKNOWN_TO_POSITIVE'); roleGrammarCheck($ledger, '无固定刺痛记录属未知,患者无药物过敏史。', false, 'NEGATION_SCOPE_WIDENED'); roleGrammarCheck($ledger, '无固定刺痛记录属未知(s999)。', false, 'SOURCE_ANNOTATION_INVALID'); roleGrammarCheck($ledger, '无固定刺痛记录属未知(2026-10-22,' . $ids['present_illness'] . ')。', false, 'SOURCE_ANNOTATION_INVALID'); $missingRecordSource = Guard::build(['present_illness' => '无固定刺痛记录属未知。']); roleGrammarCheck($missingRecordSource, '无固定刺痛。', false, 'UNKNOWN_TO_NEGATIVE'); roleGrammarCheck($missingRecordSource, '患者有固定刺痛。', false, 'UNKNOWN_TO_POSITIVE'); $missingRecordParts = (new ReflectionMethod(Guard::class, 'parts'))->invoke(null, '无固定刺痛记录属未知'); roleGrammarExpect(array_column($missingRecordParts, 'state') === ['unknown'] && array_column($missingRecordParts, 'topic') === ['固定刺痛'], 'Copular missing-record state retains its exact unknown subject'); // Explicit formula method/aim fragments are conceptual. The heading alone // grants no exemption to observed findings or subsequent unrelated prose. $functionAim = '方义上以养津改善口干舌燥'; foreach ([$functionAim . '。', '方义上以生津缓解口干舌燥。', '方义层面以养津改善口干舌燥。', '方义:以补气恢复推动固摄,以养津改善口干舌燥,并以收敛配合控制自汗。'] as $functionText) { roleGrammarCheck($ledger, $functionText, true); } foreach (['患者舌燥。', '以养津改善口干舌燥。', '方义上患者舌燥。', '方义上以养津改善患者舌燥。', '方义上以舌燥改善口干。', '方义上舌燥,以养津改善口干。', '患者舌燥,' . $functionAim . '。', $functionAim . ',舌燥。', $functionAim . ',舌红绛。', '舌红绛,' . $functionAim . '。', $functionAim . ',患者口干,以养津改善口干舌燥。'] as $actualExam) { roleGrammarCheck($ledger, $actualExam, false, 'TONGUE_FEATURE_NOT_IN_CURRENT_SOURCE'); } foreach ([',', '且', '但', '而', ''] as $join) { roleGrammarCheck($ledger, $functionAim . $join . '患者有固定刺痛。', false, 'UNKNOWN_TO_POSITIVE'); roleGrammarCheck($ledger, $functionAim . $join . '患者无固定刺痛。', false, 'UNKNOWN_TO_NEGATIVE'); roleGrammarCheck($ledger, $functionAim . $join . '自述固定刺痛属待补依据。', false, 'UNKNOWN_TO_POSITIVE'); } foreach ([',', '且', '但', '而'] as $join) { roleGrammarCheck($ledger, $functionAim . $join . '口干未记录。', false, 'POSITIVE_TO_UNKNOWN'); roleGrammarCheck($ledger, $functionAim . $join . '心悸未记录。', false, 'NEGATIVE_TO_UNKNOWN'); roleGrammarCheck($ledger, $functionAim . $join . '脉滑。', false, 'PULSE_FEATURE_NOT_IN_CURRENT_SOURCE'); } roleGrammarCheck($ledger, $functionAim . ',患者无药物过敏史。', false, 'NEGATION_SCOPE_WIDENED'); roleGrammarCheck($ledger, $functionAim . ',舌无紫暗或瘀斑。', true); roleGrammarCheck($ledger, $functionAim . ',舌无紫暗或瘀斑(s999)。', false, 'SOURCE_ANNOTATION_INVALID'); roleGrammarCheck($ledger, $functionAim . '(s999)。', false, 'SOURCE_ANNOTATION_INVALID'); roleGrammarCheck($ledger, $functionAim . '今晨固定刺痛发作。', false, 'UNKNOWN_TO_POSITIVE'); roleGrammarCheck($safetyQuantity, $functionAim . '且每次饮水量不多。', false, 'UNKNOWN_TO_POSITIVE'); roleGrammarCheck($ledger, '方义上以养津改善自述固定刺痛。', false, 'UNKNOWN_TO_POSITIVE'); roleGrammarCheck($ledger, '方义上以养津改善患者有固定刺痛。', false, 'UNKNOWN_TO_POSITIVE'); $recordAndFunctionUnits = [ '血:面色少华及脉细可作血不足鉴别,但缺少明确头晕、唇甲色淡等资料,尚不能独立定为血虚;无固定刺痛记录属未知(s2),不能据此排除血瘀。', '本例支持证据:神疲、语声低微、动则气短、自汗且活动后加重,脉细弱、按之无力,支持益气;口干欲饮而少量频饮、大便稍干、舌面少津及苔少,支持生津。' . '方义上以补气恢复推动固摄,以养津改善口干舌燥,并以收敛配合控制自汗;是否采用仍须结合面诊确认汗出性质及津亏程度。', ]; foreach ($recordAndFunctionUnits as $recordAndFunctionUnit) roleGrammarCheck($safetyLedger, $recordAndFunctionUnit, true); // v39: an observation joined without a comma is never a formula aim object. $shortFunctionAim = '方义上以养津改善口干'; $joinedObservations = [ ['并诉称固定刺痛', 'UNKNOWN_TO_POSITIVE'], ['并诊见舌红绛', 'TONGUE_FEATURE_NOT_IN_CURRENT_SOURCE'], ['同时脉滑', 'PULSE_FEATURE_NOT_IN_CURRENT_SOURCE'], ['还诉称固定刺痛', 'UNKNOWN_TO_POSITIVE'], ['又查见舌红绛', 'TONGUE_FEATURE_NOT_IN_CURRENT_SOURCE'], ['并且报告固定刺痛', 'UNKNOWN_TO_POSITIVE'], ['另查见脉滑', 'PULSE_FEATURE_NOT_IN_CURRENT_SOURCE'], ['诉称固定刺痛', 'UNKNOWN_TO_POSITIVE'], ['诊见舌红绛', 'TONGUE_FEATURE_NOT_IN_CURRENT_SOURCE'], ['舌质呈红绛', 'TONGUE_FEATURE_NOT_IN_CURRENT_SOURCE'], ['脉呈滑', 'PULSE_FEATURE_NOT_IN_CURRENT_SOURCE'], ]; foreach ($joinedObservations as [$observation, $code]) { roleGrammarCheck($safetyLedger, $shortFunctionAim . $observation . '。', false, $code); roleGrammarCheck($safetyLedger, $shortFunctionAim . ',' . $observation . '。', false, $code); roleGrammarCheck($safetyLedger, $observation . ',' . $shortFunctionAim . '。', false, $code); } roleGrammarCheck($ledger, $functionAim . '并口干未记录。', false, 'POSITIVE_TO_UNKNOWN'); roleGrammarCheck($ledger, $functionAim . '同时心悸未记录。', false, 'NEGATIVE_TO_UNKNOWN'); roleGrammarCheck($ledger, $functionAim . '还舌象未记录。', false, 'RECORDED_TO_UNKNOWN'); roleGrammarCheck($ledger, $functionAim . '又患者无固定刺痛。', false, 'UNKNOWN_TO_NEGATIVE'); roleGrammarCheck($ledger, $functionAim . '另无胸痛。', false, 'NEGATION_SCOPE_WIDENED'); roleGrammarCheck($ledger, $functionAim . '并患者无药物过敏史。', false, 'NEGATION_SCOPE_WIDENED'); roleGrammarCheck($ledger, $functionAim . '并自述固定刺痛属待补依据。', false, 'UNKNOWN_TO_POSITIVE'); roleGrammarCheck($ledger, $functionAim . '还诊见舌红绛属待补依据。', false, 'TONGUE_FEATURE_NOT_IN_CURRENT_SOURCE'); roleGrammarCheck($safetyQuantity, $functionAim . '同时每次饮水量不多。', false, 'UNKNOWN_TO_POSITIVE'); roleGrammarCheck($ledger, $functionAim . '并舌无紫暗或瘀斑。', true); roleGrammarCheck($ledger, $functionAim . '并舌无紫暗或瘀斑(s999)。', false, 'SOURCE_ANNOTATION_INVALID'); roleGrammarCheck($ledger, $functionAim . '。', true); roleGrammarCheck($ledger, '方义上以养津改善口干及舌燥。', true); roleGrammarCheck($ledger, '方义上以补气恢复推动固摄,并以养津改善口干舌燥,亦以收敛配合控制自汗。', true); // A diagnostic evidence threshold is UNKNOWN, never a blanket heat/cold denial. foreach (['本次没有', '当前无'] as $absence) foreach (['确定', '确认'] as $relation) foreach (['热象', '寒象'] as $evidenceHead) { $thresholdPrefix = $absence . '足以独立' . $relation; $thresholdSuffix = '的阳性' . $evidenceHead . '。'; roleGrammarCheck($ledger, $thresholdPrefix . '热证' . $thresholdSuffix, true); roleGrammarCheck($ledger, $thresholdPrefix . '口干' . $thresholdSuffix, false, 'POSITIVE_TO_UNKNOWN'); roleGrammarCheck($ledger, $thresholdPrefix . '心悸' . $thresholdSuffix, false, 'NEGATIVE_TO_UNKNOWN'); roleGrammarCheck($ledger, $thresholdPrefix . '舌象' . $thresholdSuffix, false, 'RECORDED_TO_UNKNOWN'); } $thresholdPhrase = '本次没有足以独立确定热证的阳性热象'; $thresholdParts = (new ReflectionMethod(Guard::class, 'parts'))->invoke(null, $thresholdPhrase); roleGrammarExpect(array_column($thresholdParts, 'state') === ['unknown'] && array_column($thresholdParts, 'topic') === ['热证'], 'An explicit independent threshold retains its unknown diagnostic subject'); foreach (['本次没有足以确定热证的阳性热象。', '本次没有阳性热象。', '本次无独立寒象。', '本次亦无独立阳性寒象。', '本次无寒象。'] as $bareSignDenial) { roleGrammarCheck($ledger, $bareSignDenial, false, 'NEGATIVE_NOT_IN_SOURCE'); } roleGrammarCheck($ledger, $thresholdPhrase . ',患者有固定刺痛。', false, 'UNKNOWN_TO_POSITIVE'); roleGrammarCheck($ledger, $thresholdPhrase . ',患者无固定刺痛。', false, 'UNKNOWN_TO_NEGATIVE'); roleGrammarCheck($ledger, $thresholdPhrase . ',口干未记录。', false, 'POSITIVE_TO_UNKNOWN'); roleGrammarCheck($ledger, $thresholdPhrase . ',舌红绛。', false, 'TONGUE_FEATURE_NOT_IN_CURRENT_SOURCE'); roleGrammarCheck($ledger, $thresholdPhrase . ',脉滑。', false, 'PULSE_FEATURE_NOT_IN_CURRENT_SOURCE'); roleGrammarCheck($ledger, $thresholdPhrase . ',患者无药物过敏史。', false, 'NEGATION_SCOPE_WIDENED'); roleGrammarCheck($ledger, $thresholdPhrase . '(s999)。', false, 'SOURCE_ANNOTATION_INVALID'); roleGrammarCheck($ledger, $thresholdPhrase . '(2026-10-22,' . $ids['present_illness'] . ')。', false, 'SOURCE_ANNOTATION_INVALID'); $knownHeat = Guard::build(array_replace($record, ['clinical_diagnosis' => '本次已确认热证。'])); roleGrammarCheck($knownHeat, $thresholdPhrase . '。', false, 'POSITIVE_TO_UNKNOWN'); roleGrammarCheck($ledger, '本次没有足以独立确定患者有固定刺痛的阳性热象。', false, 'UNKNOWN_TO_POSITIVE'); roleGrammarCheck($ledger, '本次没有足以独立确定热证并诉称固定刺痛的阳性热象。', false); // The optional Qwen grammar repair ends a negative at an explicit gradual // postfix occurrence, while the preceding qualifier and new symptom still count. $gradualOccurrenceUnit = '表里:病位在里。依据:无恶寒发热、无明显急性感染诱因,症状逐渐出现,非外感表证。限制依据:s2。'; roleGrammarCheck($safetyLedger, $gradualOccurrenceUnit, true); roleGrammarCheck($safetyLedger, str_replace('无明显急性感染诱因', '无急性感染诱因', $gradualOccurrenceUnit), false, 'NEGATION_SCOPE_WIDENED'); foreach (['逐渐', '渐次', '陆续'] as $gradual) { roleGrammarCheck($ledger, '无心悸,固定刺痛' . $gradual . '出现。', false, 'UNKNOWN_TO_POSITIVE'); roleGrammarCheck($ledger, '无心悸,口干' . $gradual . '出现。', true); roleGrammarCheck($ledger, '无心悸,口干' . $gradual . '出现,舌红绛。', false, 'TONGUE_FEATURE_NOT_IN_CURRENT_SOURCE'); roleGrammarCheck($ledger, '无胸痛,口干' . $gradual . '出现。', false, 'NEGATION_SCOPE_WIDENED'); } roleGrammarCheck($safetyLedger, '患者无急性感染诱因。', false, 'NEGATION_SCOPE_WIDENED'); roleGrammarCheck($ledger, '无心悸,胸痛,口干逐渐出现。', false, 'NEGATION_SCOPE_WIDENED'); roleGrammarCheck($ledger, '无心悸,固定刺痛,口干逐渐出现。', false, 'UNKNOWN_TO_NEGATIVE'); roleGrammarCheck($safetyLedger, '日常记录显示活动后乏力、汗出较明显,无胸痛或晕厥。', false, 'NEGATION_SCOPE_WIDENED'); roleGrammarCheck($safetyLedger, '2026-10-22活动记录见“活动后乏力、汗出较明显,无胸痛或晕厥”。', true); // v40: a complete evidence-limited exclusion and a locally linked review // question must not borrow polarity from an earlier observed statement. $syndromeExclusion = '仍不能仅凭证候排除心肺、感染或代谢因素'; $modalReviewHead = '适用前须确认仍以气虚、自汗、津少为主'; $linkedReviewQuestion = '并复核气短、自汗有无心肺、感染或代谢原因'; $linkedReview = $modalReviewHead . ',' . $linkedReviewQuestion; roleGrammarCheck($safetyLedger, '自汗及气短虽有虚象,' . $syndromeExclusion . '。', true); roleGrammarCheck($safetyLedger, $linkedReview . '。', true); roleGrammarCheck($safetyLedger, str_replace('有无', '是否', $linkedReview) . '。', true); roleGrammarCheck($safetyLedger, '适用前需核实气短、自汗为主,复核气短有无感染原因。', true); roleGrammarCheck($safetyLedger, '适用前应再次确认气虚、自汗为主,再核查气短是否感染原因。', true); foreach (['仍不能仅凭证候排除感染。', '仍不得只据证候完全排除感染。', '不能仅凭本次资料排除感染。', '不能仅凭当前阴性记录排除感染。'] as $limit) { roleGrammarCheck($safetyLedger, $limit, true); } $reviewAdjacentAssertions = [ ['患者有固定刺痛', 'UNKNOWN_TO_POSITIVE'], ['患者无固定刺痛', 'UNKNOWN_TO_NEGATIVE'], ['口干未记录', 'POSITIVE_TO_UNKNOWN'], ['舌象未记录', 'RECORDED_TO_UNKNOWN'], ['舌红绛', 'TONGUE_FEATURE_NOT_IN_CURRENT_SOURCE'], ['脉滑', 'PULSE_FEATURE_NOT_IN_CURRENT_SOURCE'], ]; foreach ([$syndromeExclusion, $linkedReview] as $reviewRole) { foreach ($reviewAdjacentAssertions as [$assertion, $code]) { roleGrammarCheck($safetyLedger, $reviewRole . ',' . $assertion . '。', false, $code); roleGrammarCheck($safetyLedger, $assertion . ',' . $reviewRole . '。', false, $code); } foreach (['并诉称固定刺痛', '还诉称固定刺痛'] as $tail) { roleGrammarCheck($safetyLedger, $reviewRole . $tail . '。', false, 'UNKNOWN_TO_POSITIVE'); } foreach (['并诊见舌红绛', '又查见舌红绛'] as $tail) { roleGrammarCheck($safetyLedger, $reviewRole . $tail . '。', false, 'TONGUE_FEATURE_NOT_IN_CURRENT_SOURCE'); } roleGrammarCheck($safetyLedger, $reviewRole . '同时脉滑。', false, 'PULSE_FEATURE_NOT_IN_CURRENT_SOURCE'); foreach (['且', '但', '而', '并', '同时', '还', '又', '另', ''] as $join) { roleGrammarCheck($safetyLedger, $reviewRole . $join . '患者有固定刺痛。', false, 'UNKNOWN_TO_POSITIVE'); } foreach (['且', '并', '同时', '还', '又', '另'] as $join) { roleGrammarCheck($safetyLedger, $reviewRole . $join . '口干未记录。', false, 'POSITIVE_TO_UNKNOWN'); roleGrammarCheck($safetyLedger, $reviewRole . $join . '心悸未记录。', false, 'NEGATIVE_TO_UNKNOWN'); } roleGrammarCheck($safetyLedger, $reviewRole . '今晨固定刺痛发作。', false, 'UNKNOWN_TO_POSITIVE'); roleGrammarCheck($safetyQuantity, $reviewRole . '每次饮水量不多。', false, 'UNKNOWN_TO_POSITIVE'); roleGrammarCheck($safetyLedger, $reviewRole . '2026-10-23患者无固定刺痛。', false, 'UNKNOWN_TO_NEGATIVE'); roleGrammarCheck($safetyLedger, $reviewRole . ',患者有感染。', false, 'NEGATIVE_TO_POSITIVE'); roleGrammarCheck($safetyLedger, $reviewRole . ',患者无急性感染诱因。', false, 'NEGATION_SCOPE_WIDENED'); roleGrammarCheck($safetyLedger, $reviewRole . ',患者无药物过敏史。', false, 'NEGATION_SCOPE_WIDENED'); roleGrammarCheck($safetyLedger, $reviewRole . '(s999)。', false, 'SOURCE_ANNOTATION_INVALID'); roleGrammarCheck($safetyLedger, $reviewRole . '(2026-10-22,s2)。', false, 'SOURCE_ANNOTATION_INVALID'); } roleGrammarCheck($safetyLedger, $linkedReviewQuestion . '。', false, 'NEGATION_SCOPE_WIDENED'); roleGrammarCheck($safetyLedger, '有无感染。', false, 'NEGATION_SCOPE_WIDENED'); roleGrammarCheck($safetyLedger, $modalReviewHead . ',复核无固定刺痛。', false, 'UNKNOWN_TO_NEGATIVE'); roleGrammarCheck($safetyLedger, '适用前须确认患者有固定刺痛,' . $linkedReviewQuestion . '。', false, 'UNKNOWN_TO_POSITIVE'); roleGrammarCheck($safetyLedger, $modalReviewHead . ',并复核患者有固定刺痛。', false, 'UNKNOWN_TO_POSITIVE'); roleGrammarCheck($safetyLedger, $modalReviewHead . ',并复核气短有无患者有固定刺痛。', false, 'UNKNOWN_TO_POSITIVE'); roleGrammarCheck($safetyLedger, $modalReviewHead . ',并复核气短有无感染患者有固定刺痛。', false, 'UNKNOWN_TO_POSITIVE'); roleGrammarCheck($safetyLedger, $modalReviewHead . ',并复核气短有无感染诊见舌红绛。', false, 'TONGUE_FEATURE_NOT_IN_CURRENT_SOURCE'); $multipleQuestionProse = (new ReflectionMethod(Guard::class, 'linkedReviewRequestProse'))->invoke(null, $modalReviewHead . ',并复核气短有无感染有无心肺原因'); roleGrammarExpect(preg_match_all('/有无/u', $multipleQuestionProse) === 2, 'A multiple-interrogative fragment receives no linked-question normalization'); roleGrammarCheck($safetyLedger, $modalReviewHead . ',并复核气短有无无固定刺痛。', false, 'UNKNOWN_TO_NEGATIVE'); roleGrammarCheck($safetyLedger, '仍不能仅凭患者无固定刺痛排除感染。', false, 'UNKNOWN_TO_NEGATIVE'); $reviewQuestionProse = (new ReflectionMethod(Guard::class, 'reportProse'))->invoke(null, $linkedReview); roleGrammarExpect(!str_contains($reviewQuestionProse, '有无') && !str_contains($reviewQuestionProse, '感染'), 'A linked question is recognized while its typed modal context still exists'); $exclusionProse = (new ReflectionMethod(Guard::class, 'reportProse'))->invoke(null, '自汗及气短虽有虚象,' . $syndromeExclusion); roleGrammarExpect(str_contains($exclusionProse, '有虚象') && !str_contains($exclusionProse, '感染'), 'The observed prefix remains visible while only the bounded exclusion fragment is normalized'); $reviewAndExclusionUnits = [ '反证与局限:寒热均未定,且限制依据:(s2);因此不能把本方向解释为治疗阴虚火旺,也不能加入温阳或清热立法。' . '便稍干、小便略多还可能受饮水、血糖等影响;自汗及气短虽有虚象,仍不能仅凭证候排除心肺、感染或代谢因素。' . '若出现明显胸痛、心悸、喘促、浮肿、意识改变或急性高热,应先行现代医学评估,而非继续按原方向处理。', '反证与局限:寒热未定,限制依据:(s2);本方向不能解释为治疗阴虚火旺,亦不据此采用温阳或清热法。' . '适用前须确认仍以气虚、自汗、津少为主,并复核气短、自汗有无心肺、感染或代谢原因;症状性质改变时应重新辨证。', ]; foreach ($reviewAndExclusionUnits as $reviewAndExclusionUnit) roleGrammarCheck($safetyLedger, $reviewAndExclusionUnit, true); // Actual terminal v41 C2 rejections: an explicit unknown-state noun and the // colon immediately before a valid source citation must not become a positive // symptom. Neither closed role may erase a known finding or borrow an adjacent // assertion, and the stream must preserve the original unmodified units. $unknownStateUnits = [ '血:面色少华、脉细可作血少鉴别,但无血常规、头晕、唇甲色泽等证据,现有资料不足以确定血虚。' . '固定刺痛为未记录状态(s2),不能据此排除血瘀。', '血:面色少华、脉细仅提示血少鉴别;血常规、头晕、唇甲色泽未记录,现有资料不足以确定血虚。' . '固定刺痛属待补依据:(s2),不能判断血瘀与否。', ]; foreach ($unknownStateUnits as $unit) roleGrammarCheck($safetyLedger, $unit, true); foreach (['为未记录状态', '属待补依据:', '属于未记录状态:'] as $role) { roleGrammarCheck($ledger, '固定刺痛' . $role . '。', true); roleGrammarCheck($ledger, '固定刺痛' . $role . '(' . $ids['present_illness'] . ')。', true); roleGrammarCheck($ledger, '口干' . $role . '。', false, 'POSITIVE_TO_UNKNOWN'); roleGrammarCheck($ledger, '心悸' . $role . '。', false, 'NEGATIVE_TO_UNKNOWN'); roleGrammarCheck($ledger, '舌象' . $role . '。', false, 'RECORDED_TO_UNKNOWN'); roleGrammarCheck($ledger, '脉象' . $role . '。', false, 'RECORDED_TO_UNKNOWN'); foreach (['自述固定刺痛', '主诉固定刺痛', '今晨固定刺痛发作', '患者有固定刺痛'] as $assertion) { roleGrammarCheck($ledger, $assertion . $role . '。', false, 'UNKNOWN_TO_POSITIVE'); } foreach ([ ['患者有固定刺痛', 'UNKNOWN_TO_POSITIVE'], ['患者无固定刺痛', 'UNKNOWN_TO_NEGATIVE'], ['口干未记录', 'POSITIVE_TO_UNKNOWN'], ['舌象未记录', 'RECORDED_TO_UNKNOWN'], ['舌红绛', 'TONGUE_FEATURE_NOT_IN_CURRENT_SOURCE'], ['脉滑', 'PULSE_FEATURE_NOT_IN_CURRENT_SOURCE'], ] as [$assertion, $code]) { roleGrammarCheck($ledger, '固定刺痛' . $role . ',' . $assertion . '。', false, $code); roleGrammarCheck($ledger, $assertion . ',固定刺痛' . $role . '。', false, $code); } foreach (['且', '并', '同时', '还', '又', '另', ''] as $join) { roleGrammarCheck($ledger, '固定刺痛' . $role . $join . '患者有固定刺痛。', false, 'UNKNOWN_TO_POSITIVE'); } roleGrammarCheck($ledger, '固定刺痛' . $role . '(s999)。', false, 'SOURCE_ANNOTATION_INVALID'); roleGrammarCheck($ledger, '固定刺痛' . $role . '(2026-10-22,' . $ids['present_illness'] . ')。', false, 'SOURCE_ANNOTATION_INVALID'); } // Each complete unit preserves byte identity, including split UTF-8 and CRLF. $units = [ '固定刺痛属待补依据(' . $ids['present_illness'] . '),且舌无紫暗或瘀斑(' . $ids['tongue'] . ')。', '肾系定位因腰酸耳鸣未记录而待定。每次饮水不多,舌淡红苔少。', '舌脉未呈黄腻、滑、紧等所见(' . $ids['tongue'] . '、' . $ids['pulse'] . ')。', ]; $wire = implode("\r\n\r\n", $units) . "\r\n\r\n"; foreach ([1, 2, 7, 31] as $size) { $visible = ''; $stream = new TcmClinicalReportStream($ledger, static function (string $delta) use (&$visible): bool { $visible .= $delta; return true; }); $pushed = true; foreach (str_split($wire, $size) as $chunk) if (!$stream->push($chunk)) { $pushed = false; break; } roleGrammarExpect($pushed && $stream->finish() && !$stream->rejected(), 'Complete role units survive byte chunk size ' . $size); roleGrammarExpect($visible === $wire, 'Complete accepted role units preserve every byte at chunk size ' . $size); } echo 'Clinical fact state bounded role grammar: ' . $checks . " assertions passed (offline, original streaming bytes).\n";