Files
zyt/server/tests/TcmR25eFiniteExamConjunctionTest.php
2026-10-27 14:03:35 +08:00

139 lines
9.4 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
declare(strict_types=1);
require dirname(__DIR__) . '/vendor/autoload.php';
require __DIR__ . '/fixtures/CompleteTcmCase.php';
use app\adminapi\logic\tcm\PatientAiReportLogic;
use app\adminapi\service\AiRequiredSource;
use app\adminapi\service\TcmClinicalEvidenceContract as Contract;
use app\adminapi\service\TcmClinicalEvidenceIndex as Index;
use app\adminapi\service\TcmClinicalFactStateGuard as Guard;
use app\adminapi\service\TcmClinicalReportStream as Stream;
$checks = 0; $cases = 0; $failures = [];
$expect = static function (bool $ok, string $message) use (&$checks, &$failures): void {
++$checks;
if (!$ok) $failures[] = $message;
};
$artifacts = dirname(__DIR__, 2) . '/app/artifacts/ai_concurrency/';
$before = file_get_contents($artifacts . 'v43r25d_before_TcmClinicalFactStateGuard.php.snapshot');
$expect(hash('sha256', $before) === 'e139ff33772ec37afacda14fb1866878c2b1572f4d0788ccc526cb0aab49cc2c', 'Exact d Guard preserved.');
if (hash('sha256', $before) !== 'e139ff33772ec37afacda14fb1866878c2b1572f4d0788ccc526cb0aab49cc2c') throw new RuntimeException('Unverified snapshot');
$before = preg_replace('/final class TcmClinicalFactStateGuard\b/', 'final class R25eBeforeGuard', $before, 1, $replaced);
$expect($replaced === 1, 'Isolated before class only.');
eval(substr($before, strlen('<?php')));
$beforeGuard = 'app\\adminapi\\service\\R25eBeforeGuard';
$raw = (new ReflectionMethod(PatientAiReportLogic::class, 'buildSourceSnapshotFromRows'))->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($beforeGuard::build(CompleteTcmCase::current(), $submission)), 'Before/current construction byte-identical.');
$expect(hash('sha256', json_encode($ledger, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES))
=== '2ebbec0b558347bf4ee128dfb5a9e63fc4e355d58f7226486a2ae96098bd57f6', 'Actual complete ledger pinned.');
$index = Index::build($ledger);
$head = "一、四诊\n\n";
foreach (Index::ROLES as $label => $role) {
$found = null;
foreach ($index['sources'] as $source) {
if ($source['scope'] !== 'current') continue;
foreach ($source['spans'] as $span) if ($span['role'] === $role && $span['recorded']) { $found = [$source, $span]; break 2; }
}
if ($found === null) $head .= $label . ($role === 'palpation' ? ":其他触诊未记录,脉象见独立脉象项。\n\n" : ":未记录\n\n");
else {
[$source, $span] = $found;
$head .= $label . ':' . $source['date'] . '“' . trim($span['quote']) . '”(' . $source['id'] . ")。\n\n";
}
}
$head .= "二、八纲\n\n";
$expect(Guard::validate($ledger, $head)['valid'] && Contract::validate($index, $head, true)['valid'], 'Real six-role source-quoted prefix valid.');
$check = static function (string $unit, bool $valid, bool $oldBypass = false) use (
$expect, $ledger, $index, $head, $beforeGuard, &$cases): void {
++$cases;
$original = $unit;
$guard = Guard::validate($ledger, $head . $unit);
$contract = Contract::validate($index, $head . $unit, true);
$expect($guard['valid'] === $valid, 'Guard ' . $unit . ': ' . json_encode($guard));
$expect(($guard['valid'] && $contract['valid']) === $valid, 'Composed result ' . $unit . ': ' . json_encode($contract));
if (!$valid) $expect(in_array('SOURCE_ANNOTATION_INVALID', array_column($guard['issues'], 'code'), true), 'Every local operand must have its own actual source ' . $unit);
if ($oldBypass) $expect($beforeGuard::validate($ledger, $head . $unit)['valid'], 'Unchanged exact baseline bypass reproduced.');
foreach ([1, 13, 47] as $size) {
$visible = '';
$stream = new Stream($ledger, 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() === $valid && $stream->rejected() === !$valid, 'Stream result ' . $size . ': ' . $unit);
$expect($visible === ($valid ? $unit . "\n\n" : ''), 'Exact raw accepted bytes / no rejected unit release ' . $size);
}
$expect($unit === $original, 'Raw model unit unchanged.');
};
foreach (['且', '并且', '而且', '并', '而', '亦', '也'] as $join) {
$check('脉细弱(s10),当前无力' . $join . '滑(s10)。', false, $join === '且');
$check('脉细弱(s10),当前无力' . $join . '有力(s10)。', false, $join === '且');
$check('脉细弱(s10),当前细' . $join . '无力(s10)。', true);
$check('脉细弱(s10),当前不浮' . $join . '不滑(s10)。', true);
$check('脉细弱(s10),当前不浮' . $join . '无力(s10)。', true);
$check('脉细弱(s10),当前不浮' . $join . '滑(s10)。', false);
$check('脉细弱(s10),当前不细' . $join . '不弱(s10)。', false);
$check('脉细弱' . $join . '滑(s10)。', false);
$check('脉细弱' . $join . '无力(s10)。', true);
$check('舌质淡红(s9),当前淡红' . $join . '少津(s9)。', true);
$check('舌质淡红(s9),当前淡红' . $join . '胖大(s9)。', false);
$check('脉细弱(s10),当前' . $join . '无力(s3)。', false);
$check('脉细弱(s10),当前' . $join . '无力(s10)。', true);
}
foreach (['脉细弱(s10),当前无力并且按之滑(s10)。',
'舌质淡红、脉细弱且滑(s9、s10)。',
'脉细弱且滑、舌质淡红(s9、s10)。',
'脉细弱(s10),当前无力且细(s3)。',
'脉细弱(s10),当前无力且细(2020-05-02,s10)。',
'脉细弱(s10),既往无力且细(s10)。',
'脉细弱(s10),当前无力且细(s14)。'] as $unit) $check($unit, false);
foreach (['舌质淡红且少津(s9)。',
'舌质淡红、脉细弱且无力(s9、s10)。',
'脉细弱且无力、舌质淡红(s9、s10)。',
'脉细弱(s10),当前无力并且按之无力(s10)。',
'脉细弱(s10),当前细且弱并且无力(s1、s2、s10)。'] as $unit) $check($unit, true);
$check('脉细弱(s10),当前无力且' . str_repeat('细', 50) . '滑(s10)。', false);
$check('脉细弱(s10),当前' . str_repeat('细', 50) . '且无力(s10)。', true);
$check('脉细弱(s10),当前不浮且不' . str_repeat('滑', 50) . '细(s10)。', false);
$check('脉细弱(s10),当前不浮且不' . str_repeat('滑', 50) . '(s10)。', true);
// Ordinary subjects and real terminators retain complete parser-array
// equality to d. These are reference reset diagnostics, not clinical passes.
$parse = new ReflectionMethod(Guard::class, 'reportClauses');
$oldParse = new ReflectionMethod($beforeGuard, 'reportClauses');
foreach (['且', '并且', '而且', '并', '而', '但', '亦', '也'] as $join) {
foreach (['口干', '腹部按之无力'] as $subject) {
$unit = '脉细弱(s10),当前无力' . $join . $subject . '(s3),目前无力(s3)。';
$expect($parse->invoke(null, $ledger, $unit) === $oldParse->invoke(null, $ledger, $unit), 'Ordinary conjunction subject resets: ' . $unit);
}
}
foreach (['。', ';', ';', '!', '?', "\n"] as $end) {
$unit = '脉细弱(s10)' . $end . '当前无力且细(s3)。';
$expect($parse->invoke(null, $ledger, $unit) === $oldParse->invoke(null, $ledger, $unit), 'Real boundary still clears inherited exam kind.');
}
foreach ([['“','”'], ['「','」'], ['"','"']] as [$open, $close]) {
$unit = '切诊:2026-10-23' . $open . $ledger['sources']['s10']['quote'] . $close . '(s10),当前无力且细(s3)。';
$expect($parse->invoke(null, $ledger, $unit) === $oldParse->invoke(null, $ledger, $unit), 'Full source-quote boundary unchanged.');
}
$finite = new ReflectionMethod(Guard::class, 'finiteExamConjunctionProse');
$preserved = $parse->invoke(null, $ledger, '脉细弱(s10),当前不浮滑且不紧(s10)。');
$negativeTopics = [];
foreach ($preserved as $entry) foreach ($entry['exam_conjunction_parts'] ?? [] as $part) {
if ($part['state'] === 'negative') $negativeTopics[] = $part['topic'];
}
$expect(in_array('脉浮滑', $negativeTopics, true) && !in_array('脉浮', $negativeTopics, true)
&& !in_array('脉滑', $negativeTopics, true), 'A compound negative operand remains one original polarity scope.');
foreach (['无力且口干', '无力且腹部按之无力', '无力并且患者无固定刺痛', '无力而且目前口干',
'无力且且滑', '且滑', '无力且', '按之无力且腹部按之滑'] as $notClosed) {
$expect($finite->invoke(null, 'pulse', $notClosed) === null, 'No ordinary/malformed prose is normalized: ' . $notClosed);
}
$expect($parse->invoke(null, $ledger, $head) === $oldParse->invoke(null, $ledger, $head), 'Whole actual quoted six-role prefix unchanged.');
$expect(serialize($ledger) === $ledgerBytes, 'All source/ledger bytes unchanged.');
$expect(!class_exists('app\\adminapi\\logic\\tcm\\DiagnosisAiLogic', false), 'Inference Logic not loaded.');
echo json_encode(['scope' => 'r25e finite same-exam conjunction/source/polarity integration', 'assertions' => $checks,
'cases' => $cases, 'failures' => $failures], JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR), "\n";
exit($failures === [] ? 0 : 1);