110 lines
5.5 KiB
PHP
110 lines
5.5 KiB
PHP
<?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";
|