invoke(null, CompleteTcmCase::rows()); $submission = (new ReflectionMethod(PatientAiReportLogic::class, 'sanitizeSnapshotForUpstream')) ->invoke(null, AiRequiredSource::submission($snapshot)); $ledger = Guard::build(CompleteTcmCase::current(), $submission); $expect($ledger['version'] === 'clinical-source-state-v2', 'The ledger schema/version is unchanged.'); $expect($ledger['sources']['s5']['quote'] === '本次询问无已知药物过敏史' && $ledger['sources']['s5']['date'] === '2026-10-23' && $ledger['sources']['s5']['scope'] === 'current', 'The real pure fixture projection proves the s5 quote/date/scope.'); $expect($ledger['sources']['s2']['quote'] === CompleteTcmCase::current()['present_illness'] && $ledger['sources']['s14']['quote'] === CompleteTcmCase::current()['present_illness'], 'Both original rejection source IDs refer to the unchanged actual fixture history.'); $check = static function (string $text, bool $valid, string $label, string $code = '', ?array $caseLedger = null) use ($ledger, $expect, $guardOnly): void { $targetLedger = $caseLedger ?? $ledger; $before = $text; $review = Guard::validate($targetLedger, $text); $expect($review['valid'] === $valid, $label . ': ' . json_encode($review, JSON_UNESCAPED_UNICODE)); if ($code !== '') $expect(in_array($code, array_column($review['issues'], 'code'), true), $label . ': expected ' . $code . ': ' . json_encode([$text, $review], JSON_UNESCAPED_UNICODE)); $expect($text === $before, $label . ': validation preserves the original string.'); if ($guardOnly) return; $raw = preg_match('/(?:\r?\n){2}$/D', $text) ? $text : $text . "\n\n"; foreach ([1, 2, 7, 31] as $size) { $visible = ''; $stream = new TcmClinicalReportStream($targetLedger, static function (string $delta) use (&$visible): bool { $visible .= $delta; return true; }); $pushed = true; foreach (str_split($raw, $size) as $chunk) if (!$stream->push($chunk)) { $pushed = false; break; } $finished = $stream->finish(); $expect(($pushed && $finished) === $valid && $stream->rejected() === !$valid, $label . ': stream state, chunk ' . $size); $expect($visible === ($valid ? $raw : ''), $label . ': exact bytes or zero rejected-unit bytes, chunk ' . $size); $expect($stream->acceptedContent() === $visible, $label . ': accepted/visible identity, chunk ' . $size); } }; // Portable exact retained units, also compared to the ignored artifact when present. $units = [ "适用前须核实:是否存在感冒、感染或其他急性疾病表现?是否存在舌苔转厚腻、胸脘痞闷等不宜收敛或滋腻的情况?是否另有处方药、保健品或中药同用?\n\n", "适用前须核实:是否另用处方药、保健品或中药?“本次询问无已知药物过敏史”(s5)仅代表已知范围,新用药仍须审核过敏及相互作用。\n\n", ]; $artifact = dirname(__DIR__, 2) . '/app/artifacts/ai_concurrency/tcm_complete_v43r5_single_openai_20261024_0_openai.rejected.json'; if (is_file($artifact)) { $saved = json_decode((string) file_get_contents($artifact), true, 512, JSON_THROW_ON_ERROR); $expect(array_column($saved['rejections'], 'unit') === $units, 'Portable units exactly equal both original retained rejection units.'); } foreach ($units as $i => $unit) $check($unit, true, 'Exact retained unit ' . $i); foreach ([ '是否存在心悸?', '是否有固定刺痛?', '是否另有处方药、保健品或中药同用?', '是否另用处方药、保健品或中药?', '是否使用中药?', '有无心悸或固定刺痛?', '是否存在舌质红绛、脉滑?', '是否存在不宜运动的情况?', '适用前须核实:是否存在心悸?', '适用前须复核:是否有固定刺痛?是否存在舌苔厚腻?', ] as $question) $check($question, true, 'A terminated neutral question owns only its object: ' . $question); foreach ([ ['是否存在心悸。', 'NEGATIVE_TO_POSITIVE'], ['是否有固定刺痛', 'UNKNOWN_TO_POSITIVE'], ['是否存在舌苔厚腻。', 'TONGUE_FEATURE_NOT_IN_CURRENT_SOURCE'], ['适用前须核实:患者有心悸?', 'NEGATIVE_TO_POSITIVE'], ['适用前须核实:患者无固定刺痛?', 'UNKNOWN_TO_NEGATIVE'], ['是否存在患者有心悸?', 'NEGATIVE_TO_POSITIVE'], ['是否有患者无固定刺痛?', 'UNKNOWN_TO_NEGATIVE'], ['是否存在感染、无固定刺痛?', 'UNKNOWN_TO_NEGATIVE'], ['是否存在感染或有心悸?', 'NEGATIVE_TO_POSITIVE'], ] as [$text, $code]) $check($text, false, 'No absent terminator/header/embedded assertion exemption', $code); $assertions = [ ['患者有心悸', 'NEGATIVE_TO_POSITIVE'], ['患者无固定刺痛', 'UNKNOWN_TO_NEGATIVE'], ['患者有固定刺痛', 'UNKNOWN_TO_POSITIVE'], ['心悸未记录', 'NEGATIVE_TO_UNKNOWN'], ['口干未记录', 'POSITIVE_TO_UNKNOWN'], ['舌质红绛', 'TONGUE_FEATURE_NOT_IN_CURRENT_SOURCE'], ['脉滑', 'PULSE_FEATURE_NOT_IN_CURRENT_SOURCE'], ['无五心烦热', 'NEGATION_SCOPE_WIDENED'], ]; foreach ([',', ',', '且', '但', '并', '并且', '同时', '还', '又', '另'] as $joiner) { foreach ($assertions as [$assertion, $code]) { $check('适用前须核实:是否存在感染' . $joiner . $assertion . '?', false, 'An assertion cannot borrow a question mark through ' . $joiner . ': ' . $assertion, $code); } } foreach ([ ['是否存在感染患者有心悸?', 'NEGATIVE_TO_POSITIVE'], ['是否存在感染并诉称固定刺痛?', 'UNKNOWN_TO_POSITIVE'], ['是否存在感染并诊见舌质红绛?', 'TONGUE_FEATURE_NOT_IN_CURRENT_SOURCE'], ['是否存在感染且需复核,心悸未记录?', 'NEGATIVE_TO_UNKNOWN'], ['是否存在感染且需复核,口干未记录?', 'POSITIVE_TO_UNKNOWN'], ['患者有心悸,是否存在感染?', 'NEGATIVE_TO_POSITIVE'], [rtrim($units[0]) . '患者无固定刺痛。', 'UNKNOWN_TO_NEGATIVE'], [rtrim($units[0]) . '心悸未记录。', 'NEGATIVE_TO_UNKNOWN'], ] as [$text, $code]) $check($text, false, 'Actual assertions before/after/inside question fragments survive', $code); $quote = '“本次询问无已知药物过敏史”(s5)'; foreach (['s9', 's10'] as $id) { $sourceQuote = '“' . $ledger['sources'][$id]['quote'] . '”(' . $id . ')'; $check($sourceQuote . '仅说明既有记录的范围。', true, 'The full-source citation boundary applies to another recorded field: ' . $id); $check($sourceQuote . '仅说明既有记录的范围且患者有心悸。', false, 'Another recorded field cannot lend source scope to an actual assertion: ' . $id, 'NEGATIVE_TO_POSITIVE'); } foreach (['仅代表已知范围', '仅说明既有记录的范围', '新用药仍须复核'] as $comment) { $check($quote . $comment . '。', true, 'Source-bound whole quote ends before ordinary commentary.'); foreach ($assertions as [$assertion, $code]) { $check($quote . $comment . ',' . $assertion . '。', false, 'Quote commentary retains an actual clinical tail: ' . $assertion, $code); } } foreach (['且', '但', '并', '并且', '同时', '还', '又', '另'] as $joiner) { $check($quote . '仅代表已知范围' . $joiner . '患者有心悸。', false, 'No commentary/joiner consumes the actual assertion: ' . $joiner, 'NEGATIVE_TO_POSITIVE'); $check($quote . $joiner . '患者无固定刺痛。', false, 'A direct post-citation assertion stays separate: ' . $joiner, 'UNKNOWN_TO_NEGATIVE'); } foreach ([ ['“本次询问无已知药物过敏史”(s9)仅代表已知范围。', 'SOURCE_ANNOTATION_INVALID'], ['“本次询问无已知药物过敏史”(s9、s10)仅代表已知范围。', 'SOURCE_ANNOTATION_INVALID'], ['“本次询问无已知药物过敏史”(s999)仅代表已知范围。', 'SOURCE_ANNOTATION_INVALID'], ['“本次询问无已知药物过敏史”(2026-10-22,s5)仅代表已知范围。', 'SOURCE_ANNOTATION_INVALID'], ['“本次询问无已知药物过敏史”(2026-13-99,s5)仅代表已知范围。', 'SOURCE_ANNOTATION_INVALID'], ['“本次询问无药物过敏史”(s5)仅代表已知范围。', 'NEGATIVE_NOT_IN_SOURCE'], ['无药物过敏史。', 'NEGATION_SCOPE_WIDENED'], ['“无”(s5)药物过敏史。', 'NEGATION_SCOPE_WIDENED'], ['“本次询问无已知药物过敏史”(s5,患者无固定刺痛)仅代表已知范围。', 'UNKNOWN_TO_NEGATIVE'], ['患者有心悸,' . $quote . '仅代表已知范围。', 'NEGATIVE_TO_POSITIVE'], [$quote . '仅代表已知范围且口干未记录。', 'POSITIVE_TO_UNKNOWN'], [$quote . '仅代表已知范围并心悸未记录。', 'NEGATIVE_TO_UNKNOWN'], [$quote . '仅代表已知范围舌质红绛。', 'TONGUE_FEATURE_NOT_IN_CURRENT_SOURCE'], [$quote . '新用药仍须复核舌质红绛。', 'TONGUE_FEATURE_NOT_IN_CURRENT_SOURCE'], [str_replace('(s5)', '(s9)', rtrim($units[1])), 'SOURCE_ANNOTATION_INVALID'], [str_replace('(s5)', '(2026-10-22,s5)', rtrim($units[1])), 'SOURCE_ANNOTATION_INVALID'], ] as [$text, $code]) $check($text, false, 'A citation boundary requires its own intact source proof', $code); // Offset-sensitive source/date proof still works after an earlier inserted boundary. $check($quote . '仅代表已知范围;“本次面诊记录:舌质淡红,舌面少津,苔少;无黄腻苔、紫暗或瘀斑。”(2026-10-23,s9)。', true, 'Internal boundary insertion precedes quote offsets for every later source.'); $check($quote . '仅代表已知范围;“本次面诊记录:舌质淡红,舌面少津,苔少;无黄腻苔、紫暗或瘀斑。”(2026-10-22,s9)。', false, 'Later source date proof cannot drift after the inserted boundary.', 'SOURCE_ANNOTATION_INVALID'); // v43r6: source locators carry no clinical polarity; 合并 is a compound, not a list joiner. $r6Units = [ "血:面色少华、脉细可供血虚鉴别(s3、s10),但舌质仅淡红,且头晕、唇甲色泽等资料未记录,现有资料不足以确定血虚;固定刺痛相关情况见s2,血瘀不立。\n\n", "适用前须核实:是否存在影响黄芪、防风、白术使用的个体禁忌、近期感染、肝肾功能变化,及与全部在用药、保健品的相互作用?妊娠问题不适用于本例男性;其他合并用药未记录。\n\n", ]; $r6Artifact = dirname(__DIR__, 2) . '/app/artifacts/ai_concurrency/tcm_complete_v43r6_single_openai_20261024_0_openai.rejected.json'; if (is_file($r6Artifact)) { $saved = json_decode((string) file_get_contents($r6Artifact), true, 512, JSON_THROW_ON_ERROR); $expect(array_column($saved['rejections'], 'unit') === $r6Units, 'Both r6 portable units equal the retained original bytes.'); } foreach ($r6Units as $i => $unit) $check($unit, true, 'Exact retained r6 unit ' . $i); foreach ([ '固定刺痛相关情况见s2。', '口干资料见s2。', '心悸记录参见s2。', '固定刺痛、心悸相关情况见s2。', '舌象结果见s9。', '脉象记录见s10。', '2026-10-23固定刺痛相关情况见s2。', '其他合并用药未记录。', '其他合并用药、保健品未记录。', '固定刺痛相关情况见s2,其他合并用药未记录。', '是否另有合并用药?', ] as $text) $check($text, true, 'A proved locator or additional-data gap asserts no new patient finding: ' . $text); foreach ([ ['固定刺痛相关情况见s999。', 'SOURCE_ANNOTATION_INVALID'], ['固定刺痛相关情况见s9。', 'SOURCE_ANNOTATION_INVALID'], ['固定刺痛、头晕相关情况见s2。', 'SOURCE_ANNOTATION_INVALID'], ['舌象结果见s10。', 'SOURCE_ANNOTATION_INVALID'], ['2026-10-22固定刺痛相关情况见s2。', 'SOURCE_ANNOTATION_INVALID'], ['历史固定刺痛相关情况见s2。', 'SOURCE_ANNOTATION_INVALID'], ['患者有固定刺痛相关情况见s2。', 'UNKNOWN_TO_POSITIVE'], ['患者无固定刺痛相关情况见s2。', 'UNKNOWN_TO_NEGATIVE'], ['诉称固定刺痛相关情况见s2。', 'UNKNOWN_TO_POSITIVE'], ['固定刺痛相关情况见s2,须复核,心悸未记录。', 'NEGATIVE_TO_UNKNOWN'], ['固定刺痛相关情况见s2,其他合并用药、口干未记录。', 'POSITIVE_TO_UNKNOWN'], ['其他合并用药、心悸未记录。', 'NEGATIVE_TO_UNKNOWN'], ['其他合并用药未记录而舌质红绛。', 'TONGUE_FEATURE_NOT_IN_CURRENT_SOURCE'], ['其他合并用药未记录并诉称固定刺痛。', 'UNKNOWN_TO_POSITIVE'], ['其他合并用药未记录且合并有心悸。', 'NEGATIVE_TO_POSITIVE'], ['肾系定位因自述固定刺痛未记录而待定。', 'UNKNOWN_TO_POSITIVE'], ['肾系定位因自述固定刺痛未记录而仍待定。', 'UNKNOWN_TO_POSITIVE'], ['二甲双胍未记录。', 'POSITIVE_TO_UNKNOWN'], ['胰岛素未记录。', 'NEGATIVE_TO_UNKNOWN'], ['用药未记录。', 'POSITIVE_TO_UNKNOWN'], ] as [$text, $code]) $check($text, false, 'Reference proof and recorded clinical/medication states stay enforced', $code); foreach ([',', '且', '但', '并', '并且', '同时', '还', '又', '另'] as $joiner) { foreach ($assertions as [$assertion, $code]) { $check('固定刺痛相关情况见s2' . $joiner . $assertion . '。', false, 'A locator cannot swallow a following assertion through ' . $joiner, $code); $check('其他合并用药未记录' . $joiner . $assertion . '。', false, 'The additional-data gap cannot swallow a following assertion through ' . $joiner, $code); } } $recordedAdditional = Guard::build(['id' => 2, 'diagnosis_date' => '2026-10-23', 'current_medications' => '二甲双胍。现用的其他合并用药为阿卡波糖。未使用胰岛素。']); foreach ([['其他合并用药未记录。', 'POSITIVE_TO_UNKNOWN'], ['合并用药未记录。', 'POSITIVE_TO_UNKNOWN'], ['阿卡波糖未记录。', 'POSITIVE_TO_UNKNOWN'], ['胰岛素未记录。', 'NEGATIVE_TO_UNKNOWN']] as [$text, $code]) { $check($text, false, 'Actually recorded additional medication cannot become unknown', $code, $recordedAdditional); } $compoundFinding = Guard::build(['id' => 3, 'diagnosis_date' => '2026-10-23', 'present_illness' => '本次合并心悸。']); $check('心悸未记录。', false, 'The compound correction also retains a recorded non-medication finding', 'POSITIVE_TO_UNKNOWN', $compoundFinding); // v43r8: an anchored pending list and a complete future precondition are operators, // rather than new present patient observations. No topic/formula list is consulted. $r8Units = [ 'tcm_complete_v43r8_single_openai_20261024_0_openai' => "适用前须核实:是否存在明显五心烦热、潮热盗汗等表现的近期变化;是否存在畏寒肢冷等寒象变化;是否有咽干、干咳、纳差腹胀及长期便溏等补充证据?\n\n", 'tcm_complete_v43r8_supplement_ooc2_20261024_0_openai' => "适用前须核实:是否另有未记录的处方药、非处方药、保健品或草药;是否存在吞咽困难、胃肠不耐受或近期血糖异常波动;是否有拟用药材或辅料过敏;是否出现感染、脱水或肝肾指标变化。\n\n", 'tcm_complete_v43r8_supplement_ooc2_20261024_1_openai' => "适用条件:医师复诊确认当前仍以乏力、活动后气短、自汗、口干少津、弱脉为核心,且不存在更需优先处理的感染、心肺事件、脱水或明显血糖失控时,方可考虑该方向。\n\n", ]; foreach ($r8Units as $stem => $unit) { $artifact = dirname(__DIR__, 2) . '/app/artifacts/ai_concurrency/' . $stem . '.rejected.json'; if (is_file($artifact)) { $saved = json_decode((string) file_get_contents($artifact), true, 512, JSON_THROW_ON_ERROR); $expect(array_column($saved['rejections'], 'unit') === [$unit], 'Portable r8 unit equals the exact retained bytes: ' . $stem); } $check($unit, true, 'Exact r8 pending-question/precondition unit: ' . $stem); } foreach ([';', ';'] as $separator) foreach (['?', '?', '。', ''] as $ending) { $check('适用前须核实:是否存在心悸' . $separator . '是否有固定刺痛' . $separator . '是否出现舌质红绛' . $ending, true, 'Only consecutive same-line neutral questions inherit the pending operator.'); } foreach ([ '适用前须核实:是否存在心悸。', '适用前需复核:是否另有未记录的处方药、非处方药。', '适用前应确认:是否存在药物不耐受。', '是否存在胃肠不耐受?', '适用前须核实:是否有未采集的体征;是否可见舌质红绛。', '适用条件:医师复诊确认有心悸时,方可考虑该方向。', '适用条件:复诊确认存在固定刺痛时,才可评估该方向。', '适用条件:医生在复诊时核实无固定刺痛时,方可讨论后续方案。', '适用条件:医师复诊确认不存在感染且有口干时,方可考虑该方向。', '适用条件:医师复诊确认舌质红绛、脉滑时,方可考虑该方向。', '适用条件:医师复诊确认无五心烦热时,方可考虑该方向。', '适用条件:医师复诊确认当前仍以固定刺痛为核心时,方可考虑该方向。', ] as $text) $check($text, true, 'A prospective question/condition does not assert current polarity: ' . $text); foreach ([ ['是否存在心悸;是否有固定刺痛。', 'NEGATIVE_TO_POSITIVE'], ["适用前须核实:是否有心悸;\n是否存在固定刺痛。", 'UNKNOWN_TO_POSITIVE'], ['适用前须核实:患者有心悸;是否有固定刺痛。', 'NEGATIVE_TO_POSITIVE'], ['适用前须核实:是否存在感染不伴固定刺痛。', 'UNKNOWN_TO_NEGATIVE'], ['适用前须核实:是否存在感染;患者有固定刺痛。', 'UNKNOWN_TO_POSITIVE'], ['适用前须核实:是否存在感染;目前无固定刺痛。', 'UNKNOWN_TO_NEGATIVE'], ['适用前须核实:是否存在感染;心悸未记录。', 'NEGATIVE_TO_UNKNOWN'], ['适用条件:医师复诊确认患者已存在固定刺痛时,方可考虑该方向。', 'UNKNOWN_TO_POSITIVE'], ['适用条件:医师复诊确认患者已无固定刺痛时,方可考虑该方向。', 'UNKNOWN_TO_NEGATIVE'], ['适用条件:医师复诊确认目前有心悸时,方可考虑该方向。', 'NEGATIVE_TO_POSITIVE'], ['适用条件:医师复诊确认当前有心悸时,方可考虑该方向。', 'NEGATIVE_TO_POSITIVE'], ['适用条件:医师复诊确认本次有心悸时,方可考虑该方向。', 'NEGATIVE_TO_POSITIVE'], ['适用条件:医师复诊确认目前无五心烦热时,方可考虑该方向。', 'NEGATION_SCOPE_WIDENED'], ['适用条件:医师复诊确认口干未记录时,方可考虑该方向。', 'POSITIVE_TO_UNKNOWN'], ['适用条件:医师复诊确认心悸未记录时,方可考虑该方向。', 'NEGATIVE_TO_UNKNOWN'], ['适用条件:医师复诊确认诊见舌质红绛时,方可考虑该方向。', 'TONGUE_FEATURE_NOT_IN_CURRENT_SOURCE'], ['适用条件:医师复诊确认条件满足时,方可考虑患者有心悸。', 'NEGATIVE_TO_POSITIVE'], ['适用条件:医师复诊确认条件满足时,方可考虑心悸未记录。', 'NEGATIVE_TO_UNKNOWN'], ['适用条件:医师复诊确认条件满足时,方可考虑舌质红绛。', 'TONGUE_FEATURE_NOT_IN_CURRENT_SOURCE'], ['适用条件:医师确认存在固定刺痛时,方可考虑该方向。', 'UNKNOWN_TO_POSITIVE'], ['适用条件:医师复诊确认有心悸。', 'NEGATIVE_TO_POSITIVE'], ['患者有固定刺痛,适用条件:医师复诊确认条件满足时,方可考虑该方向。', 'UNKNOWN_TO_POSITIVE'], ['适用条件:医师复诊确认条件满足时,方可考虑该方向,须复核,心悸未记录。', 'NEGATIVE_TO_UNKNOWN'], ['适用前须核实:是否存在感染(s999);是否出现心悸。', 'SOURCE_ANNOTATION_INVALID'], ['适用条件:医师复诊确认条件满足(s999)时,方可考虑该方向。', 'SOURCE_ANNOTATION_INVALID'], ] as [$text, $code]) $check($text, false, 'Pending/conditional operators cannot swallow actual assertions or bad proof', $code); $futurePrefix = '适用条件:医师复诊确认存在固定刺痛且无五心烦热时,方可考虑该方向'; foreach ([',', '且', '但', '并', '并且', '同时', '还', '又', '另'] as $joiner) { foreach ($assertions as [$assertion, $code]) { $check('适用前须核实:是否存在感染' . $joiner . $assertion . ';是否出现心悸。', false, 'A pending semicolon list retains the assertion before it continues: ' . $joiner, $code); $check($futurePrefix . $joiner . $assertion . '。', false, 'An only-then decision retains the actual following assertion: ' . $joiner, $code); } } // v43r9: UNKNOWN 的 is attributive only at a bounded nominal-item start. $r9Unit = "适用前须核实:是否存在感染、痰多胸闷、腹胀纳差等可能不宜收敛或滋腻的情况;是否有肝肾功能后续变化及其他未记录的药物过敏。\n\n"; $r9Artifact = dirname(__DIR__, 2) . '/app/artifacts/ai_concurrency/tcm_complete_v43r9_single_openai_20261024_0_openai.rejected.json'; if (is_file($r9Artifact)) { $saved = json_decode((string) file_get_contents($r9Artifact), true, 512, JSON_THROW_ON_ERROR); $expect($saved['rejections'][1]['unit'] === $r9Unit, 'The r9 second retained unit is tested without rewriting any byte.'); } $check($r9Unit, true, 'Exact retained r9 nested attributive-question unit.'); foreach (['尚未记录', '尚未评估', '未曾采集', '未填写', '未询问', '未检查', '未明确', '不详', '未知', '不清楚', '待核实', '待补充', '待补采', '待补', '待采集', '待查'] as $state) { foreach ([ $state . '的药物过敏及肝肾功能变化', '肝肾功能变化及其他' . $state . '的药物过敏', '用药变更或者' . $state . '的其他禁忌', '其余' . $state . '的体征以及饮水变化', '饮水变化、额外' . $state . '的体征', ] as $object) $check('适用前须核实:是否有' . $object . '。', true, 'Attributive record state remains a question across unrelated object/order variants: ' . $state); } foreach ([ ['药物过敏未记录。', 'NEGATIVE_TO_UNKNOWN'], ['药物过敏不详。', 'NEGATIVE_TO_UNKNOWN'], ['药物过敏未知。', 'NEGATIVE_TO_UNKNOWN'], ['药物过敏不清楚。', 'NEGATIVE_TO_UNKNOWN'], ['适用前须核实:是否有药物过敏未记录。', 'NEGATIVE_TO_POSITIVE'], ['适用前须核实:是否有未知的药物过敏未记录。', 'NEGATIVE_TO_UNKNOWN'], ['是否有未知的药物过敏。', 'NEGATIVE_TO_UNKNOWN'], ['适用前须核实:是否有肝肾变化及其他未记录的药物过敏;药物过敏未记录。', 'NEGATIVE_TO_UNKNOWN'], ['适用前须核实:是否有其他未知的禁忌(s999)。', 'SOURCE_ANNOTATION_INVALID'], ['适用前须核实:是否有其他未记录的资料,患者已存在固定刺痛。', 'UNKNOWN_TO_POSITIVE'], ['适用前须核实:是否有其他未记录的资料,目前无固定刺痛。', 'UNKNOWN_TO_NEGATIVE'], ['适用前须核实:是否有其他未记录的资料,本次诊见舌质红绛。', 'TONGUE_FEATURE_NOT_IN_CURRENT_SOURCE'], ] as [$text, $code]) $check($text, false, 'An attributive question does not waive postfix erasure or current assertions', $code); foreach ([',', '且', '但', '并', '并且', '同时', '还', '又', '另'] as $joiner) { foreach ($assertions as [$assertion, $code]) { $check('适用前须核实:是否有肝肾变化及其他未记录的药物过敏' . $joiner . $assertion . '。', false, 'A nested attributive modifier cannot consume an actual adjacent assertion: ' . $joiner, $code); } $check('适用前须核实:是否有未知的药物过敏' . $joiner . '药物过敏未记录。', false, 'A known allergy record stays known after every assertion joiner: ' . $joiner, 'NEGATIVE_TO_UNKNOWN'); } // v43r10: inference labels, source-opening/suffix quotes and post-subject questions. $r10Units = [ ['tcm_complete_v43r10_supplement_ooc2_20261024_0_openai', 0, "本例支持点:气虚方面见乏力、动则气短、低声、脉细弱无力;津少方面见口干少量频饮、舌少津苔少;卫外不固线索为白天自汗并活动后加重(s1、s2、s3、s9、s10、s29)。\n\n"], ['tcm_complete_v43r10_supplement_ooc2_20261024_0_openai', 1, "本次询问“无已知药物过敏史”(s5),不等于排除首次反应。治疗期间监测口干、汗出、气短、体力、饮水与尿量变化,并复查空腹及餐后血糖、HbA1c、肝肾功能;异常加重及时就医。\n\n"], ['tcm_complete_v43r10_supplement_ooc2_20261024_1_openai', 1, "适用前须核实:是否存在甘草、人参类或五味子相关既往不耐受;是否另服处方药、非处方药、保健品或中药;是否存在影响选方的肝肾功能变化;汗出与气短是否出现新的严重程度或诱因。\n\n"], ]; foreach ($r10Units as [$stem, $index, $unit]) { $artifact = dirname(__DIR__, 2) . '/app/artifacts/ai_concurrency/' . $stem . '.rejected.json'; if (is_file($artifact)) { $saved = json_decode((string) file_get_contents($artifact), true, 512, JSON_THROW_ON_ERROR); $expect($saved['rejections'][$index]['unit'] === $unit, 'Exact r10 retained unit: ' . $stem . ':' . $index); } $check($unit, true, 'Exact r10 grammatical-role unit: ' . $stem . ':' . $index); } foreach ([ '卫外不固线索为白天自汗并活动后加重。', '津液不足依据为口干少量频饮。', '气机不畅证据是乏力。', '本次询问“无已知药物过敏史”(s5),不等于排除首次反应。', '本次询问“无已知药物过敏史”(2026-10-23,s5),仅代表已知范围。', '适用前须核实:是否另服中药;汗出与气短是否出现新的严重程度或诱因。', '适用前须核实:固定刺痛是否存在;饮水与尿量是否出现新变化。', '汗出与气短是否出现新的严重程度或诱因?', '固定刺痛是否存在?', ] as $text) $check($text, true, 'The bounded role retains the independent clinical claims.'); foreach ([ ['卫外不固线索为无自汗。', 'POSITIVE_TO_NEGATIVE'], ['无自汗,津液不足依据为口干。', 'POSITIVE_TO_NEGATIVE'], ['固定刺痛不足线索为口干。', 'UNKNOWN_TO_POSITIVE'], ['舌质红绛不润线索为口干。', 'TONGUE_FEATURE_NOT_IN_CURRENT_SOURCE'], ['卫外不固线索为患者有固定刺痛。', 'UNKNOWN_TO_POSITIVE'], ['津液不足依据为患者无固定刺痛。', 'UNKNOWN_TO_NEGATIVE'], ['津液不足线索为口干未记录。', 'POSITIVE_TO_UNKNOWN'], ['本次询问“无药物过敏史”(s5),不等于排除首次反应。', 'NEGATIVE_NOT_IN_SOURCE'], ['本次询问“无已知药物过敏史”(s9),不等于排除首次反应。', 'SOURCE_ANNOTATION_INVALID'], ['本次询问“无已知药物过敏史”(s999),不等于排除首次反应。', 'SOURCE_ANNOTATION_INVALID'], ['本次询问“无已知药物过敏史”(2026-10-22,s5),不等于排除首次反应。', 'SOURCE_ANNOTATION_INVALID'], ['本次询问“无已知药物过敏史”(s5),药物过敏未记录。', 'NEGATIVE_TO_UNKNOWN'], ['患者有固定刺痛,汗出与气短是否出现新的严重程度或诱因?', 'UNKNOWN_TO_POSITIVE'], ['适用前须核实:目前舌质红绛是否出现新变化。', 'TONGUE_FEATURE_NOT_IN_CURRENT_SOURCE'], ['适用前须核实:患者已无固定刺痛是否出现新变化。', 'UNKNOWN_TO_NEGATIVE'], ['适用前须核实:诉称固定刺痛是否出现新变化。', 'UNKNOWN_TO_POSITIVE'], ['汗出与气短是否出现感染。', 'NEGATIVE_TO_POSITIVE'], ] as [$text, $code]) $check($text, false, 'Inference/quote/question roles cannot hide actual state or bad proof', $code); foreach ([',', '且', '但', '并', '并且', '同时', '还', '又', '另'] as $joiner) { foreach ($assertions as [$assertion, $code]) { $check('卫外不固线索为口干' . $joiner . $assertion . '。', false, 'An inference label does not change an actual clue/tail: ' . $joiner, $code); $check('本次询问“无已知药物过敏史”(s5),不等于排除首次反应' . $joiner . $assertion . '。', false, 'Source-bound quote commentary retains its actual tail: ' . $joiner, $code); $check('适用前须核实:汗出与气短是否出现新变化' . $joiner . $assertion . '。', false, 'Post-subject questions retain their actual tail: ' . $joiner, $code); } } // v43r12: a prospective prepositional object owns its question predicate. $r11Unit = "六、中药方药及复核监测\n\n主方方向可考虑生脉散方向,仅供医师复诊辨证后决定,不构成可执行处方,不列新增剂量、频次、疗程、合方或加味。\n\n支持点:气虚证据见s1、s3、s10,津亏证据见s2、s9,自汗另有2026-10-22追踪s29。三组证据与益气、生津、敛汗目标相应,并非依据糖尿病病名选方。\n\n反证与局限:相关限制依据回指s2、s9、s10,不能反向证明本方适用。肺脾定位尚不充分,口渴与血糖波动是否同步、汗量及近期体重变化未记录,故须以复诊主次为准。\n\n适用条件:复诊仍以气虚为主、津亏为兼,且舌脉与本次记录基本一致时,方可考虑。若口渴、汗出或活动耐量变化,或出现新的舌脉资料,应重新辨证。\n\n方义:人参类益气,麦冬类养阴生津,五味子类敛汗生津,共同对应第五节的主次治法;此处仅说明组方结构,不确定药材品种、炮制规格及处方量。\n\n适用前须核实:是否另用处方药、非处方药、保健品或中药?是否对拟用各药材有过敏或不耐受?是否存在影响出汗、口渴及气短判断的疾病或药物因素?\n\n"; $r11Artifact = dirname(__DIR__, 2) . '/app/artifacts/ai_concurrency/tcm_complete_v43r11_single_openai_20261024_0_openai.rejected.json'; if (is_file($r11Artifact)) { $saved = json_decode((string) file_get_contents($r11Artifact), true, 512, JSON_THROW_ON_ERROR); $expect($saved['rejections'][1]['unit'] === $r11Unit, 'The complete r11 second retained unit is tested byte-for-byte.'); } $check($r11Unit, true, 'The exact retained r11 unit no longer asserts a prospective allergy.'); foreach (['对', '针对', '对于'] as $preposition) { foreach (['拟用', '拟选', '拟选用', '拟采用', '拟使用', '待选', '待用', '待选用', '候选', '计划使用', '准备采用'] as $prospective) { foreach (['有', '存在', '出现', '可见'] as $predicate) { foreach (['过敏或不耐受', '心悸', '固定刺痛', '舌质红绛、脉滑', '不耐受'] as $property) { $check('是否' . $preposition . $prospective . '方案' . $predicate . $property . '?', true, 'Only the bounded prospective operator owns this pending predicate.'); } } } } foreach ([ '适用前须核实:是否对拟用各药材有过敏或不耐受。', '适用前须复核:是否另用中药;是否针对待选材料存在不耐受;是否对准备采用方案有心悸。', '是否对于候选材料出现过敏?是否对拟用方案可见固定刺痛?', ] as $text) $check($text, true, 'A bounded pending context permits neutral prospective objects.'); foreach ([ ['是否对拟用方案有过敏。', 'NEGATIVE_TO_POSITIVE'], ['是否针对待选材料有心悸', 'NEGATIVE_TO_POSITIVE'], ['适用前须核实:是否对已用方案有药物过敏?', 'NEGATIVE_TO_POSITIVE'], ['适用前须核实:是否对当前方案有心悸?', 'NEGATIVE_TO_POSITIVE'], ['是否对拟用目前方案有药物过敏?', 'NEGATIVE_TO_POSITIVE'], ['是否对拟用患者已有心悸的方案有过敏?', 'NEGATIVE_TO_POSITIVE'], ['是否对拟用诉称固定刺痛的方案有过敏?', 'UNKNOWN_TO_POSITIVE'], ['是否对拟用药物过敏未记录的方案有心悸?', 'NEGATIVE_TO_UNKNOWN'], ['是否对拟用方案有药物过敏未记录?', 'NEGATIVE_TO_UNKNOWN'], ['是否对拟用方案有口干未记录?', 'POSITIVE_TO_UNKNOWN'], ['是否对拟用方案有患者无固定刺痛?', 'UNKNOWN_TO_NEGATIVE'], ['是否对拟用方案有过敏或有心悸?', 'NEGATIVE_TO_POSITIVE'], ['是否对拟用方案有过敏因已出现心悸?', 'NEGATIVE_TO_POSITIVE'], ['是否对拟用方案有过敏,患者目前已有心悸?', 'NEGATIVE_TO_POSITIVE'], ['是否对拟用方案有过敏,已诊见舌质红绛?', 'TONGUE_FEATURE_NOT_IN_CURRENT_SOURCE'], ['是否对拟用方案有不伴心悸?', 'NEGATION_SCOPE_WIDENED'], ['是否对拟用方案无固定刺痛?', 'UNKNOWN_TO_NEGATIVE'], ['是否对拟用方案有无五心烦热?', 'NEGATION_SCOPE_WIDENED'], ['是否对拟用方案有过敏(s999)?', 'SOURCE_ANNOTATION_INVALID'], ['是否对拟用方案有过敏(2026-10-22,s5)?', 'SOURCE_ANNOTATION_INVALID'], ['对拟用方案有药物过敏。', 'NEGATIVE_TO_POSITIVE'], ['患者目前对拟用方案有药物过敏。', 'NEGATIVE_TO_POSITIVE'], ['适用前须核实:是否对拟用方案有过敏?药物过敏未记录。', 'NEGATIVE_TO_UNKNOWN'], ] as [$text, $code]) $check($text, false, 'Prospective objects do not waive observations, erasure, qualifier or proof errors.', $code); foreach ([',', ',', '且', '但', '并', '并且', '同时', '还', '又', '另'] as $joiner) { foreach ($assertions as [$assertion, $code]) { foreach (['是否对拟用方案有过敏或不耐受', '适用前须核实:是否针对待选材料存在不耐受'] as $question) { $check($question . $joiner . $assertion . '?', false, 'A prospective object cannot absorb a joined actual assertion: ' . $joiner, $code); } } } // v43r13: a neutral post-subject question can lead into a bounded contingency. $r12Tail = "适用前须核实:汗出是否伴胸部不适、心悸、晕厥、持续喘促或意识改变;如有,应优先按急症路径评估。\n\n"; $r12Unit = "六、中药方药及复核监测\n\n主方方向:可由医师评估生脉散方向。原文“动则气短、自汗、口干”(s1)对应益气、生津、敛汗三项目标;细弱无力脉及舌面少津(s9、s10)进一步支持该方向,优先次序与第五节一致。\n\n方义:方中益气部分着重改善神疲、活动耐量及气短;养津部分针对口干、少量频饮和舌面少津;敛汗部分针对自汗、活动后加重。其作用覆盖当前核心目标,但不能据方义反推本例已成立阴虚或肺脾气虚。\n\n反证与局限:寒热未定,脏腑定位仅偏肺;汗出程度缺少客观量化,口干也可能受血糖影响。相关阴性限制见s2、s9、s10,但只能缩小鉴别,不能证明本方必然适用。若复核出现新的实邪、湿热或急性代谢异常证据,应重新辨证,而非机械使用。\n\n适用条件:复诊四诊仍以气虚表现为主,口干少津持续,且出汗、气短不能由低血糖、感染、心肺事件或其他急症解释;服药后能够监测血糖、汗出、口渴、活动耐量及胃肠反应。条件未满足前不得形成可直接执行处方。\n\n适用前须核实:是否存在近期体重明显变化、进食减少、低血糖样发作或血糖剧烈波动?\n\n适用前须核实:是否另用保健品、中草药或其他处方药,以及是否曾对拟用方中成分发生不良反应?\n\n" . $r12Tail; $r12Artifact = dirname(__DIR__, 2) . '/app/artifacts/ai_concurrency/tcm_complete_v43r12_supplement_ooc2_20261024_0_openai.rejected.json'; if (is_file($r12Artifact)) { $saved = json_decode((string) file_get_contents($r12Artifact), true, 512, JSON_THROW_ON_ERROR); $expect($saved['rejections'][0]['unit'] === $r12Unit, 'The exact complete r12 first retained unit is unchanged.'); } $check($r12Unit, true, 'The entire retained r12 unit passes Guard without rewriting.'); $check($r12Tail, true, 'The isolated actual post-subject question and contingency are prospective.'); foreach (['汗出', '口渴', '固定刺痛'] as $subject) { foreach (['伴', '伴有'] as $predicate) { foreach (['心悸', '固定刺痛', '舌质红绛、脉滑'] as $object) { foreach ([';', ';', '?'] as $separator) { foreach (['如有', '若有', '如果有'] as $contingency) { foreach (['应优先评估', '请及时按应急路径评估'] as $action) { $check('适用前须核实:' . $subject . '是否' . $predicate . $object . $separator . $contingency . ',' . $action . '。', true, 'A neutral question and immediate conditional action assert no patient finding.'); } } } } } } foreach ([ ['汗出是否伴心悸。', 'NEGATIVE_TO_POSITIVE'], ['汗出是否伴固定刺痛;如有,应优先评估。', 'UNKNOWN_TO_POSITIVE'], ['适用前须核实:患者现有心悸是否伴固定刺痛;如有,应评估。', 'NEGATIVE_TO_POSITIVE'], ['适用前须核实:已确定心悸是否伴固定刺痛;如有,应评估。', 'UNKNOWN_TO_POSITIVE'], ['适用前须核实:已确定有心悸是否伴固定刺痛;如有,应评估。', 'NEGATIVE_TO_POSITIVE'], ['适用前须核实:完成检查舌质红绛是否伴心悸;如有,应评估。', 'TONGUE_FEATURE_NOT_IN_CURRENT_SOURCE'], ['适用前须核实:目前舌质红绛是否伴心悸;如有,应评估。', 'TONGUE_FEATURE_NOT_IN_CURRENT_SOURCE'], ['适用前须核实:诉称固定刺痛是否伴心悸;如有,应评估。', 'UNKNOWN_TO_POSITIVE'], ['适用前须核实:汗出是否伴患者有心悸;如有,应评估。', 'NEGATIVE_TO_POSITIVE'], ['适用前须核实:汗出是否伴口干未记录;如有,应评估。', 'POSITIVE_TO_UNKNOWN'], ['适用前须核实:汗出是否伴心悸未记录;如有,应评估。', 'NEGATIVE_TO_UNKNOWN'], ['适用前须核实:汗出是否伴药物过敏未记录;如有,应评估。', 'NEGATIVE_TO_UNKNOWN'], ['适用前须核实:汗出是否伴舌象未记录;如有,应评估。', 'RECORDED_TO_UNKNOWN'], ['适用前须核实:汗出是否伴无五心烦热;如有,应评估。', 'NEGATION_SCOPE_WIDENED'], ['适用前须核实:汗出是否伴心悸(s999);如有,应评估。', 'SOURCE_ANNOTATION_INVALID'], ['适用前须核实:汗出是否伴心悸(2026-10-22,s5);如有,应评估。', 'SOURCE_ANNOTATION_INVALID'], ['适用前须核实:汗出是否伴心悸;如有,现有心悸。', 'NEGATIVE_TO_POSITIVE'], ['适用前须核实:汗出是否伴心悸;如有,应评估,患者有固定刺痛。', 'UNKNOWN_TO_POSITIVE'], ['适用前须核实:汗出是否伴心悸?若有,应评估且患者无固定刺痛。', 'UNKNOWN_TO_NEGATIVE'], ['适用前须核实:汗出是否伴心悸;如果有,应复核,心悸未记录。', 'NEGATIVE_TO_UNKNOWN'], ] as [$text, $code]) $check($text, false, 'Question/contingency roles cannot hide actual or erased facts.', $code); foreach ([',', ',', '且', '但', '并', '并且', '同时', '还', '又', '另'] as $joiner) { foreach ($assertions as [$assertion, $code]) { foreach (['如有', '若有', '如果有'] as $contingency) { $check('适用前须核实:汗出是否伴心悸;' . $contingency . ',应优先评估' . $joiner . $assertion . '。', false, 'A conditional action cannot lend its modal role to an actual tail: ' . $joiner, $code); $check('适用前须核实:汗出是否伴心悸;' . $contingency . $joiner . $assertion . '。', false, 'A contingency operator cannot absorb its actual adjoining assertion: ' . $joiner, $code); } $check('适用前须核实:汗出是否伴心悸' . $joiner . $assertion . ';如有,应评估。', false, 'A post-subject question retains its actual adjoining assertion: ' . $joiner, $code); } } // r13 integration regression: occurrence questions and their own linked inquiry. $r30Portable = "适用前须核实:复核自汗与气短是否持续呈活动后加重,有无新发感染、胸闷胸痛、心悸、晕厥或气促加剧;补采完整饮水量、尿量、体重变化、腹诊及腰膝耳鸣等资料。若出现急性发热、明显实热、痰湿壅盛或其他实邪表现,应重新辨证,不能机械沿用本方向。\n\n"; $r30Artifact = dirname(__DIR__, 2) . '/app/artifacts/ai_concurrency/tcm_complete_v30_isolated_c4_20261023_3_openai.rejected.json'; if (is_file($r30Artifact)) { $saved = json_decode((string) file_get_contents($r30Artifact), true, 512, JSON_THROW_ON_ERROR); $expect($saved['rejections'][0]['unit'] === $r30Portable, 'The portable v30 regression reproduces its entire retained unit exactly.'); } $check($r30Portable, true, 'The original complete v30 review role remains accepted.'); foreach (['汗出', '口干', '固定刺痛'] as $subject) { foreach (['持续', '反复发作', '持续呈活动后加重', '加重', '缓解', '减轻', '复发', '发病', '发生', '持续3个月'] as $predicate) { foreach (['有无心悸或固定刺痛', '是否伴心悸', '是否存在舌质红绛、脉滑'] as $linked) { $check('适用前须核实:复核' . $subject . '是否' . $predicate . ',' . $linked . '。', true, 'Whether governs its occurrence predicate and each linked question has its own operator.'); } } } $occurrenceReview = '适用前须核实:复核自汗与气短是否持续呈活动后加重,有无新发感染、心悸或气促加剧'; foreach ([ ['适用前须核实:患者有心悸是否持续,有无固定刺痛。', 'NEGATIVE_TO_POSITIVE'], ['适用前须核实:目前舌质红绛是否加重,有无心悸。', 'TONGUE_FEATURE_NOT_IN_CURRENT_SOURCE'], ['适用前须核实:已完成诊见舌质红绛是否加重,有无心悸。', 'TONGUE_FEATURE_NOT_IN_CURRENT_SOURCE'], ['适用前须核实:诉称固定刺痛是否持续,有无心悸。', 'UNKNOWN_TO_POSITIVE'], ['适用前须核实:汗出是否口干未记录,有无心悸。', 'POSITIVE_TO_UNKNOWN'], ['适用前须核实:汗出是否心悸未记录,有无固定刺痛。', 'NEGATIVE_TO_UNKNOWN'], ['适用前须核实:汗出是否无五心烦热,有无心悸。', 'NEGATION_SCOPE_WIDENED'], ['适用前须核实:汗出是否持续,有心悸,有无固定刺痛。', 'NEGATIVE_TO_POSITIVE'], ['适用前须核实:汗出是否持续,口干未记录,有无固定刺痛。', 'POSITIVE_TO_UNKNOWN'], ['适用前须核实:汗出是否持续,是否口干未记录。', 'POSITIVE_TO_UNKNOWN'], ['适用前须核实:汗出是否持续,是否心悸未记录。', 'NEGATIVE_TO_UNKNOWN'], ['适用前须核实:汗出是否持续,有无口干未记录。', 'POSITIVE_TO_NEGATIVE'], ['适用前须核实:汗出是否持续,有无心悸未记录。', 'NEGATION_SCOPE_WIDENED'], ['适用前须核实:汗出是否持续,是否舌象未记录。', 'RECORDED_TO_UNKNOWN'], ['适用前须核实:汗出是否持续,无五心烦热,有无固定刺痛。', 'NEGATION_SCOPE_WIDENED'], ['适用前须核实:汗出是否持续,有无心悸(s999)。', 'SOURCE_ANNOTATION_INVALID'], ['适用前须核实:汗出是否持续,有无心悸(2026-10-22,s5)。', 'SOURCE_ANNOTATION_INVALID'], ] as [$text, $code]) $check($text, false, 'An occurrence query and linked inquiry cannot hide a finding, erasure or invalid proof.', $code); foreach ([',', ',', '且', '但', '并', '并且', '同时', '还', '又', '另'] as $joiner) { foreach ($assertions as [$assertion, $code]) { $check($occurrenceReview . $joiner . $assertion . '。', false, 'Linked questions retain their actual or erased tail: ' . $joiner, $code); $check('适用前须核实:复核汗出是否持续呈活动后加重' . $joiner . $assertion . ',有无心悸。', false, 'An assertion between independent questions stays checked: ' . $joiner, $code); } } // r14: polarity entailment retains degree scope; prospective risk roles stay local. $r14Units = [ ['tcm_complete_v43r13_single_openai_20261024_0_openai', 0, "表里:症状逐渐出现且已持续3个月,并涉及汗出、饮水、二便等整体功能变化(s1、s2),倾向里证。表证所需的独立阳性证据不足,现有资料不足以确定表里兼夹。\n\n"], ['tcm_complete_v43r13_supplement_ooc2_20261024_1_openai', 0, "表里:病程3个月、症状逐渐出现,并涉及汗出、口渴、二便等内在功能表现(s1、s2),结合细弱无力脉(s10),偏里证;表证阳性依据不足,阴性限制见s2、s10。\n\n"], ['tcm_complete_v43r13_supplement_ooc2_20261024_1_openai', 1, "表里:症状持续3个月且逐渐出现,口干、汗出及二便改变均属内在功能失调(s1、s2),故偏里;表证所需的本次阳性证据不足,首节相关阴性仅用于限制判断。\n\n"], ['tcm_complete_v43r13_openai_deadline_20261024_openai_next_full90_openai', 0, "六、中药方药及复核监测\n\n主方方向限一个:生脉散方向,仅供医师在补齐安全条件后辨证复核,不构成可直接执行处方。本例同时存在气虚、津少与自汗证据(s1—s3、s9、s10),其益气、生津、敛汗三项方义可分别对应第五节目标,且以益气、生津为主,敛汗为辅。\n\n方义复核:人参类承担补气,针对神疲、动则气短、低声、脉弱;麦冬类侧重养津,对应口干、少量频饮、舌面少津;五味子类取收敛之意,对应白天自汗及活动后加重。这里只说明方义,不新增剂量、频次、疗程或加味。\n\n反证与局限:寒热总纲未定,阴虚尚未确立,故不能把本方向解释为治疗阴虚火旺;肺脏定位仅为倾向,亦不能据方名反向确认肺气虚。食欲一般且大便稍干(s2),消化耐受性仍需观察。\n\n适用条件:复诊时气虚、津少和自汗仍同时存在,且口干并非短期脱水、急性高血糖或其他可纠正因素所致;若症状性质改变、出现急性病情或辨证转实,应重新评估,不沿用本方向。\n\n适用前须核实:是否存在未记录的中药、保健品或其他处方药使用?\n\n"], ['tcm_complete_v43r13_openai_deadline_20261024_openai_next_full90_openai', 1, "六、中药方药及复核监测\n\n主方方向限一个:生脉散方向。本例气虚、津少、自汗证据并存(s1—s3、s9、s10),其益气、生津、敛汗方义依次对应第五节目标,不作气阴两虚或肺气虚的反向确诊。\n\n方义上,补气针对神疲、动则气短、语声低微及脉弱;养津针对口干少量频饮、舌面少津;收敛仅对应白天及活动后自汗。不得据此自行确定药味、剂量、频次或疗程。\n\n反证与局限:寒热、阴阳及脏腑定位尚未充分确定,不能将本方向解释为滋阴清热或温阳;全天饮水量、口干昼夜差异及汗出量未记录。若核心表现复诊时不再并存,应撤回该方向并重新辨证。\n\n适用前须核实:是否存在未记录的其他处方药、保健品或近期中药暴露?\n\n适用前须核实:是否存在吞咽困难、胃肠耐受问题,或拟用具体药物的既往不良反应?\n\n当前用药原文为2026-10-23“二甲双胍0.5g,每日2次,既有医嘱;未使用胰岛素或中药。”(s6);不得擅停换。既有过敏询问见s5,拟用中药仍须逐味核实。\n\n"], ]; foreach ($r14Units as [$stem, $index, $unit]) { $path = dirname(__DIR__, 2) . '/app/artifacts/ai_concurrency/' . $stem . '.rejected.json'; if (is_file($path)) { $saved = json_decode((string) file_get_contents($path), true, 512, JSON_THROW_ON_ERROR); $expect($saved['rejections'][$index]['unit'] === $unit, 'Exact r14 retained whole unit: ' . $stem . ':' . $index); } $check($unit, true, 'The whole retained r14 unit preserves source and grammatical roles.'); } foreach ([['明显', '腹胀', true], ['剧烈', '腹痛', true], ['大量', '皮疹', true], ['频繁', '心悸', true], ['持续', '咳嗽', true], ['淋漓', '汗出', false], ['严重', '腹泻', false]] as [$degree, $finding, $prefix]) { $qualifiedFinding = $prefix ? $degree . $finding : $finding . $degree; $qualifiedLedger = Guard::build(['diagnosis_date' => '2026-10-23', 'present_illness' => '无' . $qualifiedFinding . ';未记录固定刺痛。']); $broadLedger = Guard::build(['diagnosis_date' => '2026-10-23', 'present_illness' => '无' . $finding . ';未记录固定刺痛。']); $check('患者有' . $finding . '。', true, 'Qualified absence does not deny every degree of the broader finding.', '', $qualifiedLedger); $check('患者有' . $qualifiedFinding . '。', false, 'The complete qualified absence remains binding.', 'NEGATIVE_TO_POSITIVE', $qualifiedLedger); $check('患者有' . $qualifiedFinding . '。', false, 'A broad negative still forbids a specific qualified positive.', 'NEGATIVE_TO_POSITIVE', $broadLedger); $check('无' . $finding . '。', false, 'Degree removal from a patient negative still widens scope.', 'NEGATION_SCOPE_WIDENED', $qualifiedLedger); $check('患者有固定刺痛。', false, 'Unknown observations are independent of degree entailment.', 'UNKNOWN_TO_POSITIVE', $qualifiedLedger); } $compoundNegative = Guard::build(['diagnosis_date' => '2026-10-23', 'present_illness' => '无胸痛心悸、咳嗽咯痰。']); foreach (['胸痛', '心悸', '咳嗽', '咯痰'] as $finding) $check('患者有' . $finding . '。', false, 'A compound component is not a removable degree modifier.', 'NEGATIVE_TO_POSITIVE', $compoundNegative); foreach (['未记录', '尚未采集', '未提供', '待核实', '不详', '未知'] as $gap) { foreach (['中药、保健品或其他处方药', '设备或外用制剂'] as $target) { foreach (['使用', '服用', '应用', '启用'] as $action) { foreach (['', '情况', '记录', '史'] as $noun) $check('适用前须核实:是否存在' . $gap . '的' . $target . $action . $noun . '?', true, 'A record-gap adjective and nominal action remain a pending inquiry.'); } } } foreach (['拟用', '待选', '计划使用', '准备采用'] as $planned) { foreach (['逐项核实', '逐味复核', '进一步审核'] as $review) { $check('既有过敏询问见s5,' . $planned . '方案仍须' . $review . '。', true, 'A postfix prospective review action does not inherit prior presence.'); foreach ($assertions as [$assertion, $code]) $check('既有过敏询问见s5,' . $planned . '方案仍须' . $review . '且' . $assertion . '。', false, 'Prospective review owns only its target, retaining the actual tail.', $code); } } foreach ([ ['患者有汗出淋漓。', 'NEGATIVE_TO_POSITIVE'], ['患者有心悸。', 'NEGATIVE_TO_POSITIVE'], ['舌质紫暗。', 'TONGUE_FEATURE_NOT_IN_CURRENT_SOURCE'], ['舌苔黄腻。', 'TONGUE_FEATURE_NOT_IN_CURRENT_SOURCE'], ['舌苔少而非腻。', 'NEGATION_SCOPE_WIDENED'], ['无五心烦热。', 'NEGATION_SCOPE_WIDENED'], ['适用前须核实:是否存在未记录的患者使用中药?', 'NEGATIVE_TO_POSITIVE'], ['适用前须核实:是否存在未记录的中药已使用?', 'NEGATIVE_TO_POSITIVE'], ['中药未记录。', 'NEGATIVE_TO_UNKNOWN'], ['二甲双胍0.5g未记录。', 'POSITIVE_TO_UNKNOWN'], ['适用前须核实:是否存在未记录的中药使用;患者使用中药。', 'NEGATIVE_TO_POSITIVE'], ['既有过敏询问见s5,拟用正在使用中药仍须逐味核实。', 'NEGATIVE_TO_POSITIVE'], ['既有过敏询问见s5,拟用方案已确认患者有心悸。', 'NEGATIVE_TO_POSITIVE'], ] as [$text, $code]) $check($text, false, 'Degree/query/review handling retains actual violations.', $code); foreach ([',', ',', '且', '但', '并', '并且', '同时', '还', '又', '另'] as $joiner) { foreach ($assertions as [$assertion, $code]) { $check('适用前须核实:是否存在未记录的中药使用' . $joiner . $assertion . '?', false, 'The nominal action query retains its actual adjoining assertion.', $code); $check('既有过敏询问见s5,拟用中药仍须逐味核实' . $joiner . $assertion . '。', false, 'The prospective postfix action retains its actual adjoining assertion.', $code); } } $medicationReference = '当前用药原文为2026-10-23“' . $ledger['sources']['s6']['quote'] . '”(s6);不得擅停换。既有过敏询问见s5,拟用中药仍须逐味核实。'; $check($medicationReference, true, 'The exact known medication baseline remains explicit beside the pending action.'); $check(str_replace('(s6)', '(s999)', $medicationReference), false, 'A future action cannot repair an invalid baseline citation.', 'SOURCE_ANNOTATION_INVALID'); $check(str_replace('(s6)', '(2026-10-22,s6)', $medicationReference), false, 'A future action cannot repair the wrong baseline date.', 'SOURCE_ANNOTATION_INVALID'); // r14 medication aliases: complete field references use source provenance. $medicationAliasMinimal = Guard::build(['id' => 1, 'diagnosis_date' => '2026-10-23', 'current_medications' => '甲药1g,每日2次;未使用乙药。']); $medicationAliases = ['用药', '现用药', '在用药', '当前用药', '目前用药', '本次用药', '现用药物', '在用药物', '用药情况', '用药记录', '用药信息', '现用药情况', '当前用药情况', '目前用药情况', '用药史', '药物使用史']; foreach ($medicationAliases as $alias) { foreach (['未记录', '未采集', '不详'] as $gap) { foreach (['', '拟用中药仍须逐味核实,', '既有过敏询问见s5,拟用中药仍须逐味核实,'] as $prefix) { $check($prefix . $alias . $gap . '。', false, 'A complete known medication field cannot be erased through an alias.', 'RECORDED_TO_UNKNOWN'); } $check($alias . $gap . '。', false, 'A minimal medication field needs no unrelated source topic to retain its state.', 'RECORDED_TO_UNKNOWN', $medicationAliasMinimal); } } foreach (['现用药剂量未记录。', '当前用药频次未采集。', '用药起始日期未记录。', '首次用药时间未记录。', '既往用药不良反应史未采集。', '其他合并用药未记录。', '额外用药未记录。', '拟用中药剂量未记录。'] as $specific) { $check($specific, true, 'A specific uncollected medication detail is not a whole-field alias.'); $check('拟用中药仍须逐味核实,' . $specific, true, 'A planned-review prefix retains a genuine specific data gap.'); } foreach ([null, '', '无', '未记录'] as $missing) { $missingMedication = Guard::build(['diagnosis_date' => '2026-10-23', 'current_medications' => $missing]); foreach ($medicationAliases as $alias) { $check($alias . '未记录。', true, 'Missing medication fields remain unknown.', '', $missingMedication); $check('拟用中药仍须逐味核实,' . $alias . '未记录。', true, 'Pending review does not fill an unknown medication field.', '', $missingMedication); foreach ([$alias . ':无。', '无' . $alias . '。', '拟用中药仍须逐味核实,' . $alias . ':无。'] as $denial) { $check($denial, false, 'An unknown medication alias cannot become a patient denial.', 'UNKNOWN_TO_NEGATIVE', $missingMedication); } } $check('当前用药:甲药。', false, 'A missing field cannot acquire an invented active drug.', 'UNKNOWN_TO_POSITIVE', $missingMedication); $check("现用药:\n\n未记录。", true, 'An unknown medication field stays unknown across a heading boundary.', '', $missingMedication); foreach ([',', '且', '但', '并', '同时'] as $joiner) { $check('无现用药' . $joiner . '另有资料。', false, 'An adjacent statement cannot hide an unknown whole-field denial.', 'UNKNOWN_TO_NEGATIVE', $missingMedication); } } $oldMedicationRows = ['required_content' => [['key' => 'prescriptions', 'value' => [['id' => 9, 'prescription_date' => '2020-05-02', 'prescription_name' => '旧方甲', 'usage_instruction' => '已停用']]]]]; $oldOnlyMedication = Guard::build(['id' => 1, 'diagnosis_date' => '2026-10-23'], $oldMedicationRows); $mixedMedication = Guard::build(['id' => 1, 'diagnosis_date' => '2026-10-23', 'current_medications' => '甲药1g;未使用乙药。'], $oldMedicationRows); $check('现用药未记录。', true, 'An old prescription does not create a known current medication field.', '', $oldOnlyMedication); $check('当前用药:无。', false, 'An old prescription cannot turn missing current medications into a denial.', 'UNKNOWN_TO_NEGATIVE', $oldOnlyMedication); $check('用药史未记录。', false, 'An unqualified whole medication history cannot erase a known old record.', 'RECORDED_TO_UNKNOWN', $oldOnlyMedication); $check('2019年既往用药史未记录。', true, 'A specific uncollected historical period is distinct from a known later record.', '', $oldOnlyMedication); $check('既往用药史未采集。', true, 'A current medication field alone does not fill specific historical collection.', '', $medicationAliasMinimal); $check('现用药未记录。', false, 'Historical material cannot erase the known current medication field.', 'RECORDED_TO_UNKNOWN', $mixedMedication); $check('现用药未记录(2020-05-02,s2)。', false, 'An old citation cannot change an explicitly current field reference.', 'RECORDED_TO_UNKNOWN', $mixedMedication); $check('现用药:无。', false, 'A recorded active medication field cannot become a blanket denial.', 'POSITIVE_TO_NEGATIVE', $medicationAliasMinimal); $partialNegativeMedication = Guard::build(['diagnosis_date' => '2026-10-23', 'current_medications' => '未使用乙药。']); $wholeNegativeMedication = Guard::build(['diagnosis_date' => '2026-10-23', 'current_medications' => '未使用任何药物。']); $check('现用药:无。', false, 'Non-use of one item cannot establish an empty whole medication field.', 'NEGATION_SCOPE_WIDENED', $partialNegativeMedication); $check('现用药:无。', true, 'An explicit whole-current-field denial retains its original negative meaning.', '', $wholeNegativeMedication); $check('用药史:无。', false, 'Current non-use does not establish a whole lifetime medication-history denial.', 'NEGATION_SCOPE_WIDENED', $wholeNegativeMedication); foreach ([',', ',', '且', '但', '并', '并且', '同时', '还', '又', '另'] as $joiner) { foreach ($assertions as [$assertion, $code]) { $check('现用药剂量未记录' . $joiner . $assertion . '。', false, 'A specific data gap does not exempt adjacent actual assertions.', $code); $check('拟用中药仍须逐味核实' . $joiner . '现用药未记录。', false, 'An action joiner cannot exempt whole-field medication erasure.', 'RECORDED_TO_UNKNOWN'); } } if (!$guardOnly) { foreach ([$ledger, $medicationAliasMinimal, $oldOnlyMedication] as $caseIndex => $caseLedger) { foreach ([1, 2, 7, 31] as $size) { $header = "现用药:\n\n"; $tail = ($caseIndex < 2 ? '未记录' : '无') . "。\n\n"; $visible = ''; $stream = new TcmClinicalReportStream($caseLedger, static function (string $delta) use (&$visible): bool { $visible .= $delta; return true; }); $pushed = true; foreach (str_split($header . $tail, $size) as $chunk) if (!$stream->push($chunk)) { $pushed = false; break; } $expect(!$pushed && $stream->rejected(), 'A carried medication field rejects the invalid value, chunk ' . $size); $expect($visible === $header && $stream->acceptedContent() === $header, 'The accepted heading is exact and zero rejected value bytes become visible, chunk ' . $size); } } } // Medication note provenance: a same-time explicit field value is still a source. $noteMedicationData = ['id' => 900000044, 'diagnosis_date' => strtotime('2026-10-23'), 'current_medications' => null, 'remark' => '当前用药:使用甲药。']; $noteMedication = Guard::build($noteMedicationData); $expect($noteMedication['sources']['s1']['quote'] === $noteMedicationData['remark'] && $noteMedication['sources']['s1']['path'] === 'current.remark' && $noteMedication['sources']['s1']['scope'] === 'current' && $noteMedication['sources']['s1']['date'] === '2026-10-23', 'The exact root reproduction retains its original source ID/quote/scope/date.'); $check('当前用药:使用甲药。', true, 'Exact root reproduction: an explicit same-time remark remains valid.', '', $noteMedication); foreach (['remark', 'doctor_advice', 'present_illness'] as $noteField) { foreach (['使用甲药', '服用甲药', '甲药1g'] as $value) { $case = Guard::build(['id' => 1, 'diagnosis_date' => '2026-10-23', 'current_medications' => null, $noteField => '当前用药:' . $value . ',每日2次。']); $check('当前用药:' . $value . '。', true, 'An explicit actual field value can occur in another submitted source.', '', $case); foreach (['现用药未记录。', '当前用药未记录。', '用药史未记录。'] as $erasure) $check($erasure, false, 'A known labelled note cannot be erased by another whole-field alias.', 'RECORDED_TO_UNKNOWN', $case); } } $negativeMedicationNote = Guard::build(array_replace($noteMedicationData, ['remark' => '当前用药:未使用乙药。'])); $emptyMedicationNote = Guard::build(array_replace($noteMedicationData, ['remark' => '当前用药:无。'])); $check('当前用药:未使用乙药。', true, 'A bounded negative note value keeps its exact original meaning.', '', $negativeMedicationNote); $check('当前用药:使用乙药。', false, 'Explicit non-use in a labelled note still forbids use of that item.', 'NEGATIVE_TO_POSITIVE', $negativeMedicationNote); $check('当前用药:无。', false, 'A partial non-use note cannot establish a blanket denial.', 'NEGATION_SCOPE_WIDENED', $negativeMedicationNote); $check('现用药:无。', true, 'A labelled whole-field negative is an actual negative source.', '', $emptyMedicationNote); $check('当前用药未记录。', false, 'A labelled negative cannot become missing information.', 'RECORDED_TO_UNKNOWN', $emptyMedicationNote); foreach (['当前用药:未记录。', '当前用药:不详。', '当前用药:暂无。', '当前用药:未记录,患者有心悸。', '当前用药:未记录;患者有心悸。', '当前用药:未记录。使用甲药。', "当前用药:\n患者有心悸。", '当前用药:需核实甲药。', '当前用药:拟用甲药。', '当前用药:医生建议使用甲药。', '当前用药:可考虑使用甲药。', '医师建议复核当前用药。使用甲药。', '当前用药相关问题待核实。神疲口干。'] as $unknownNote) { $case = Guard::build(array_replace($noteMedicationData, ['remark' => $unknownNote])); $check('当前用药:使用甲药。', false, 'A missing/plan/mention/unrelated note does not establish an active medication field.', 'UNKNOWN_TO_POSITIVE', $case); $check('现用药未记录。', true, 'A label without an actual value remains unknown.', '', $case); $check('当前用药未记录。', true, 'A metadata heading is not a phantom positive medication fact.', '', $case); $check('当前用药:无。', false, 'Unknown note labels cannot become observed medication absence.', 'UNKNOWN_TO_NEGATIVE', $case); } foreach (['当前用药:既往使用甲药。', '当前用药:2026-10-22使用甲药。'] as $datedNote) { $case = Guard::build(array_replace($noteMedicationData, ['remark' => $datedNote])); $check('当前用药:使用甲药。', false, 'Historical or mismatched-time note content cannot prove current medication state.', 'UNKNOWN_TO_POSITIVE', $case); } $historyMedicationNote = Guard::build(['id' => 1, 'diagnosis_date' => '2026-10-23'], ['required_content' => [['key' => 'doctor_image_notes', 'value' => [['diagnosis_id' => 2, 'note_date' => '2020-05-02', 'content' => '当前用药:使用甲药。']]]]]); $check('当前用药:使用甲药。', false, 'A historical source with a current-looking label remains historical.', 'UNKNOWN_TO_POSITIVE', $historyMedicationNote); $check('现用药未记录。', true, 'An old labelled note does not fill the current field.', '', $historyMedicationNote); $check('用药史未记录。', false, 'A generic history label still retains a known historical note.', 'RECORDED_TO_UNKNOWN', $historyMedicationNote); $sameDayMedicationNote = Guard::build(['id' => 1, 'diagnosis_date' => '2026-10-23'], ['required_content' => [['key' => 'doctor_image_notes', 'value' => [['note_date' => '2026-10-23', 'content' => '当前用药:使用甲药。']]]]]); $check('当前用药:使用甲药。', true, 'An explicitly labelled same-day source remains eligible without changing its unspecified scope.', '', $sameDayMedicationNote); $expect($sameDayMedicationNote['sources']['s1']['scope'] === 'unspecified', 'Recognizing a note does not rewrite its stored scope.'); $wrongDayMedicationNote = Guard::build(['id' => 1, 'diagnosis_date' => '2026-10-23'], ['required_content' => [['key' => 'doctor_image_notes', 'value' => [['diagnosis_id' => 1, 'note_date' => '2026-10-22', 'content' => '当前用药:使用甲药。']]]]]); $check('当前用药:使用甲药。', false, 'A mismatched note date cannot prove current medication state even with a matching visit ID.', 'UNKNOWN_TO_POSITIVE', $wrongDayMedicationNote); $historyLabelOnly = Guard::build(array_replace($noteMedicationData, ['remark' => '用药史:使用甲药。'])); $check('当前用药:使用甲药。', false, 'A history-only label is not a current-field label.', 'UNKNOWN_TO_POSITIVE', $historyLabelOnly); $check('用药史未记录。', false, 'An actual history-labelled source remains recorded.', 'RECORDED_TO_UNKNOWN', $historyLabelOnly); $qualifiedMedicationNote = Guard::build(array_replace($noteMedicationData, ['remark' => '当前用药:未使用其他药物。'])); $check('当前用药:未使用其他药物。', true, 'The exact negative qualifier in a note stays usable.', '', $qualifiedMedicationNote); $check('当前用药:未使用药物。', false, 'A note-source qualifier cannot be widened into blanket non-use.', 'NEGATION_SCOPE_WIDENED', $qualifiedMedicationNote); $expect($qualifiedMedicationNote['sources']['s1']['quote'] === '当前用药:未使用其他药物。', 'Field recognition retains the full original qualified quote.'); $noteClinicalLedger = Guard::build(array_replace(CompleteTcmCase::current(), ['current_medications' => null, 'remark' => '当前用药:使用甲药。']), $submission); foreach ([',', '且', '但', '并', '同时'] as $joiner) { foreach ($assertions as [$assertion, $code]) { $check('当前用药:使用甲药' . $joiner . $assertion . '。', false, 'Actual statements adjacent to a known note value remain independently checked.', $code, $noteClinicalLedger); } $check('拟用中药仍须逐味核实' . $joiner . '现用药未记录。', false, 'A planned-role prefix cannot erase an explicitly labelled known source.', 'RECORDED_TO_UNKNOWN', $noteMedication); } // r15 record predicates and future causal criteria: retained model bytes are immutable. $r15RecordUnit = "寒热:口干、少津、苔少仅提示津液受损,不能单独成立热证;寒热相关限制见s2。舌质淡红且未记红绛,现有资料不足以确定寒或热。\n\n"; $r15CausalParagraph = "适用条件:复核后仍以气虚为主、津亏为兼,且汗出并非低血糖、感染、心肺疾病或药物因素所致;若口渴、多尿或乏力随血糖明显波动,应先评估代谢原因。\n\n"; $r15DeadlineUnit = "六、中药方药及复核监测\n\n主方方向限一个:生脉散方向,须由医师面诊审核后方可决定,不给出新增剂量、频次或疗程。本例取其益气、生津、敛汗的整体方义,与第五节治法次序一致。\n\n支持点:气短乏力、自汗、语声低微、脉细弱无力构成益气依据;口干少量频饮、舌面少津苔少构成生津依据;活动汗增支持兼顾敛汗(s1、s2、s3、s9、s10)。\n\n局限与反证:阴虚、虚热及明确肺脏证名均未充分成立;寒热限制信息见s2,舌脉限制信息见s9、s10。因此该方向只覆盖“气虚津亏、汗出”层面,不据此扩写阴虚方证。\n\n" . $r15CausalParagraph; foreach ([ ['tcm_complete_v43r14_single_openai_20261024_0_openai', 0, $r15RecordUnit], ['tcm_complete_v43r14_openai_deadline_20261024_openai_next_full90_openai', 1, $r15DeadlineUnit], ] as [$name, $index, $unit]) { $artifact = dirname(__DIR__, 2) . '/app/artifacts/ai_concurrency/' . $name . '.rejected.json'; if (is_file($artifact)) { $saved = json_decode((string) file_get_contents($artifact), true, 512, JSON_THROW_ON_ERROR); $expect($saved['rejections'][$index]['unit'] === $unit, 'r15 portable retained unit equals the original artifact byte for byte.'); } $expect(Guard::validate($ledger, $unit)['valid'], 'r15 exact whole retained unit has no old-Guard contradiction; this does not complete the report contract.'); } $check($r15RecordUnit, true, 'The exact retained record-predicate paragraph preserves its complete wire bytes.'); $check($r15CausalParagraph, true, 'The exact retained future-causal paragraph preserves its complete wire bytes.'); foreach (['舌质淡红且未记红绛。', '舌质淡红,未记裂纹。', '脉细弱,未记结代。', '未记固定刺痛。', '固定刺痛未记。', '未记载固定刺痛。'] as $text) { $check($text, true, 'An abbreviated record predicate expresses missing information, not observed absence.'); } foreach ([ ['口干未记。', 'POSITIVE_TO_UNKNOWN'], ['未记淡红。', 'POSITIVE_TO_UNKNOWN'], ['心悸未记。', 'NEGATIVE_TO_UNKNOWN'], ['未记紫暗。', 'NEGATIVE_TO_UNKNOWN'], ['未记舌象。', 'RECORDED_TO_UNKNOWN'], ['脉象未记。', 'RECORDED_TO_UNKNOWN'], ['现用药未记。', 'RECORDED_TO_UNKNOWN'], ['药物过敏史未记。', 'RECORDED_TO_UNKNOWN'], ['舌质淡红且未见红绛。', 'TONGUE_FEATURE_NOT_IN_CURRENT_SOURCE'], ['舌质红绛且未记裂纹。', 'TONGUE_FEATURE_NOT_IN_CURRENT_SOURCE'], ['舌质淡红且未记红绛(s999)。', 'SOURCE_ANNOTATION_INVALID'], ] as [$text, $code]) $check($text, false, 'An abbreviated record predicate cannot erase known values or create an observed denial.', $code); $recordPredicate = new ReflectionMethod(Guard::class, 'reportRecordPredicateProse'); foreach (['未记录固定刺痛', '未记得固定刺痛', '未记忆固定刺痛', '未记住固定刺痛', '未记清固定刺痛'] as $text) { $expect($recordPredicate->invoke(null, $text) === $text, 'A complete record predicate or distinct recollection verb is not rewritten.'); } $r15Generic = Guard::build(['diagnosis_date' => '2026-10-23', 'present_illness' => '无明显甲因素诱因。有乙症。未记录丙项。']); $r15CausalFrame = '适用条件:复核后仍以丁判断为主,且乙症并非甲因素或其他因素所致'; $check($r15CausalFrame . '。', true, 'A future causal criterion has the same role with unrelated subject/cause vocabulary.', '', $r15Generic); foreach (['复查', '复诊确认', '复诊复核', '重新核实'] as $review) { $check(str_replace('复核后', $review . '后', $r15CausalFrame) . '。', true, 'An explicit future reassessment operator scopes only its closed causal criterion.', '', $r15Generic); } foreach ([ '乙症并非甲因素或其他因素所致。', '适用条件:乙症并非甲因素或其他因素所致。', '适用条件:目前乙症并非甲因素或其他因素所致。', '适用条件:复核后仍以丁判断为主,且目前乙症并非甲因素或其他因素所致。', '适用条件:复核后仍以丁判断为主,且患者乙症并非甲因素或其他因素所致。', '适用条件:复核后仍以丁判断为主,且已经确认乙症并非甲因素或其他因素所致。', ] as $text) $check($text, false, 'A label, present subject or completed confirmation cannot borrow a future operator.', 'NEGATION_SCOPE_WIDENED', $r15Generic); $futureCriterion = new ReflectionMethod(Guard::class, 'futureCausalCriterionProse'); foreach ([ '适用条件:复核后仍以患者有固定刺痛为主,且汗出并非感染所致', '适用条件:复核后仍以舌质红绛为主,且汗出并非感染所致', '适用条件:复核后仍以气虚为主,且汗出并非患者有心悸所致', '适用条件:复核后仍以气虚为主,且汗出并非口干未记录所致', '适用条件:复核后仍以气虚为主,且舌质红绛并非感染所致', '适用条件:复核后仍以气虚为主,且汗出并非感染所致的现症', '适用条件:复核后仍以气虚为主,目前患者有心悸,且汗出并非感染所致', '适用条件:复核后仍以气虚为主,口干未记录,且汗出并非感染所致', ] as $text) $expect($futureCriterion->invoke(null, $text) === $text, 'An actual/unknown/examination assertion or incomplete causal complement is never removed as a future criterion.'); $r15Future = '适用条件:复核后仍以气虚为主、津亏为兼,且汗出并非低血糖、感染、心肺疾病或药物因素所致'; foreach ([',', ',', '且', '但', '并', '并且', '同时', '还', '又', '另'] as $joiner) { foreach ($assertions as [$assertion, $code]) { $check('舌质淡红且未记红绛' . $joiner . $assertion . '。', false, 'A record predicate cannot swallow any adjoining actual assertion.', $code); $check($r15Future . $joiner . $assertion . '。', false, 'A closed future causal relation cannot swallow any adjoining actual assertion.', $code); } $check($r15Future . $joiner . '固定刺痛不存在。', false, 'An adjacent postfix absence remains an actual assertion after a removed future relation.', 'UNKNOWN_TO_POSITIVE'); $check($r15Future . $joiner . '现用药未记录。', false, 'Future causal criteria cannot erase the known medication field.', 'RECORDED_TO_UNKNOWN'); } $check($r15Future . '(s999)。', false, 'Future role parsing cannot repair an invalid source ID.', 'SOURCE_ANNOTATION_INVALID'); $check($r15Future . ',“本次询问无已知药物过敏史”(2026-10-22,s5)。', false, 'Future role parsing cannot repair a mismatched source date.', 'SOURCE_ANNOTATION_INVALID'); $expect(hash('sha256', json_encode($ledger, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)) === '2ebbec0b558347bf4ee128dfb5a9e63fc4e355d58f7226486a2ae96098bd57f6', 'r15 leaves every actual fixture source ID, quote, field, date, role and ledger byte unchanged.'); // r16 assertion/reference boundaries: no diagnostic-term exceptions or changed source ledger. $r16SingleUnit = "主证可暂拟“气虚津少证,偏肺气不足、卫外不固”。“气虚、津少”分别由s1、s2、s3、s9、s10支持;“肺气不足、卫外不固”仅为功能定位倾向,主要依据气短、低声及自汗,尚缺更完整肺系资料。\n\n"; $r16C2Unit = "表里:自汗、活动后加重可涉及卫表不固,但病程三个月、神疲及脉按之无力更支持内在功能不足。缺少外感起病及表证阳性依据,故以内证倾向为主,表证现有资料不足以确定(s1、s2、s10)。\n\n"; $r16ReferenceParagraph = "现用药记录见s6;仅复核依从性、胃肠反应及血糖变化,不擅自停换二甲双胍。既有过敏询问见s5,但拟用中药及辅料的个体暴露仍须逐项核实。\n\n"; $r16SingleFinal = "六、中药方药及复核监测\n\n本次暂不列具体主方、药味或新剂量。虽有益气固表、生津的治疗方向,但脾肺定位尚未完全落实,寒热未定,且津少尚不足成立阴虚;直接套用益气固表方或益气养阴方,均可能超出本例证据。\n\n适用前须核实:是否存在反复易感、畏风及汗出后的冷热感,以复核卫表不固的范围?\n\n适用前须核实:是否有腹胀便溏、食后困倦等脾气不足特征,或咳嗽、声音持续低弱等肺系表现?\n\n适用前须核实:全天饮水量、口干昼夜差异及汗量是否可量化,近期体重变化与血糖波动是否同步?\n\n" . $r16ReferenceParagraph; foreach ([ ['tcm_complete_v43r15_single_openai_20261024_0_openai', 0, $r16SingleUnit], ['tcm_complete_v43r15_single_openai_20261024_0_openai', 1, $r16SingleFinal], ['tcm_complete_v43r15_supplement_ooc2_20261024_1_openai', 0, $r16C2Unit], ] as [$name, $index, $unit]) { $artifact = dirname(__DIR__, 2) . '/app/artifacts/ai_concurrency/' . $name . '.rejected.json'; if (is_file($artifact)) { $saved = json_decode((string) file_get_contents($artifact), true, 512, JSON_THROW_ON_ERROR); $expect($saved['rejections'][$index]['unit'] === $unit, 'r16 portable unit is exactly the retained original model output.'); } $expect(Guard::validate($ledger, $unit)['valid'], 'r16 whole retained unit clears only old-Guard lexical rejection, not final/clinical acceptance.'); } foreach ([$r16SingleUnit, $r16C2Unit, $r16ReferenceParagraph] as $unit) { $check($unit, true, 'r16 exact affected paragraph preserves all original bytes at every chunk size.'); } $r16Generic = Guard::build(['diagnosis_date' => '2026-10-23', 'present_illness' => '有甲项及乙项。无丙项及丁项。戊项未记录。']); foreach (['但', '但是', '然而', '不过'] as $adversative) { $check('甲类不定,' . $adversative . '甲项及乙项。', true, 'Adversative boundary restarts assertion polarity with unrelated vocabulary.', '', $r16Generic); $check('无丙项及丁项,' . $adversative . '甲项及乙项。', true, 'A genuine inherited negative list ends at the adversative operator.', '', $r16Generic); foreach ([['无甲项及乙项', 'POSITIVE_TO_NEGATIVE'], ['有丙项', 'NEGATIVE_TO_POSITIVE'], ['有戊项', 'UNKNOWN_TO_POSITIVE'], ['无戊项', 'UNKNOWN_TO_NEGATIVE'], ['甲项未记录', 'POSITIVE_TO_UNKNOWN'], ['丙项未记录', 'NEGATIVE_TO_UNKNOWN']] as [$assertion, $code]) { $check('甲类不定,' . $adversative . $assertion . '。', false, 'Every independent actual assertion retains its own polarity after an adversative.', $code, $r16Generic); } foreach ($assertions as [$assertion, $code]) { $check('卫外不固,' . $adversative . $assertion . '。', false, 'An inference label cannot shield an adjacent actual fixture assertion.', $code); $check('适用前须核实:是否有固定刺痛,' . $adversative . '须复核,' . $assertion . '。', false, 'An adversative boundary does not end the strict scope of an earlier question.', $code); } } foreach (['及', '以及', '、', '或', ','] as $listJoiner) { $check('无丙项' . $listJoiner . '丁项。', true, 'Genuine inherited negative lists retain their scope.', '', $r16Generic); $check('无甲项' . $listJoiner . '乙项。', false, 'A denied positive list remains a contradiction.', 'POSITIVE_TO_NEGATIVE', $r16Generic); } foreach (['主要依据', '具体依据', '直接依据', '主要根据', '具体根据', '直接根据'] as $intro) { $check('甲类不定,' . $intro . '甲项及乙项。', true, 'An affirmative evidence introducer starts an independent finding list.', '', $r16Generic); foreach ($assertions as [$assertion, $code]) { $check('卫外不固,' . $intro . $assertion . '。', false, 'The evidence introducer preserves negative/unknown/value and qualifier checks.', $code); } $check('卫外不固,' . $intro . '无口干及乏力。', false, 'An explicit denial after an evidence introducer keeps its true negative meaning.', 'POSITIVE_TO_NEGATIVE'); } foreach (['用药', '现用药', '在用药', '当前用药', '目前用药', '本次用药', '现用药物', '在用药物', '用药情况', '用药记录', '用药信息', '现用药情况', '当前用药情况', '目前用药情况'] as $alias) { $check($alias . '记录见s6。', true, 'A complete medication field alias can locate its actual schema source.'); $check($alias . '记录见s5。', false, 'A medication alias cannot bind an allergy source.', 'SOURCE_ANNOTATION_INVALID'); $check($alias . '记录见s23。', false, 'An incidental medication mention cannot supply medication-field provenance.', 'SOURCE_ANNOTATION_INVALID'); } foreach (['现用药记录见s999。', '现用药记录见s16。', '现用药记录见s30。', '2026-10-22现用药记录见s6。', '现用药记录见s6(2026-10-22,s6)。'] as $text) { $check($text, false, 'A medication field locator requires an existing same-time source with matching provenance.', 'SOURCE_ANNOTATION_INVALID'); } foreach (['拟用中药及辅料的个体暴露仍须逐项核实', '待选材料及辅料的相关暴露仍需逐项复核', '计划使用甲材料及乙材料的记录仍应审核'] as $plan) { $check('既有过敏询问见s5,但' . $plan . '。', true, 'A neutral planned nominal target with an extended data head retains its postfix review role.'); foreach ([',', ',', '且', '但', '并', '并且', '同时', '还', '又', '另'] as $joiner) { foreach ($assertions as [$assertion, $code]) { $check('既有过敏询问见s5,但' . $plan . $joiner . $assertion . '。', false, 'A planned target cannot swallow any actual assertion after a joiner.', $code); } $check('现用药记录见s6' . $joiner . '现用药未记录。', false, 'A neutral locator cannot erase the medication baseline in its adjoining prose.', 'RECORDED_TO_UNKNOWN'); } } foreach ([ ['但拟用目前已使用中药的个体暴露仍须逐项核实。', 'NEGATIVE_TO_POSITIVE'], ['但拟用患者有固定刺痛的资料仍须逐项核实。', 'UNKNOWN_TO_POSITIVE'], ['但拟用患者无固定刺痛的资料仍须逐项核实。', 'UNKNOWN_TO_NEGATIVE'], ['但拟用已诊见舌质红绛的资料仍须逐项核实。', 'TONGUE_FEATURE_NOT_IN_CURRENT_SOURCE'], ['但拟用中药的个体暴露仍须逐项核实,现用药未记录。', 'RECORDED_TO_UNKNOWN'], ] as [$text, $code]) $check($text, false, 'Current/completed/embedded predicates cannot become a neutral planned target.', $code); $check('**表里:偏里证。**症状已持续3个月并逐渐出现,以“神疲乏力”“轻微活动后气短”“白天自汗”为主;本次又记录“无恶寒发热”,脉“不浮”,现有证据更支持内在功能失调,而非急性表证。但这只反映本次接诊状态,后续若新出现恶寒、发热、咽痛等,仍须重新判断。', true, 'The original portable v15 full paragraph retains its exact source facts and future qualification.'); $check('舌脉未呈黄腻、滑、紧等所见,但不能仅凭这些否定完全排除夹实。', true, 'A complete negative exam list remains complete before an adversative commentary boundary.'); $check('舌脉未呈黄腻、滑、紧等所见,但不能仅凭这些否定完全排除夹实,患者有固定刺痛。', false, 'Commentary after a complete negative exam list cannot exempt an adjoining patient assertion.', 'UNKNOWN_TO_POSITIVE'); $expect(hash('sha256', json_encode($ledger, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)) === '2ebbec0b558347bf4ee128dfb5a9e63fc4e355d58f7226486a2ae96098bd57f6', 'r16 keeps every immutable ledger source ID, quote, field, date, role and schema byte unchanged.'); // r17 exact-atom triage: questions already pass; record locator and future trigger roles were missing. $r17RecordUnit = "血:面色少华可提示血分失养,但非特异;固定刺痛的资料状态见s2,舌质亦无足够血虚或血瘀阳性特征。血虚、血瘀均待定。\n\n"; $r17Questions = [ "适用前须核实:是否另有未记录的处方药、非处方药、保健品及肝肾疾病变化?是否存在吞咽困难、明显脱水或血糖急剧波动等需优先处理情况?\n\n", "适用前须核实:是否还在使用其他处方药、保健品或中草药?是否存在肝肾功能近期变化?是否出现低血糖样不适或胃肠反应?是否有拟用药既往不耐受?\n\n", ]; $r17Triage = [ "监测乏力、气短、自汗、口干、饮水及二便变化,并继续记录空腹与餐后血糖;若启用中药,复核血糖趋势、胃肠耐受及肝肾指标。出现意识异常、持续明显气短或其他急性变化时,应及时按西医急症路径评估。\n\n", "如医师采用该方向,应监测乏力、气短、汗出、口干、食欲、大便及血糖变化;症状加重或出现新的胸闷、心悸、意识改变等情况,应先行医学评估,不以中药替代必要检查。\n\n", ]; foreach (['tcm_complete_v43r16_supplement_ooc2_20261024_0_openai', 'tcm_complete_v43r16_openai_deadline_20261024_openai_next_full90_openai'] as $i => $name) { $artifact = dirname(__DIR__, 2) . '/app/artifacts/ai_concurrency/' . $name . '.rejected.json'; if (is_file($artifact)) { $saved = json_decode((string) file_get_contents($artifact), true, 512, JSON_THROW_ON_ERROR); if ($i === 0) $expect($saved['rejections'][0]['unit'] === $r17RecordUnit, 'The r17 portable record-state paragraph is byte-identical to its retained unit.'); $unit = $saved['rejections'][1]['unit']; $original = $unit; $expect(str_contains($unit, $r17Questions[$i]) && str_contains($unit, $r17Triage[$i]), 'The exact supplied question list and actual offending triage paragraph remain in the original whole unit.'); $expect(Guard::validate($ledger, $unit)['valid'] && $unit === $original, 'The exact whole section clears only old-Guard lexical errors, with no source/model/output substitution.'); } } foreach (array_merge([$r17RecordUnit], $r17Questions, $r17Triage) as $text) { $check($text, true, 'r17 retained paragraph passes lexical validation and, when enabled, exact wire checks.'); } foreach (['资料状态', '记录状态', '信息状态', '资料的状态', '记录的状态'] as $role) { $check('固定刺痛的' . $role . '见s2。', true, 'A neutral cited record-state locator does not assert its subject is present.'); $check('舌象的' . $role . '见s9。', true, 'A neutral named-exam locator points to its actual field without erasing its values.'); foreach ([',', ',', '且', '但', '并', '并且', '同时', '还', '又', '另'] as $joiner) { foreach ($assertions as [$assertion, $code]) { $check('固定刺痛的' . $role . '见s2' . $joiner . $assertion . '。', false, 'A record-state locator cannot consume an adjacent actual assertion.', $code); } $check('固定刺痛的' . $role . '见s2' . $joiner . '现用药未记录。', false, 'A locator cannot erase the current medication baseline.', 'RECORDED_TO_UNKNOWN'); } } foreach ([ ['固定刺痛的资料状态见s999。', 'SOURCE_ANNOTATION_INVALID'], ['固定刺痛的资料状态见s5。', 'SOURCE_ANNOTATION_INVALID'], ['2026-10-22固定刺痛的资料状态见s2。', 'SOURCE_ANNOTATION_INVALID'], ['固定刺痛的资料状态见s2(2026-10-22,s2)。', 'SOURCE_ANNOTATION_INVALID'], ['舌象的资料状态见s10。', 'SOURCE_ANNOTATION_INVALID'], ['患者有固定刺痛的资料状态见s2。', 'UNKNOWN_TO_POSITIVE'], ['患者无固定刺痛的资料状态见s2。', 'UNKNOWN_TO_NEGATIVE'], ['舌质红绛的资料状态见s9。', 'TONGUE_FEATURE_NOT_IN_CURRENT_SOURCE'], ] as [$text, $code]) $check($text, false, 'Locator syntax cannot manufacture source provenance, dates or actual clinical values.', $code); $r17Generic = Guard::build(['diagnosis_date' => '2026-10-23', 'present_illness' => '有甲项。无乙项。丙项未记录。']); foreach (['甲项', '乙项', '丙项'] as $subject) { $check($subject . '的记录状态见s1。', true, 'The same locator grammar preserves positive, negative and unknown record states with unrelated vocabulary.', '', $r17Generic); } foreach (['出现', '发生'] as $event) { foreach (['时', '等情况'] as $ending) { foreach (['应', '须', '需'] as $modal) { foreach (['评估', '就医', '检查', '处理'] as $action) { $check($event . '乙项' . $ending . ',' . $modal . '立即' . $action . '。', true, 'Only a closed event trigger linked to a mandatory future action is prospective.', '', $r17Generic); } } } } $check('事件恶化或发生新的乙项等情况,应先行评估。', true, 'A neutral event progression can introduce an alternative future occurrence.', '', $r17Generic); $check('出现固定刺痛时,应立即评估。', true, 'A future trigger does not turn unknown data into a current positive.'); $check('出现舌质红绛时,应立即评估。', true, 'An explicitly hypothetical trigger is distinct from a current examination value.'); foreach ([ ['出现意识异常时。', 'NEGATIVE_TO_POSITIVE'], ['出现意识异常,应立即评估。', 'NEGATIVE_TO_POSITIVE'], ['出现意识异常时,已经评估。', 'NEGATIVE_TO_POSITIVE'], ['患者已出现意识异常时,应立即评估。', 'NEGATIVE_TO_POSITIVE'], ['目前出现意识异常时,应立即评估。', 'NEGATIVE_TO_POSITIVE'], ['出现已经确认意识异常时,应立即评估。', 'NEGATIVE_TO_POSITIVE'], ['患者症状已加重或出现意识异常等情况,应立即评估。', 'NEGATIVE_TO_POSITIVE'], ['出现意识异常且患者有固定刺痛时,应立即评估。', 'UNKNOWN_TO_POSITIVE'], ['患者已出现舌质红绛时,应立即评估。', 'TONGUE_FEATURE_NOT_IN_CURRENT_SOURCE'], ] as [$text, $code]) $check($text, false, 'A current/completed/embedded assertion or incomplete future frame cannot borrow the event-trigger role.', $code); foreach ([',', ',', '且', '但', '并', '并且', '同时', '还', '又', '另'] as $joiner) { foreach ($assertions as [$assertion, $code]) { $check('出现意识异常时,应立即评估' . $joiner . $assertion . '。', false, 'The future action cannot hide a factual assertion adjoining its trigger.', $code); } $check('出现意识异常时,应立即评估' . $joiner . '现用药未记录。', false, 'A prospective trigger/action pair cannot erase the known medication field.', 'RECORDED_TO_UNKNOWN'); } $triggerRole = new ReflectionMethod(Guard::class, 'prospectiveTriggerProse'); foreach (['出现口干未记录时', '出现患者有固定刺痛时', '出现2026-10-22意识异常时', '出现诊见舌质红绛时', '出现曾经意识异常时'] as $text) { $expect($triggerRole->invoke(null, $text, ',应立即评估。') === $text, 'A record-state, attributed, dated or completed predicate is never removed as a neutral trigger.'); } $check('出现意识异常时(s999),应立即评估。', false, 'A future trigger cannot repair an invalid source ID.', 'SOURCE_ANNOTATION_INVALID'); $check('出现意识异常时(2026-10-22,s2),应立即评估。', false, 'A future trigger cannot repair a mismatched source date.', 'SOURCE_ANNOTATION_INVALID'); $expect(hash('sha256', json_encode($ledger, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)) === '2ebbec0b558347bf4ee128dfb5a9e63fc4e355d58f7226486a2ae96098bd57f6', 'r17 preserves every actual source ID, quote, field, scope, date and ledger schema byte.'); // r17 epistemic followup: a bounded absence of evidence is UNKNOWN, never a patient denial. $r17EpistemicUnit = "寒热:口干、少津、苔少及大便稍干(s2、s9)可提示津少,尚不足单独成立热证;寒象亦无本次独立阳性证据。寒热属性现有资料不足以确定。\n\n"; $artifact = dirname(__DIR__, 2) . '/app/artifacts/ai_concurrency/tcm_complete_v43r16_openai_deadline_20261024_openai_next_full90_openai.rejected.json'; if (is_file($artifact)) { $saved = json_decode((string) file_get_contents($artifact), true, 512, JSON_THROW_ON_ERROR); $expect($saved['rejections'][0]['unit'] === $r17EpistemicUnit, 'The epistemic followup uses the exact retained successor unit without substitution.'); } $check($r17EpistemicUnit, true, 'The complete retained epistemic paragraph passes old-Guard validation without output rewriting.'); $check('寒象亦无本次独立阳性证据。', true, 'The exact bounded epistemic clause is not an observed clinical absence.'); $epistemicGeneric = Guard::build(['diagnosis_date' => '2026-10-23', 'present_illness' => '有甲项。无乙项。丙项未记录。']); foreach (['', '亦', '也', '尚', '仍', '均', '并'] as $adverb) { foreach (['无', '没有', '未见'] as $negative) { foreach (['', '本次独立阳性', '明确可靠的', '现有充分相应'] as $modifier) { foreach (['证据', '依据'] as $noun) { $phrase = $adverb . $negative . $modifier . $noun . '。'; $check('丁诊断' . $phrase, true, 'A generic diagnostic noun plus closed evidential role asserts no patient absence.', '', $epistemicGeneric); $check('甲项' . $phrase, false, 'Evidence-role wording cannot erase a recorded positive.', 'POSITIVE_TO_UNKNOWN', $epistemicGeneric); $check('乙项' . $phrase, false, 'Evidence-role wording cannot erase a recorded negative.', 'NEGATIVE_TO_UNKNOWN', $epistemicGeneric); $check('丙项' . $phrase, true, 'A source-unknown subject stays unknown, without becoming an observed absence.', '', $epistemicGeneric); } } } } foreach ([ ['口干亦无本次独立阳性证据。', 'POSITIVE_TO_UNKNOWN'], ['心悸亦无本次独立阳性证据。', 'NEGATIVE_TO_UNKNOWN'], ['舌质淡红亦无本次独立阳性证据。', 'POSITIVE_TO_UNKNOWN'], ['舌质紫暗亦无本次独立阳性证据。', 'NEGATIVE_TO_UNKNOWN'], ['舌象亦无本次独立阳性证据。', 'RECORDED_TO_UNKNOWN'], ['脉象亦无本次独立阳性证据。', 'RECORDED_TO_UNKNOWN'], ['现用药亦无本次独立阳性证据。', 'RECORDED_TO_UNKNOWN'], ['寒象亦无恶寒、肢冷等证据。', 'NEGATION_SCOPE_WIDENED'], ['患者有心悸亦无本次独立阳性证据。', 'NEGATIVE_NOT_IN_SOURCE'], ['寒象亦无本次独立阳性证据(s999)。', 'SOURCE_ANNOTATION_INVALID'], ['寒象亦无本次独立阳性证据(2026-10-22,s2)。', 'SOURCE_ANNOTATION_INVALID'], ] as [$text, $code]) $check($text, false, 'Epistemic grammar preserves known facts, examination roles, qualifiers and source/date checks.', $code); $check('固定刺痛亦无本次独立阳性证据。', true, 'Explicitly unrecorded clinical information remains unknown.'); $check('“无咳嗽咯痰、胸痛心悸、浮肿或腹泻”(s2)。', true, 'An exact source-backed current negative list keeps its original clinical meaning.'); foreach ([',', ',', '且', '但', '并', '并且', '同时', '还', '又', '另'] as $joiner) { foreach ($assertions as [$assertion, $code]) { $check('寒象亦无本次独立阳性证据' . $joiner . $assertion . '。', false, 'A bounded evidence noun cannot consume an adjacent actual assertion.', $code); } $check('寒象亦无本次独立阳性证据' . $joiner . '现用药未记录。', false, 'A local epistemic statement cannot erase an adjoining known medication field.', 'RECORDED_TO_UNKNOWN'); } foreach (['望诊', '闻诊', '问诊', '切诊', '舌象', '脉象'] as $role) { foreach ($assertions as [$assertion, $code]) { $check($role . ':寒象亦无本次独立阳性证据且' . $assertion . '。', false, 'A report-role heading cannot turn evidence-role grammar into an examination/factual waiver.', $code); } } $epistemicRole = new ReflectionMethod(Guard::class, 'reportEvidenceGapProse'); foreach (['患者有心悸亦无本次独立阳性证据', '患者无固定刺痛', '寒象亦无恶寒、肢冷等证据', '舌质红绛且寒象无独立证据', '诊见舌质红绛无独立证据', '寒象无本次独立阳性证据支持患者无固定刺痛', '寒象无完整独立充分足够明确直接可靠相应阳性证据'] as $text) { $expect($epistemicRole->invoke(null, $text) === $text, 'Clinical predicates, adjacent assertions and non-closed evidence syntax are never removed by this grammar.'); } $unknownView = (new ReflectionMethod(Guard::class, 'reportClauses'))->invoke(null, $ledger, '固定刺痛亦无本次独立阳性证据'); $unknownParts = (new ReflectionMethod(Guard::class, 'parts'))->invoke(null, $unknownView[0]['text']); $expect(count($unknownParts) === 1 && $unknownParts[0]['state'] === 'unknown', 'The evidence-absence validation view is explicitly UNKNOWN, never a negative observation.'); $expect(hash('sha256', json_encode($ledger, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)) === '2ebbec0b558347bf4ee128dfb5a9e63fc4e355d58f7226486a2ae96098bd57f6', 'The epistemic followup leaves all actual source IDs, quotes, scopes, dates and schema bytes unchanged.'); if ($guardOnly) { $expect(!class_exists('app\\adminapi\\logic\\tcm\\DiagnosisAiLogic', false) && !class_exists(TcmClinicalReportStream::class, false) && !class_exists('app\\adminapi\\service\\TcmClinicalEvidenceContract', false), 'Guard-only verification does not load mutable peer classes.'); } echo "Clinical question/citation boundary: {$count} assertions passed (pure actual fixture; " . ($guardOnly ? 'Guard only; streaming deferred' : 'exact 1/2/7/31-byte streaming') . ").\n";