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

110 lines
5.5 KiB
PHP
Raw 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';
use app\adminapi\service\TcmClinicalFactStateGuard as Guard;
use app\adminapi\service\TcmClinicalReportStream as Stream;
$checks = 0;
$expect = static function (bool $condition, string $message) use (&$checks): void {
++$checks;
if (!$condition) throw new RuntimeException($message);
};
$check = static function (array $ledger, string $text, bool $valid, string $code = '', string $safePrefix = '') use ($expect): void {
$before = serialize($ledger);
$review = Guard::validate($ledger, $text);
$label = json_encode([$text, $review], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
$expect($review['valid'] === $valid, $label);
if ($code !== '') $expect(in_array($code, array_column($review['issues'], 'code'), true), $code . ': ' . $label);
$expect(serialize($ledger) === $before, 'The validation view cannot change source facts: ' . $label);
foreach ([1, 2, 7, 31] as $size) {
$visible = '';
$stream = new Stream($ledger, static function (string $delta) use (&$visible): bool {
$visible .= $delta;
return true;
});
$wire = $text . "\r\n\r\n";
foreach (str_split($wire, $size) as $chunk) if (!$stream->push($chunk)) break;
$expect($stream->finish() === $valid && $stream->rejected() === !$valid,
'Stream verdict at chunk ' . $size . ': ' . $label);
$expect($visible === ($valid ? $wire : $safePrefix),
'Original accepted bytes or only the safe heading may be visible at chunk ' . $size . ': ' . $label);
$expect($stream->acceptedContent() === $visible, 'Accepted and emitted bytes must match: ' . $label);
}
};
$unknown = Guard::build(['present_illness' => '胸痛、心悸、固定刺痛未记录。']);
$known = Guard::build([
'present_illness' => '口干;无心悸;无明显胸痛;未记录固定刺痛。',
'sleep_condition' => '易醒。',
'allergy_history' => '本次询问无已知药物过敏史。',
'tongue' => '舌质淡红,苔薄白。',
'pulse' => '脉细。',
]);
// A matching pair may wrap one unknown field value, including after a heading.
foreach ([['(', ')'], ['(', ')']] as [$open, $close]) {
foreach (['未记录', '不详', '待补充', '尚未采集'] as $state) {
foreach (['睡眠', '过敏史', '舌象', '脉象'] as $field) {
$value = $open . $state . $close;
$check($unknown, $field . ':' . $value . '。', true);
$check($unknown, $field . ":\n\n" . $value . '。', true);
$check($known, $field . ':' . $value . '。', false, 'RECORDED_TO_UNKNOWN');
$check($known, $field . ":\n\n" . $value . '。', false, 'RECORDED_TO_UNKNOWN', $field . ":\n\n");
}
}
}
$check($unknown, '睡眠:( 未记录 )。', true);
$check($unknown, '睡眠:(不详)。', false, 'UNKNOWN_TO_POSITIVE');
$check($unknown, '睡眠:(无异常)。', false);
$check($unknown, '睡眠:(未记录);患者无固定刺痛。', false, 'UNKNOWN_TO_NEGATIVE');
$check($unknown, '睡眠:(未记录,患者有固定刺痛)。', false, 'UNKNOWN_TO_POSITIVE');
$check($known, '睡眠:(未记录)但舌质红绛。', false, 'TONGUE_FEATURE_NOT_IN_CURRENT_SOURCE');
// The collection action scopes its whether-object even when it names the patient.
foreach ([
'需核实患者是否存在心悸。', '补问患者是否有胸痛?',
'需询问患者有无胸痛。', '请询问患者有无胸痛。',
'仍需进一步询问患者有无胸痛、心悸。', '请再次复核本例是否存在固定刺痛。',
'应核实:患者是否存在舌质红绛、脉滑。',
] as $text) {
$check($unknown, $text, true);
$check($known, $text, true);
}
// True claims, record gaps and qualifiers remain checked after the bounded object.
$assertions = [
['患者有心悸', 'NEGATIVE_TO_POSITIVE'],
['患者有固定刺痛', 'UNKNOWN_TO_POSITIVE'],
['患者无固定刺痛', 'UNKNOWN_TO_NEGATIVE'],
['患者无胸痛', 'NEGATION_SCOPE_WIDENED'],
['心悸未记录', 'NEGATIVE_TO_UNKNOWN'],
['口干未记录', 'POSITIVE_TO_UNKNOWN'],
['舌质红绛', 'TONGUE_FEATURE_NOT_IN_CURRENT_SOURCE'],
['脉滑', 'PULSE_FEATURE_NOT_IN_CURRENT_SOURCE'],
];
foreach ([',', '且', '并且', '但', '同时', '还', '又', '另'] as $joiner) {
foreach ($assertions as [$assertion, $code]) {
$check($known, '需核实患者是否存在感染' . $joiner . $assertion . '。', false, $code);
}
}
foreach ([
['需核实患者有心悸。', 'NEGATIVE_TO_POSITIVE'],
['已核实患者是否存在心悸。', 'NEGATIVE_TO_POSITIVE'],
['需核实患者是否存在患者有固定刺痛。', 'UNKNOWN_TO_POSITIVE'],
['需核实患者是否心悸未记录。', 'NEGATIVE_TO_UNKNOWN'],
['需核实患者是否存在已记录的固定刺痛。', 'UNKNOWN_TO_POSITIVE'],
['需核实患者是否存在感染,需关注患者有固定刺痛。', 'UNKNOWN_TO_POSITIVE'],
] as [$text, $code]) $check($known, $text, false, $code);
// Do not relax the independent negative-qualifier or concatenated-topic policy.
$check($known, '无胸痛。', false, 'NEGATION_SCOPE_WIDENED');
$check($known, '无明显胸痛。', true);
$compound = Guard::build(['present_illness' => '无胸痛心悸。']);
$check($compound, '无胸痛、心悸。', false, 'NEGATION_SCOPE_WIDENED');
$check($compound, '无胸痛心悸。', true);
echo "Clinical fact state natural language: {$checks} assertions passed (synthetic, exact streaming bytes).\n";