'a4b7f13452d8d884699041a8f5daa2889b366ea05985d39febd405873fbcd8fd', Contract::class => 'c25e88fcc0633e957cf3a7aca4b417140b9ce9766e745c0d7b1bc2528aaaa509', Index::class => '4597475de68f8a45d14f4d309e3c4fadf9e585dd1dafab39b3df8319ea05f6e7', Stream::class => '3b0664a0e988e8a60920e2f9c1885bc13b5d8f644156522a9db4a38409ffae5a', ]; foreach ($pins as $class => $hash) { $path = (new ReflectionClass($class))->getFileName(); $expect(hash_file('sha256', $path) === $hash, 'exact final source pin ' . $class); } $before = file_get_contents($artifacts . 'v43r25d_before_TcmClinicalFactStateGuard.php.snapshot'); if (hash('sha256', $before) !== 'e139ff33772ec37afacda14fb1866878c2b1572f4d0788ccc526cb0aab49cc2c') { throw new RuntimeException('Unverified before snapshot'); } $before = preg_replace('/final class TcmClinicalFactStateGuard\b/', 'final class IndependentR25eBeforeGuard', $before, 1, $replaced); $expect($replaced === 1, 'before snapshot isolated class only'); eval(substr($before, strlen('invoke(null, CompleteTcmCase::rows()); $submission = (new ReflectionMethod(PatientAiReportLogic::class, 'sanitizeSnapshotForUpstream'))->invoke(null, AiRequiredSource::submission($raw)); $ledger = Guard::build(CompleteTcmCase::current(), $submission); $ledgerBytes = serialize($ledger); $expect($ledgerBytes === serialize($oldGuard::build(CompleteTcmCase::current(), $submission)), 'before/current ledger construction exact'); $expect(hash('sha256', json_encode($ledger, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)) === '2ebbec0b558347bf4ee128dfb5a9e63fc4e355d58f7226486a2ae96098bd57f6', 'actual complete source ledger identity'); $expect(hash_file('sha256', __DIR__ . '/fixtures/CompleteTcmCase.php') === 'b54c47ec11f6a99dc87bdda78629e18c8a6f37a64017e42a72db399c727cb1c6', 'fixture unchanged'); $headFor = static function (array $index): string { $head = "一、四诊\n\n"; foreach (Index::ROLES as $label => $role) { $chosen = null; foreach ($index['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) { $head .= $label . ($role === 'palpation' ? ":其他触诊未记录,脉象见独立脉象项。\n\n" : ":未记录\n\n"); } else { [$source, $span] = $chosen; $head .= $label . ':' . $source['date'] . '“' . trim($span['quote']) . '”(' . $source['id'] . ")。\n\n"; } } return $head . "二、八纲\n\n"; }; $case = static function (string $label, string $unit, bool $expected, ?array $custom = null) use ( $ledger, $headFor, $oldGuard, $expect, &$observations): void { $input = $custom ?? $ledger; $inputBytes = serialize($input); $originalUnit = $unit; $index = Index::build($input); $head = $headFor($index); $expect(Guard::validate($input, $head)['valid'] && Contract::validate($index, $head, true)['valid'], $label . ' valid independent six-role prefix'); $guard = Guard::validate($input, $head . $unit); $old = $oldGuard::validate($input, $head . $unit); $contract = Contract::validate($index, $head . $unit, true); $expect($guard['valid'] === $expected, $label . ' Guard expected state'); $expect(($guard['valid'] && $contract['valid']) === $expected, $label . ' composed expected state'); foreach ([1, 13, 47] as $size) { $visible = ''; $stream = new Stream($input, static function (string $delta) use (&$visible): bool { $visible .= $delta; return true; }, null, $head, null, $index); foreach (mb_str_split($unit . "\n\n", $size, 'UTF-8') as $chunk) if (!$stream->push($chunk)) break; $expect($stream->finish() === $expected && $stream->rejected() === !$expected, $label . ' stream outcome ' . $size); $expect($visible === ($expected ? $unit . "\n\n" : ''), $label . ' exact accepted / withheld rejected bytes ' . $size); } $expect($unit === $originalUnit && serialize($input) === $inputBytes, $label . ' original output and source bytes unchanged'); $observations[] = ['label' => $label, 'unit_sha256' => hash('sha256', $unit), 'expected_valid' => $expected, 'before_guard_valid' => $old['valid'], 'before_guard_issues' => $old['issues'], 'guard_valid' => $guard['valid'], 'contract_valid' => $contract['valid'], 'guard_issues' => $guard['issues'], 'contract_issues' => $contract['issues']]; }; // Exercise every newly closed operator with three operands, rather than only // the original two-operand counterexamples. No extra conjunction is invented. foreach (['且', '并且', '而且', '并', '而', '亦', '也'] as $number => $join) { $case('closed_three_positive_' . $number, '脉细弱(s10),当前细' . $join . '弱' . $join . '无力(s10)。', true); $case('closed_third_positive_unsupported_' . $number, '脉细弱(s10),当前细' . $join . '弱' . $join . '有力(s10)。', false); $case('closed_three_mixed_polarity_' . $number, '脉细弱(s10),当前不浮' . $join . '无力' . $join . '不紧(s10)。', true); $case('closed_third_polarity_not_borrowed_' . $number, '脉细弱(s10),当前不浮' . $join . '无力' . $join . '紧(s10)。', false); $case('closed_tongue_label_' . $number, '当前舌质淡红' . $join . '少津(2026-10-23,s9)。', true); $case('closed_tongue_third_unsupported_' . $number, '当前舌质淡红' . $join . '少津' . $join . '胖大(s9)。', false); } foreach ([ ['shared_group_supports_each_kind', '舌质淡红且少津、脉细弱且无力(s9、s10)。', true], ['shared_group_missing_tongue', '舌质淡红且少津、脉细弱且无力(s10)。', false], ['right_elsewhere_not_local_support', '脉细弱且无力(s10),本次细并且无力(s3)。', false], ['wrong_group_not_repaired_later', '脉细弱且无力(s3),本次脉细弱无力(s10)。', false], ['same_kind_union_valid', '脉细弱(s10),当前细且弱并且无力(s1、s2、s10)。', true], ['current_date_exact', '脉细弱(s10),当前不浮并且无力(2026-10-23,s10)。', true], ['current_date_wrong', '脉细弱(s10),当前不浮并且无力(2020-05-02,s10)。', false], ['historical_role_not_current', '脉细弱(s10),当前细并且无力(s14)。', false], ['finite_tail_cannot_hide_denial', '脉细弱且无力(s10),患者无固定刺痛。', false], ['finite_tail_cannot_hide_pulse', '脉细弱且无力(s10),脉滑(s10)。', false], ['long_late_tight_not_hidden', '脉细弱(s10),当前不浮且' . str_repeat('细', 60) . '紧(s10)。', false], ['long_supported_force', '脉细弱(s10),当前' . str_repeat('弱', 60) . '并且按之无力(s10)。', true], ] as [$label, $unit, $expected]) $case($label, $unit, $expected); $historical = $ledger; $historical['sources']['s40'] = array_replace($ledger['sources']['s10'], ['id' => 's40', 'scope' => 'historical', 'path' => 'history.0.pulse', 'date' => '2020-05-02', 'quote' => '2020-05-02医师切诊:脉细弱无力。']); $case('historical_closed_local_valid', '既往脉细弱(2020-05-02,s40),当年细且无力(s40)。', true, $historical); $case('historical_closed_cannot_promote', '既往脉细弱(2020-05-02,s40),本次细且无力(s40)。', false, $historical); $case('historical_closed_cannot_cite_current', '既往脉细弱(2020-05-02,s40),当年细且无力(s10)。', false, $historical); $unknownSource = $ledger; $unknownSource['sources']['s40'] = array_replace($ledger['sources']['s10'], ['id' => 's40', 'quote' => '未记录']); $case('unknown_source_cannot_borrow_known_closed_features', '脉细弱且无力(s40)。', false, $unknownSource); // The changed parser entry is a separate validation view. Ordinary text, // quotes, IDs, dates and flags must remain exactly the old entry's values. $parse = new ReflectionMethod(Guard::class, 'reportClauses'); $oldParse = new ReflectionMethod($oldGuard, 'reportClauses'); $helper = new ReflectionMethod(Guard::class, 'explicitExamReferencesSupported'); $finite = new ReflectionMethod(Guard::class, 'finiteExamConjunctionProse'); $unknownProse = '脉细弱(s10),当前不浮且无力未记录(s10)。'; $expect($finite->invoke(null, 'pulse', '不浮且无力未记录') === null, 'reporting unknown prose is outside the closed feature conjunction grammar'); $expect($parse->invoke(null, $ledger, $unknownProse) === $oldParse->invoke(null, $ledger, $unknownProse), 'outside-grammar unknown-reporting prose has identical before/current parser entries'); foreach (['脉细弱且无力(s10)。', '舌质淡红并且少津(2026-10-23,s9)。', '脉细弱(s10),当前不浮并且无力(s10)。'] as $number => $unit) { $entries = $parse->invoke(null, $ledger, $unit); $hadView = false; foreach ($entries as &$entry) { $hadView = $hadView || isset($entry['exam_conjunction_parts']); unset($entry['exam_conjunction_parts']); } unset($entry); $expect($hadView, 'closed supported single-kind entry receives only a separate parts view ' . $number); $expect($entries === $oldParse->invoke(null, $ledger, $unit), 'raw entry fields byte-identical after removing parts view ' . $number); } foreach (['脉细弱且无力(s10)口干', '脉细弱且无力(s10)及脉细弱(s10)', '“脉细弱且无力”(s10)', '「脉细弱且无力」(s10)', '"脉细弱且无力"(s10)'] as $number => $unit) { $context = []; $view = ['sentinel']; $helper->invokeArgs(null, [$ledger, $unit, &$context, &$view]); $expect($view === null, 'quoted/tail/multiple-citation helper cannot export replacement parts ' . $number); } $context = []; $view = ['sentinel']; $expect(!$helper->invokeArgs(null, [$ledger, '脉细弱且无力(s3)', &$context, &$view]) && $context === [] && $view === null, 'invalid own citation cannot export context or replacement parts'); foreach (['不浮且口干', '细并且腹部按之无力', '细而且患者无固定刺痛', '弱也当前无力', '细且且弱', '细并且', '并且细', '舌淡红且少津'] as $number => $ordinary) { $expect($finite->invoke(null, 'pulse', $ordinary) === null, 'whole-operand finite grammar excludes ordinary or malformed subject ' . $number); } foreach (['。', ';', ';', '!', '?', "\n", "\r\n"] as $number => $end) { $text = '脉细弱且无力(s10)' . $end . '当前不浮且无力(s3)。'; // The first entry may gain only its allowed parts key; the real reset is // the last entry, which must remain the original independent subject. $newEntries = $parse->invoke(null, $ledger, $text); $oldEntries = $oldParse->invoke(null, $ledger, $text); $expect(end($newEntries) === end($oldEntries), 'real sentence boundary cannot propagate finite exam context ' . $number); } foreach (['口干', '腹部按之无力'] as $number => $newSubject) { $text = '脉细弱(s10),当前无力且' . $newSubject . '(s3),目前无力(s3)。'; $expect($parse->invoke(null, $ledger, $text) === $oldParse->invoke(null, $ledger, $text), 'explicit ordinary subject retains complete old parser equality ' . $number); } 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), 'complete actual source-quote reset and offsets unchanged ' . $number); } $parts = (new ReflectionMethod(Guard::class, 'parts'))->invoke(null, '脉' . $finite->invoke(null, 'pulse', '不浮滑并且不紧')); $topics = array_column($parts, 'topic'); $expect(in_array('脉浮滑', $topics, true) && !in_array('脉浮', $topics, true) && !in_array('脉滑', $topics, true), 'compound negative feature remains a single scoped topic in the normalized validation view'); $expect(serialize($ledger) === $ledgerBytes, 'all source bytes retained after composed and helper paths'); $expect(!class_exists('app\\adminapi\\logic\\tcm\\DiagnosisAiLogic', false), 'no inference Logic loaded'); foreach ($pins as $class => $hash) { $expect(hash_file('sha256', (new ReflectionClass($class))->getFileName()) === $hash, 'exact final source pin after tests ' . $class); } echo json_encode(['scope' => 'independent r25e closed finite exam conjunction review', 'assertions' => $checks, 'cases' => count($observations), 'failures' => $failures, 'observations' => $observations], JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR), "\n"; exit($failures === [] ? 0 : 1);