'b608f236c0b8877dbffb106ca8461596cf95ce911c060171cd20da31ca4a450c', 'TcmClinicalEvidenceContract' => 'c25e88fcc0633e957cf3a7aca4b417140b9ce9766e745c0d7b1bc2528aaaa509', ]; foreach ($snapshots as $class => $hash) { $source = file_get_contents($artifacts . ($class === 'TcmClinicalFactStateGuard' ? 'v43r25c_before_' : 'v43r25b_before_') . $class . '.php.snapshot'); $expect(hash('sha256', $source) === $hash, 'before snapshot exact identity ' . $class); if (hash('sha256', $source) !== $hash) throw new RuntimeException('Unverified before snapshot'); $source = preg_replace('/final class ' . $class . '\b/', 'final class IndependentBefore' . $class, $source, 1, $replaced); $expect($replaced === 1, 'before class loaded under distinct review-only name ' . $class); eval(substr($source, strlen('invoke(null, CompleteTcmCase::rows()); $submission = (new ReflectionMethod(PatientAiReportLogic::class, 'sanitizeSnapshotForUpstream'))->invoke(null, AiRequiredSource::submission($raw)); $ledger = Guard::build(CompleteTcmCase::current(), $submission); $index = Index::build($ledger); $sourceIdentity = hash('sha256', serialize($ledger)); $expect(serialize($ledger) === serialize($beforeGuard::build(CompleteTcmCase::current(), $submission)), 'before/current canonical source ledgers are byte identical'); $expect(hash_file('sha256', __DIR__ . '/fixtures/CompleteTcmCase.php') === 'b54c47ec11f6a99dc87bdda78629e18c8a6f37a64017e42a72db399c727cb1c6', 'immutable source fixture'); $makeHead = static function (array $inputIndex): string { $prefix = "一、四诊\n\n"; foreach (Index::ROLES as $label => $role) { $chosen = null; foreach ($inputIndex['sources'] as $source) { if ($source['scope'] !== 'current') continue; foreach ($source['spans'] as $span) if ($span['role'] === $role && $span['recorded']) { $chosen = [$source, $span]; break 2; } } if ($chosen === null) { $prefix .= $label . ($role === 'palpation' ? ":其他触诊未记录,脉象见独立脉象项。\n\n" : ":未记录\n\n"); } else { [$source, $span] = $chosen; $prefix .= $label . ':' . $source['date'] . '“' . trim($span['quote']) . '”(' . $source['id'] . ")。\n\n"; } } return $prefix . "二、八纲\n\n"; }; $head = $makeHead($index); $expect(Guard::validate($ledger, $head)['valid'] && Contract::validate($index, $head, true)['valid'], 'independent full six-role candidate prefix is valid'); $case = static function (string $name, string $unit, bool $valid, ?array $customLedger = null, bool $streaming = true) use ( $ledger, $index, $head, $makeHead, $beforeGuard, $beforeContract, $expect, &$observations): void { $input = $customLedger ?? $ledger; $inputIndex = $customLedger === null ? $index : Index::build($input); $inputHead = $customLedger === null ? $head : $makeHead($inputIndex); $expect(Guard::validate($input, $inputHead)['valid'] && Contract::validate($inputIndex, $inputHead, true)['valid'], $name . ': fixture prefix itself is valid'); $g = Guard::validate($input, $inputHead . $unit); $c = Contract::validate($inputIndex, $inputHead . $unit, true); $oldG = $beforeGuard::validate($input, $inputHead . $unit); $oldC = $beforeContract::validate($inputIndex, $inputHead . $unit, true); $actual = $g['valid'] && $c['valid']; $expect($actual === $valid, $name . ': composed expected ' . (int) $valid . ', got ' . json_encode([$g, $c])); $entry = ['label' => $name, 'unit_sha256' => hash('sha256', $unit), 'expected_composed_valid' => $valid, 'before_guard_valid' => $oldG['valid'], 'before_contract_valid' => $oldC['valid'], 'before_guard_issues' => $oldG['issues'], 'before_contract_issues' => $oldC['issues'], 'after_guard_valid' => $g['valid'], 'after_contract_valid' => $c['valid'], 'after_guard_issues' => $g['issues'], 'after_contract_issues' => $c['issues']]; if ($streaming) foreach ([1, 13, 47] as $size) { $visible = ''; $reader = new Stream($input, static function (string $delta) use (&$visible): bool { $visible .= $delta; return true; }, null, $inputHead, null, $inputIndex); foreach (mb_str_split($unit . "\n\n", $size, 'UTF-8') as $chunk) if (!$reader->push($chunk)) break; $done = $reader->finish(); $expect($done === $valid && $reader->rejected() === !$valid, $name . ': actual composed stream at chunk ' . $size); $expect($valid ? $visible === $unit . "\n\n" : $visible === '', $name . ': accepted bytes exact / rejected unit unreleased'); } $observations[] = $entry; }; $abstracts = ['不能仅据相关阴性反推为绝对无实', '该限制不等同于证明纯虚无夹杂', '病位不在表,属里证']; foreach ($abstracts as $number => $abstract) { $case('abstract_allowed_' . $number, $abstract . '。', true); foreach ([',患者无固定刺痛。', '且患者无固定刺痛。', '并患者有固定刺痛。', '患者有腰酸耳鸣。', ',脉滑。', ',舌质红绛。', '而患者无急性感染诱因。', ';本次脉细弱有力。', ',口干未记录。'] as $tailNumber => $tail) $case('abstract_keeps_tail_' . $number . '_' . $tailNumber, $abstract . $tail, false); } foreach ([ '并非不能仅据相关阴性反推为绝对无实。', '不能不据相关阴性反推为绝对无实。', '不能仅据相关阴性反推为绝对无实性皮疹。', '不能仅据相关阴性反推为绝对无实且患者无固定刺痛。', '该限制不等同于证明纯虚无夹杂为事实。', '并非该限制不等同于证明纯虚无夹杂。', '该限制不等同于证明纯虚无夹杂(s999)。', '该限制不等同于证明纯虚无夹杂(s0)。', '该限制不等同于证明纯虚无夹杂(2020-05-02,s2)。', '病位不在表,属里证,患者无腰酸耳鸣或固定刺痛。', '病位不在表,属里证,纯虚无夹杂。', ] as $number => $unit) $case('abstract_no_outer_or_clinical_exemption_' . $number, $unit, false); $case('epistemic_annotation_existing_sources', '该限制不等同于证明纯虚无夹杂(s2、s3)。', true); $pulse = '气虚、卫表不固证,依据为动则气短、自汗活动后加重及脉细弱无力(s1、s2、s10)。'; $case('pulse_own_positive_evidence_scope', $pulse, true); $case('pulse_minimal_wrong_historical_source', '脉细弱无力(s14)。', false); $case('pulse_summary_wrong_current_role_source', '卫表不固证,依据为脉细弱无力(s3)。', false); foreach (['脉细弱有力', '脉细弱滑无力', '脉细弱无力及脉结', '脉细弱无力但脉浮', '脉不细弱无力'] as $number => $feature) $case('pulse_no_feature_widening_' . $number, str_replace('脉细弱无力', $feature, $pulse), false); foreach (['患者无气短,依据为脉细弱无力(s10)。', $pulse . '患者无自汗。', $pulse . '患者有固定刺痛。', '卫表不固证,依据为脉细弱无力(2020-05-02,s10)。', '卫表不固证,依据为脉细弱无力(s14)。', '卫表不固证,依据为患者无固定刺痛、脉细弱无力。'] as $number => $unit) $case('pulse_scope_preserves_other_rejection_' . $number, $unit, false); foreach (['weak_only' => '本次医师切诊:脉细弱。', 'missing' => null] as $name => $record) { $variant = CompleteTcmCase::current(); $variant['pulse'] = $record; $case('pulse_no_borrow_' . $name, $pulse, false, Guard::build($variant, $submission)); } $locator = '腰肾相关资料见s2,暂不定肾虚'; $case('verified_exact_locator', $locator . '。', true); foreach (['s0', 's02', 's3', 's10', 's14', 's999', 's2x', 's-2'] as $number => $id) $case('locator_bad_id_' . $number, str_replace('s2', $id, $locator) . '。', false); foreach ([',患者无腰酸耳鸣。', '且患者有腰酸耳鸣。', '并患者有固定刺痛。', '而脉滑。', '(2020-05-02,s2)。'] as $number => $tail) $case('locator_cannot_hide_claim_' . $number, $locator . $tail, false); $noLumbar = CompleteTcmCase::current(); $noLumbar['present_illness'] = '神疲乏力,未记录固定刺痛。'; $case('locator_actual_subject_required', $locator . '。', false, Guard::build($noLumbar, $submission)); $historicalLedger = $ledger; $historicalLedger['sources']['s2']['scope'] = 'historical'; $case('locator_same_date_historical_not_current', $locator . '。', false, $historicalLedger); $future = '适用条件:复核症状仍以活动后乏力、气短、自汗为主,口干少量频饮及少津舌持续存在,且无需要优先处理的急性病因'; $case('future_exact_acute_prerequisite', $future . '。', true); $case('future_without_optional_persistence', '适用条件:复核症状仍以活动后乏力为主,且无需要优先处理的急性病因。', true); foreach ([',患者无固定刺痛。', '并无腰酸耳鸣。', '且患者有固定刺痛。', '患者无胸痛。', ',脉滑。', ',舌质红绛。', '且无急性感染诱因。', ';口干未记录。', '这已被本次排除。'] as $number => $tail) $case('future_keeps_actual_tail_' . $number, $future . $tail, false); foreach ([ str_replace('复核症状', '本次复核症状', $future), str_replace('且无需要', '且本次无需要', $future), str_replace('且无需要', '且已确认无需要', $future), str_replace('复核症状仍以', '复核患者自述仍以', $future), str_replace('口干少量频饮及少津舌', '脉滑', $future), str_replace('口干少量频饮及少津舌', '无固定刺痛', $future), str_replace('活动后乏力、气短、自汗', '患者无固定刺痛', $future), str_replace('活动后乏力、气短、自汗', '脉滑', $future), '并非' . $future, $future . '(s2)', ] as $number => $unit) $case('future_no_current_or_target_leak_' . $number, $unit . '。', false); // NEW independent r25b local-list/citation boundaries, declared before replay. foreach ([ '脉细弱,无力(s10)。', '脉细弱,按之无力(s10)。', '舌质淡红,少津(s9)。', '舌质淡红(s9),少津(s9)。', '脉细弱(s10),无力(s10)。', '脉细弱无力(s10),舌质淡红(s9)。', '舌质淡红、脉细弱无力(s9、s10)。', ] as $number => $unit) $case('r25b_valid_own_source_' . $number, $unit, true); foreach ([ '脉细弱,无力(s14)。', '脉细弱,无力(s3)。', '舌质淡红,少津(s3)。', '舌质淡红(s9),少津(s3)。', '脉不浮,不滑,不紧(s14)。', '脉细弱,无力(s14),口干(s10)。', '脉细弱(s10),无力(s14)。', '脉细弱,按之无力(s14)。', '舌质淡红、脉细弱无力(s9)。', '舌质淡红、脉细弱无力(s10)。', '脉细弱无力(s3、s14),舌质淡红(s9)。', '舌质淡红(s9),脉细弱无力(s3、s14)。', '本次脉细弱无力(2020-05-02,s10)。', '脉细弱无力(s10),按之有力(s10)。', ] as $number => $unit) $case('r25b_wrong_local_source_or_state_' . $number, $unit, false); $extraSource = $ledger; $extraSource['sources']['s40'] = $ledger['sources']['s10']; $extraSource['sources']['s40']['id'] = 's40'; foreach ([ 'historical_scope' => ['scope' => 'historical', 'path' => 'history.0.pulse'], 'wrong_date' => ['date' => '2020-05-02'], 'opposite_state' => ['quote' => '本次医师切诊:不细、不弱,有力。'], 'unknown_record' => ['quote' => '未记录'], 'historical_clause' => ['quote' => '既往医师切诊:脉细弱无力。'], ] as $name => $delta) { $variantLedger = $extraSource; $variantLedger['sources']['s40'] = array_replace($variantLedger['sources']['s40'], $delta); $case('r25b_source_cannot_launder_' . $name, '本次脉细弱无力(s40)。', false, $variantLedger); } $split = CompleteTcmCase::current(); $split['pulse'] = '本次医师切诊:脉细弱。'; $split['pulse_condition'] = '本次医师切诊:按之无力。'; $splitLedger = Guard::build($split); $ids = []; foreach ($splitLedger['sources'] as $source) if (in_array($source['field'], ['pulse', 'pulse_condition'], true)) $ids[$source['field']] = $source['id']; $case('r25b_real_split_alias_union', '脉细弱无力(' . $ids['pulse'] . '、' . $ids['pulse_condition'] . ')。', true, $splitLedger); $case('r25b_alias_field_not_feature_support', '脉细弱无力(' . $ids['pulse_condition'] . ')。', false, $splitLedger); $case('r25b_cited_pulse_then_actual_force_alias', '脉细弱(' . $ids['pulse'] . '),按之无力(' . $ids['pulse_condition'] . ')。', true, $splitLedger); $case('r25b_force_cannot_borrow_prior_cited_weak_source', '脉细弱(' . $ids['pulse'] . '),按之无力(' . $ids['pulse'] . ')。', false, $splitLedger); foreach (['既往脉细弱无力(s10)。', '既往脉细弱无力(2020-05-02,s10)。', '既往脉细弱无力(s10),本次脉细弱无力(s10)。'] as $number => $unit) $case('r25b_historical_claim_cannot_cite_current_' . $number, $unit, false); $validHistorical = $extraSource; $validHistorical['sources']['s40'] = array_replace($validHistorical['sources']['s40'], ['scope' => 'historical', 'path' => 'history.0.pulse', 'date' => '2020-05-02', 'quote' => '2020-05-02医师切诊:脉细弱无力。']); $case('r25b_valid_historical_own_source', '既往脉细弱无力(2020-05-02,s40)。', true, $validHistorical); // Independent c grammar extensions. Original 1151 test remains untouched. foreach ([ '脉细弱(s10),亦无力(s10)。', '舌质淡红(s9),亦少津(s9)。', '脉细弱(s10),本次无力(s10)。', '舌质淡红(s9),当前少津(s9)。', '脉细弱、舌质淡红,少津(s9、s10)。', '舌质淡红、脉细弱,无力(s9、s10)。', ] as $number => $unit) $case('r25c_supported_continuation_' . $number, $unit, true); foreach ([ '脉细弱(s10),亦无力(s3)。', '舌质淡红(s9),亦少津(s3)。', '脉细弱(s10),本次无力(s3)。', '舌质淡红(s9),当前少津(s3)。', '脉细弱(s10),既往无力(s10)。', '既往脉细弱(2020-05-02,s40),当前无力(s40)。', '脉细弱(s10),口干(s2),脉无力(s3)。', '舌质淡红(s9),腹部按之无力(s3),舌少津(s3)。', '脉细弱、舌质淡红,少津(s9)。', '舌质淡红、脉细弱,无力(s10)。', '脉细弱(s10),不无力(s10)。', ] as $number => $unit) { $case('r25c_no_wrong_scope_role_or_polarity_' . $number, $unit, false, str_contains($unit, 's40') ? $validHistorical : null); } $case('r25c_historical_current_explicit_switch', '既往脉细弱(2020-05-02,s40),本次脉细弱无力(s10)。', true, $validHistorical); $case('r25c_historical_adjacent_bare_with_conjunction', '既往脉细弱(2020-05-02,s40),亦无力(s40)。', true, $validHistorical); $oldLeap = CompleteTcmCase::current(); $oldLeap['diagnosis_date'] = '2020-02-29'; $oldLeapLedger = Guard::build($oldLeap); $case('r25c_consistent_old_leap_visit', '本次脉细弱,无力(s10)。', true, $oldLeapLedger); $parser = new ReflectionMethod(Guard::class, 'explicitExamReferencesSupported'); foreach (['脉细弱(s10),本次无力(s3)。', '舌质淡红(s9),当前少津(s3)。', '脉细弱(s10),既往无力(s10)。', '既往脉细弱(2020-05-02,s40),当前无力(s40)。'] as $number => $text) $expect(!$parser->invoke(null, str_contains($text, 's40') ? $validHistorical : $ledger, $text), 'r25c whole-sentence helper rejects the bounded temporal continuation that composed parsing loses ' . $number); foreach (['脉细弱(s10),口干(s2),无力(s3)。', '脉细弱(s10),腹部按之无力(s3)。', '舌质淡红(s9),口干(s2),少津(s3)。'] as $number => $text) $expect($parser->invoke(null, $ledger, $text), 'r25c parser-only ordinary subject reset ' . $number); foreach ([null, '', false, 20261023, '2026-02-29', '2026-13-01', 'not-a-date'] as $number => $date) { $parserLedger = $extraSource; $parserLedger['sources']['s40']['date'] = '2020-05-02'; $parserLedger['field_states']['pulse']['date'] = $date; $expect($parser->invoke(null, $parserLedger, '本次脉细弱无力(s40)。'), 'r25c parser-only missing/invalid construction metadata retains declared trust boundary ' . $number); } // d-only bounded integration permutations; older suites remain untouched. foreach ([ '脉细弱(s10), 当前亦无力(s10)。', '舌质淡红(s9), 此次亦少津(s9)。', '脉细弱(2026-10-23,s10),本次无力(s10)。', '脉细弱(s10),当前舌质淡红(s9),目前少津(s9)。', '脉细弱、舌质淡红(s10、s9),当前少津(s9)。', '舌质淡红、脉细弱(s9、s10),本次无力(s10)。', '脉不浮(s10),当前不滑(s10)。', ] as $number => $unit) $case('r25d_correct_context_chain_' . $number, $unit, true); foreach ([ '脉细弱(s10), 当前亦无力(s3)。', '舌质淡红(s9), 此次亦少津(s3)。', '脉细弱(2026-10-23,s10),本次无力(s3)。', '脉细弱(s10),当前舌质淡红(s9),目前少津(s3)。', '脉细弱、舌质淡红(s10、s9),当前少津(s3)。', '舌质淡红、脉细弱(s9、s10),本次无力(s3)。', '脉不浮(s10),当前不滑(s14)。', '脉细弱(s10),当前无力及滑(s10)。', '脉细弱(s10),当前无力且滑(s10)。', '脉细弱(s10),当前无力且有力(s10)。', ] as $number => $unit) $case('r25d_wrong_context_role_or_feature_' . $number, $unit, false); $case('r25d_three_scope_switches_supported', '脉细弱(s10),既往无力(2020-05-02,s40),本次无力(s10)。', true, $validHistorical); $case('r25d_wrong_middle_scope_not_rescued', '脉细弱(s10),既往无力(s10),本次无力(s10)。', false); $reference = new ReflectionMethod(Guard::class, 'explicitExamReferencesSupported'); $parse = new ReflectionMethod(Guard::class, 'reportClauses'); $oldParse = new ReflectionMethod($beforeGuard, 'reportClauses'); foreach (['。', ';', ';', '!', '?', "\n", "\r\n"] as $number => $separator) { $text = '脉细弱(s10)' . $separator . '当前无力(s3)。'; $expect($parse->invoke(null, $ledger, $text) === $oldParse->invoke(null, $ledger, $text), 'r25d independent real-boundary parser array equality ' . $number); } foreach (['脉细弱(s10),无力,口干,当前无力(s3)。', '舌质淡红(s9),少津,腹部按之无力(s3),目前少津(s3)。', '脉象资料见上文(s3),目前无力(s3)。'] as $number => $text) $expect($parse->invoke(null, $ledger, $text) === $oldParse->invoke(null, $ledger, $text), 'r25d independent ordinary-subject reset parser equality ' . $number); $context = []; $expect(!$reference->invokeArgs(null, [$ledger, '脉细弱(s3)', &$context]) && $context === [], 'r25d invalid citation cannot seed checker context'); foreach ([['“','”'], ['「','」'], ['"','"']] as $number => [$open,$close]) { $text = '切诊:2026-10-23' . $open . $ledger['sources']['s10']['quote'] . $close . '(s10),当前无力(s3)。'; $expect($parse->invoke(null, $ledger, $text) === $oldParse->invoke(null, $ledger, $text), 'r25d independent complete source quote parser array equality ' . $number); } // Strictly require every original file and full decoded unit; no optional skips. $files = [ 'single_openai_20261024_0_openai' => ['39056e3750c9b385f543db7da351b47657d997c5ac705e48cbd93f088b4c178e', 2, true], 'double_openai_20261024_0_openai' => ['0c03a7a8a764179e4535ea40c0c608e811598a2259ae9aefec3c3bdc3959f543', 1, true], 'double_openai_20261024_1_openai' => ['e1e5a7a26aae0f7443b00c6dc1f4db2ad0f97006fc84fd91bb0171423c21c252', 1, true], 'openai_deadline_20261024_openai_next_full90_openai' => ['084a59af11c14b06d6a9d5664db53a87617c5e0083bcd6640b52c2c8d1d7a21c', 1, true], 'single_qwen_20261024_0_qwen' => ['55ee0125d1cec5ccc344d9fef747e118d9e55b8147fd901e819d8a4a38408b8a', 2, false], 'double_qwen_20261024_0_qwen' => ['2e7cce04ca83526a17c3cda27764b2386e192df35b36ef37f3dd47f622f561ba', 2, false], 'double_qwen_20261024_1_qwen' => ['0d803a89c349f10f1317080c7232379c59b7e1f291e514b373e63960819d4b85', 2, false], ]; $retainedCount = 0; $wideningCount = 0; foreach ($files as $stem => [$hash, $count, $valid]) { $path = $artifacts . 'tcm_complete_v43r24_' . $stem . '.rejected.json'; if (!is_file($path)) throw new RuntimeException('Missing original evidence file'); $expect(hash_file('sha256', $path) === $hash, 'exact original rejection file ' . $stem); $document = json_decode(file_get_contents($path), true, 512, JSON_THROW_ON_ERROR); $expect(count($document['rejections']) === $count, 'all retained units present ' . $stem); foreach ($document['rejections'] as $number => $unit) { ++$retainedCount; $case('original_r24_' . $stem . '_' . $number, $unit['unit'], $valid); $g = Guard::validate($ledger, $unit['unit']); if (in_array('NEGATION_SCOPE_WIDENED', array_column($g['issues'], 'code'), true)) ++$wideningCount; } $expect(hash_file('sha256', $path) === $hash, 'retained artifact unchanged ' . $stem); } $expect($retainedCount === 11 && $wideningCount === 5, 'exact 11 retained units and all five actual Q degree deletions preserved'); $expect(hash('sha256', serialize($ledger)) === $sourceIdentity, 'source ledger unchanged by all validation/replay paths'); $expect(!class_exists('app\\adminapi\\logic\\tcm\\DiagnosisAiLogic', false), 'inference Logic never loaded'); echo json_encode(['scope' => 'independent r25d final checker-only temporal integration review', 'assertions' => $checks, 'failures' => $failures, 'observations' => $observations, 'retained_units' => $retainedCount, 'qwen_real_degree_loss_still_rejected' => $wideningCount], JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR), "\n"; exit($failures === [] ? 0 : 1);