563 lines
45 KiB
PHP
563 lines
45 KiB
PHP
<?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\TcmClinicalFactStateGuard as Guard;
|
||
use app\adminapi\service\TcmClinicalReportStream;
|
||
|
||
// Pure lexical role regressions. A lexical pass never certifies a diagnosis,
|
||
// source-citation relevance, or an unquoted positive finding's attribution.
|
||
$checks = 0;
|
||
function roleGrammarExpect(bool $condition, string $message): void
|
||
{
|
||
++$GLOBALS['checks'];
|
||
if (!$condition) throw new RuntimeException($message);
|
||
}
|
||
function roleGrammarCheck(array $ledger, string $text, bool $expected, string $code = ''): void
|
||
{
|
||
$result = Guard::validate($ledger, $text);
|
||
roleGrammarExpect($result['valid'] === $expected,
|
||
json_encode([$text, $result], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
|
||
if ($code !== '') roleGrammarExpect(in_array($code, array_column($result['issues'], 'code'), true),
|
||
$text . ': expected ' . $code);
|
||
foreach ([1, 2, 7, 31] as $size) {
|
||
$visible = '';
|
||
$stream = new TcmClinicalReportStream($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;
|
||
roleGrammarExpect($stream->finish() === $expected && $stream->rejected() === !$expected,
|
||
'Stream decision differs at byte chunk size ' . $size . ': ' . $text);
|
||
roleGrammarExpect($expected ? $visible === $wire : $visible === '',
|
||
'Accepted bytes changed or a rejected unit leaked at byte chunk size ' . $size . ': ' . $text);
|
||
}
|
||
}
|
||
|
||
$record = [
|
||
'id' => 1, 'diagnosis_date' => '2026-10-23',
|
||
'present_illness' => '口干;无心悸;无明显胸痛;未记录固定刺痛或腰酸耳鸣。',
|
||
'past_history' => '无已知冠心病。', 'allergy_history' => '无已知药物过敏史。',
|
||
'tongue' => '舌质淡红,舌面少津,苔少;无黄腻苔、紫暗或瘀斑。',
|
||
'pulse' => '脉细弱,按之无力;不浮、不滑、不紧。',
|
||
];
|
||
$ledger = Guard::build($record);
|
||
$ids = [];
|
||
foreach ($ledger['sources'] as $source) $ids[$source['field']] = $source['id'];
|
||
|
||
foreach (['属', '属于', '为'] as $copula) foreach (['待补', '未记录'] as $state)
|
||
foreach (['依据', '证据', '资料'] as $noun) {
|
||
roleGrammarCheck($ledger, '固定刺痛' . $copula . $state . $noun . '。', true);
|
||
roleGrammarCheck($ledger, '口干' . $copula . $state . $noun . '。', false, 'POSITIVE_TO_UNKNOWN');
|
||
roleGrammarCheck($ledger, '心悸' . $copula . $state . $noun . '。', false, 'NEGATIVE_TO_UNKNOWN');
|
||
}
|
||
roleGrammarCheck($ledger, '固定刺痛属待补依据,且舌无紫暗瘀斑。', true);
|
||
roleGrammarCheck($ledger, '固定刺痛属待补依据,舌象未记录。', false, 'RECORDED_TO_UNKNOWN');
|
||
foreach (['患者有固定刺痛,属待补依据。', '患者有固定刺痛属待补依据。',
|
||
'固定刺痛属待补依据且患者有固定刺痛。', '固定刺痛属待补依据,患者有固定刺痛。'] as $text) {
|
||
roleGrammarCheck($ledger, $text, false, 'UNKNOWN_TO_POSITIVE');
|
||
}
|
||
roleGrammarCheck($ledger, '固定刺痛属待补依据,患者无固定刺痛。', false, 'UNKNOWN_TO_NEGATIVE');
|
||
roleGrammarCheck($ledger, '固定刺痛属待补依据,舌红绛。', false, 'TONGUE_FEATURE_NOT_IN_CURRENT_SOURCE');
|
||
roleGrammarCheck($ledger, '固定刺痛属待补依据,脉滑。', false, 'PULSE_FEATURE_NOT_IN_CURRENT_SOURCE');
|
||
|
||
// P2 audit follow-up: a pending record role cannot retract a self-report,
|
||
// chief complaint, or an occurrence already asserted by its own subject.
|
||
foreach (['自述固定刺痛', '主诉固定刺痛', '今晨固定刺痛发作', '固定刺痛发作',
|
||
'今晨自述固定刺痛', '昨日固定刺痛持续了3小时'] as $assertion) {
|
||
roleGrammarCheck($ledger, $assertion . '。', false, 'UNKNOWN_TO_POSITIVE');
|
||
foreach (['属待补依据', '为待补资料', '属于未记录证据'] as $recordTail) {
|
||
roleGrammarCheck($ledger, $assertion . $recordTail . '。', false, 'UNKNOWN_TO_POSITIVE');
|
||
}
|
||
roleGrammarCheck($ledger, '固定刺痛属待补依据,' . $assertion . '为待补资料。', false, 'UNKNOWN_TO_POSITIVE');
|
||
roleGrammarCheck($ledger, $assertion . '属待补依据,舌淡红苔少。', false, 'UNKNOWN_TO_POSITIVE');
|
||
}
|
||
foreach (['自述固定刺痛属未记录固定刺痛。', '主诉固定刺痛为未记录固定刺痛。',
|
||
'今晨固定刺痛发作属未记录固定刺痛。', '自述固定刺痛属未记录而非否认。',
|
||
'主诉:固定刺痛为待补资料。', '肾系定位因自述固定刺痛未记录而待定。'] as $assertedRecord) {
|
||
roleGrammarCheck($ledger, $assertedRecord, false, 'UNKNOWN_TO_POSITIVE');
|
||
}
|
||
foreach (['固定刺痛发作时段属待补依据。', '今晨固定刺痛发作频次为待补资料。',
|
||
'固定刺痛持续时间属于未记录信息。', '自述内容属待补资料。', '主诉内容为待补资料。',
|
||
'固定刺痛、腰酸耳鸣属待补依据。', '本次固定刺痛属待补依据。'] as $neutralRecord) {
|
||
roleGrammarCheck($ledger, $neutralRecord, true);
|
||
}
|
||
roleGrammarCheck($ledger, '口干属待补依据,自述固定刺痛为待补资料。', false, 'POSITIVE_TO_UNKNOWN');
|
||
roleGrammarCheck($ledger, '心悸为待补资料,主诉固定刺痛属待补依据。', false, 'NEGATIVE_TO_UNKNOWN');
|
||
roleGrammarCheck($ledger, '固定刺痛属待补依据,舌象未记录。', false, 'RECORDED_TO_UNKNOWN');
|
||
$assertedSource = Guard::build(['present_illness' => '自述固定刺痛属待补依据。']);
|
||
roleGrammarCheck($assertedSource, '固定刺痛未记录。', false, 'POSITIVE_TO_UNKNOWN');
|
||
|
||
foreach (['肾系定位', '证名判断', '脏腑归属'] as $conclusion) foreach (['未记录', '尚未记录', '未采集'] as $state) {
|
||
roleGrammarCheck($ledger, $conclusion . '因腰酸耳鸣' . $state . '而待定。', true);
|
||
roleGrammarCheck($ledger, $conclusion . '因口干' . $state . '而待定。', false, 'POSITIVE_TO_UNKNOWN');
|
||
roleGrammarCheck($ledger, $conclusion . '因心悸' . $state . '而待定。', false, 'NEGATIVE_TO_UNKNOWN');
|
||
}
|
||
roleGrammarCheck($ledger, '腰酸耳鸣未记录而待定。', true);
|
||
roleGrammarCheck($ledger, '肾系定位因舌象未记录而待定。', false, 'RECORDED_TO_UNKNOWN');
|
||
foreach (['患者有腰酸耳鸣,肾系定位因未记录而待定。', '患者有腰酸耳鸣未记录而待定。',
|
||
'肾系定位因腰酸耳鸣未记录而待定且患者有固定刺痛。'] as $text) {
|
||
roleGrammarCheck($ledger, $text, false, 'UNKNOWN_TO_POSITIVE');
|
||
}
|
||
roleGrammarCheck($ledger, '肾系定位因腰酸耳鸣未记录而待定,口干未记录。', false, 'POSITIVE_TO_UNKNOWN');
|
||
roleGrammarCheck($ledger, '肾系定位因腰酸耳鸣未记录而待定,舌红绛。', false, 'TONGUE_FEATURE_NOT_IN_CURRENT_SOURCE');
|
||
|
||
foreach (['每次', '每日', '每餐'] as $iteration) foreach (['饮水', '喝水', '进食'] as $measure) {
|
||
roleGrammarCheck($ledger, $iteration . $measure . '不多,舌淡红苔少。', true);
|
||
}
|
||
foreach (['、', ',', '且', '但'] as $boundary) {
|
||
roleGrammarCheck($ledger, '每次饮水不多' . $boundary . '患者无固定刺痛。', false, 'UNKNOWN_TO_NEGATIVE');
|
||
roleGrammarCheck($ledger, '每次饮水不多' . $boundary . '患者有心悸。', false, 'NEGATIVE_TO_POSITIVE');
|
||
roleGrammarCheck($ledger, '每次饮水不多' . $boundary . '口干未记录。', false, 'POSITIVE_TO_UNKNOWN');
|
||
roleGrammarCheck($ledger, '每次饮水不多' . $boundary . '舌红绛。', false, 'TONGUE_FEATURE_NOT_IN_CURRENT_SOURCE');
|
||
roleGrammarCheck($ledger, '每次饮水不多' . $boundary . '脉滑。', false, 'PULSE_FEATURE_NOT_IN_CURRENT_SOURCE');
|
||
}
|
||
roleGrammarCheck(Guard::build(['water_intake' => '饮水未记录。']), '每次饮水不多。', false, 'UNKNOWN_TO_POSITIVE');
|
||
roleGrammarCheck($ledger, '每次腰酸耳鸣不多。', false, 'UNKNOWN_TO_POSITIVE');
|
||
roleGrammarCheck($ledger, '每次饮水不多舌红绛。', false, 'TONGUE_FEATURE_NOT_IN_CURRENT_SOURCE');
|
||
|
||
foreach (['', '或', '及', '和', '、'] as $join) foreach (['舌', '舌质', '舌象', '舌体', '舌色'] as $subject) {
|
||
roleGrammarCheck($ledger, $subject . '无紫暗' . $join . '瘀斑。', true);
|
||
}
|
||
$qualified = Guard::build(array_replace($record, ['tongue' => '舌质淡红;无明显紫暗或瘀斑。']));
|
||
$partial = Guard::build(array_replace($record, ['tongue' => '舌质淡红;无紫暗。']));
|
||
$otherField = Guard::build(array_replace($record, ['tongue' => '舌质淡红;无紫暗。', 'tongue_coating' => '无瘀斑。']));
|
||
foreach (['', '或', '及', '和', '、'] as $join) {
|
||
roleGrammarCheck($qualified, '舌无紫暗' . $join . '瘀斑。', false, 'TONGUE_FEATURE_NOT_IN_CURRENT_SOURCE');
|
||
roleGrammarCheck($qualified, '舌无明显紫暗' . $join . '瘀斑。', true);
|
||
roleGrammarCheck($partial, '舌无紫暗' . $join . '瘀斑。', false, 'TONGUE_FEATURE_NOT_IN_CURRENT_SOURCE');
|
||
roleGrammarCheck($otherField, '舌无紫暗' . $join . '瘀斑。', false, 'TONGUE_FEATURE_NOT_IN_CURRENT_SOURCE');
|
||
}
|
||
roleGrammarCheck($ledger, '舌无紫。', false, 'TONGUE_FEATURE_NOT_IN_CURRENT_SOURCE');
|
||
roleGrammarCheck($ledger, '舌无紫暗或瘀斑,舌裂纹。', false, 'TONGUE_FEATURE_NOT_IN_CURRENT_SOURCE');
|
||
roleGrammarCheck($ledger, '舌无紫暗或瘀斑,脉涩。', false, 'PULSE_FEATURE_NOT_IN_CURRENT_SOURCE');
|
||
roleGrammarCheck($ledger, '2026-10-22舌无紫暗或瘀斑。', false, 'TONGUE_FEATURE_NOT_IN_CURRENT_SOURCE');
|
||
roleGrammarCheck($ledger, '舌无紫暗或瘀斑(2026-10-22,' . $ids['tongue'] . ')。', false, 'SOURCE_ANNOTATION_INVALID');
|
||
roleGrammarCheck($ledger, '舌无紫暗或瘀斑(s999)。', false, 'SOURCE_ANNOTATION_INVALID');
|
||
foreach (['红绛', '绛红', '紫暗', '淡红'] as $phenotype) {
|
||
$matching = Guard::build(['tongue' => '舌' . $phenotype . '。']);
|
||
roleGrammarCheck($matching, '舌' . $phenotype . '。', true);
|
||
roleGrammarCheck($matching, '舌无' . $phenotype . '。', false, 'TONGUE_FEATURE_NOT_IN_CURRENT_SOURCE');
|
||
}
|
||
|
||
foreach ([',舌', ',且舌', ',舌'] as $boundary) {
|
||
roleGrammarCheck($ledger, '无心悸' . $boundary . '淡红苔少,脉细弱。', true);
|
||
roleGrammarCheck($ledger, '无心悸' . $boundary . '红绛。', false, 'TONGUE_FEATURE_NOT_IN_CURRENT_SOURCE');
|
||
roleGrammarCheck($ledger, '无心悸' . $boundary . '无淡红。', false, 'TONGUE_FEATURE_NOT_IN_CURRENT_SOURCE');
|
||
roleGrammarCheck($ledger, '无固定刺痛' . $boundary . '淡红苔少。', false, 'UNKNOWN_TO_NEGATIVE');
|
||
}
|
||
roleGrammarCheck($ledger, '无明显胸痛,舌淡红苔少。', true);
|
||
roleGrammarCheck($ledger, '无胸痛,舌淡红苔少。', false, 'NEGATION_SCOPE_WIDENED');
|
||
roleGrammarCheck($ledger, '无药物过敏史,舌淡红苔少。', false, 'NEGATION_SCOPE_WIDENED');
|
||
|
||
foreach (['舌脉未呈黄腻、滑、紧等所见。', '舌未呈紫暗或瘀斑。', '脉未呈滑、紧等所见。',
|
||
'舌象未呈黄腻苔。', '本次脉象未呈浮。',
|
||
'舌脉未呈黄腻、滑、紧等所见,但不能仅凭这些否定完全排除夹实。'] as $text) {
|
||
roleGrammarCheck($ledger, $text, true);
|
||
}
|
||
roleGrammarCheck($qualified, '舌未呈紫暗或瘀斑。', false, 'TONGUE_FEATURE_NOT_IN_CURRENT_SOURCE');
|
||
roleGrammarCheck($qualified, '舌未呈明显紫暗或瘀斑。', true);
|
||
roleGrammarCheck($partial, '舌脉未呈黄腻、滑、紧等所见。', false, 'TONGUE_FEATURE_NOT_IN_CURRENT_SOURCE');
|
||
roleGrammarCheck($ledger, '舌脉未呈黄腻、滑、涩等所见。', false, 'PULSE_FEATURE_NOT_IN_CURRENT_SOURCE');
|
||
roleGrammarCheck($ledger, '舌脉未呈红绛、滑、紧等所见。', false, 'TONGUE_FEATURE_NOT_IN_CURRENT_SOURCE');
|
||
roleGrammarCheck($ledger, '舌未呈滑。', false, 'TONGUE_FEATURE_NOT_IN_CURRENT_SOURCE');
|
||
roleGrammarCheck($ledger, '脉未呈黄腻。', false, 'PULSE_FEATURE_NOT_IN_CURRENT_SOURCE');
|
||
roleGrammarCheck($ledger, '舌脉未呈黄腻、滑、紧等所见,患者有固定刺痛。', false, 'UNKNOWN_TO_POSITIVE');
|
||
roleGrammarCheck($ledger, '舌脉未呈黄腻、滑、紧等所见且患者有固定刺痛。', false, 'UNKNOWN_TO_POSITIVE');
|
||
roleGrammarCheck($ledger, '舌脉未呈黄腻、滑、紧等所见,舌红绛。', false, 'TONGUE_FEATURE_NOT_IN_CURRENT_SOURCE');
|
||
roleGrammarCheck($ledger, '舌脉未呈黄腻、滑、紧等所见,脉涩。', false, 'PULSE_FEATURE_NOT_IN_CURRENT_SOURCE');
|
||
roleGrammarCheck($ledger, '舌象未记录。', false, 'RECORDED_TO_UNKNOWN');
|
||
roleGrammarCheck($ledger, '未记录紫暗或瘀斑。', false, 'NEGATIVE_TO_UNKNOWN');
|
||
roleGrammarCheck($ledger, '舌未呈紫暗或瘀斑(s999)。', false, 'SOURCE_ANNOTATION_INVALID');
|
||
|
||
$parsed = (new ReflectionMethod(Guard::class, 'parts'))->invoke(null, '每次饮水不多,舌淡红苔少');
|
||
roleGrammarExpect(!in_array('negative', array_column($parsed, 'state'), true), 'Quantity 多 is not an absent clinical topic');
|
||
$features = (new ReflectionMethod(Guard::class, 'features'))->invoke(null, 'tongue', '舌紫暗或瘀斑');
|
||
roleGrammarExpect($features === ['紫暗', '瘀斑'], 'The optional tongue subject cannot consume the start of 紫暗');
|
||
$mixed = (new ReflectionMethod(Guard::class, 'parts'))->invoke(null, '舌脉未呈黄腻、滑、紧等所见');
|
||
roleGrammarExpect(array_column($mixed, 'topic') === ['舌黄腻', '脉滑、紧']
|
||
&& array_column($mixed, 'state') === ['negative', 'negative'], 'Mixed examinations retain separate typed negative parts');
|
||
|
||
// v37: discomfort is a complete symptom lexeme; an outer negative survives.
|
||
$gutPositive = Guard::build(array_replace($record, ['present_illness' => '胃肠不适;口干;无心悸;未记录固定刺痛。']));
|
||
$gutUnknown = Guard::build(array_replace($record, ['present_illness' => '口干;无心悸;未记录胃肠不适或固定刺痛。']));
|
||
$gutNegative = Guard::build(array_replace($record, ['present_illness' => '口干;无胃肠不适;未记录固定刺痛。']));
|
||
$gutQualified = Guard::build(array_replace($record, ['present_illness' => '口干;无明显胃肠不适。']));
|
||
roleGrammarCheck($gutPositive, '胃肠不适,口干。', true);
|
||
roleGrammarCheck($gutPositive, '用后监测胃肠不适、汗出、口干及气短。', true);
|
||
foreach (['无胃肠不适。', '患者无胃肠不适。', '未见胃肠不适。'] as $negative) {
|
||
roleGrammarCheck($gutPositive, $negative, false, 'POSITIVE_TO_NEGATIVE');
|
||
roleGrammarCheck($gutUnknown, $negative, false, 'UNKNOWN_TO_NEGATIVE');
|
||
roleGrammarCheck($gutNegative, $negative, true);
|
||
}
|
||
roleGrammarCheck($gutUnknown, '患者有胃肠不适。', false, 'UNKNOWN_TO_POSITIVE');
|
||
roleGrammarCheck($gutUnknown, '胃肠不适每日1次。', false, 'UNKNOWN_TO_POSITIVE');
|
||
roleGrammarCheck($gutUnknown, '胃肠不适未记录。', true);
|
||
roleGrammarCheck($gutPositive, '胃肠不适未记录。', false, 'POSITIVE_TO_UNKNOWN');
|
||
roleGrammarCheck($gutNegative, '胃肠不适未记录。', false, 'NEGATIVE_TO_UNKNOWN');
|
||
roleGrammarCheck($gutNegative, '患者有胃肠不适。', false, 'NEGATIVE_TO_POSITIVE');
|
||
roleGrammarCheck($gutQualified, '无胃肠不适。', false, 'NEGATION_SCOPE_WIDENED');
|
||
roleGrammarCheck($gutQualified, '无明显胃肠不适。', true);
|
||
roleGrammarCheck($gutPositive, '胃肠不适,舌红绛。', false, 'TONGUE_FEATURE_NOT_IN_CURRENT_SOURCE');
|
||
roleGrammarCheck($gutPositive, '胃肠不适,脉滑。', false, 'PULSE_FEATURE_NOT_IN_CURRENT_SOURCE');
|
||
$gutParts = (new ReflectionMethod(Guard::class, 'parts'))->invoke(null, '胃肠不适、汗出、口干');
|
||
roleGrammarExpect(array_column($gutParts, 'state') === ['positive'], 'Discomfort cannot negate following positive symptoms');
|
||
$negativeGutParts = (new ReflectionMethod(Guard::class, 'parts'))->invoke(null, '无胃肠不适');
|
||
roleGrammarExpect(array_column($negativeGutParts, 'state') === ['negative']
|
||
&& array_column($negativeGutParts, 'topic') === ['胃肠不适'], 'An outer absence retains the complete discomfort object');
|
||
|
||
$pendingReview = '新方各成分既往耐受、保健品及其他未记录药物仍待核';
|
||
foreach ([$pendingReview, '新药既往耐受、合并用药尚待复核', '药物相互作用、肝肾功能仍需核查',
|
||
'三味药耐受或保健品待核实'] as $review) {
|
||
roleGrammarCheck($ledger, $review . '。', true);
|
||
roleGrammarCheck($ledger, '本次无已知药物过敏史,但' . $review . '。', true);
|
||
}
|
||
foreach ([',', '且', '但', '而', ''] as $join) {
|
||
roleGrammarCheck($ledger, $pendingReview . $join . '患者无固定刺痛。', false, 'UNKNOWN_TO_NEGATIVE');
|
||
roleGrammarCheck($ledger, $pendingReview . $join . '患者有心悸。', false, 'NEGATIVE_TO_POSITIVE');
|
||
roleGrammarCheck($ledger, $pendingReview . $join . '自述固定刺痛属待补依据。', false, 'UNKNOWN_TO_POSITIVE');
|
||
}
|
||
foreach ([',', '且', '但', '而'] as $join) {
|
||
roleGrammarCheck($ledger, $pendingReview . $join . '口干未记录。', false, 'POSITIVE_TO_UNKNOWN');
|
||
roleGrammarCheck($ledger, $pendingReview . $join . '心悸未记录。', false, 'NEGATIVE_TO_UNKNOWN');
|
||
roleGrammarCheck($ledger, $pendingReview . $join . '舌红绛。', false, 'TONGUE_FEATURE_NOT_IN_CURRENT_SOURCE');
|
||
roleGrammarCheck($ledger, $pendingReview . $join . '脉滑。', false, 'PULSE_FEATURE_NOT_IN_CURRENT_SOURCE');
|
||
}
|
||
roleGrammarCheck($ledger, '患者无固定刺痛,' . $pendingReview . '。', false, 'UNKNOWN_TO_NEGATIVE');
|
||
roleGrammarCheck($ledger, '本次无药物过敏史,但' . $pendingReview . '。', false, 'NEGATION_SCOPE_WIDENED');
|
||
roleGrammarCheck($ledger, $pendingReview . '(s999)。', false, 'SOURCE_ANNOTATION_INVALID');
|
||
roleGrammarCheck($ledger, $pendingReview . '今晨固定刺痛发作。', false, 'UNKNOWN_TO_POSITIVE');
|
||
roleGrammarCheck($ledger, $pendingReview . '2026-10-23患者无固定刺痛。', false, 'UNKNOWN_TO_NEGATIVE');
|
||
$safetyQuantity = Guard::build(array_replace($record, ['water_intake' => '饮水量未记录。']));
|
||
roleGrammarCheck($safetyQuantity, $pendingReview . '每次饮水量不多。', false, 'UNKNOWN_TO_POSITIVE');
|
||
roleGrammarCheck($safetyQuantity, $pendingReview . '且每次饮水量不多。', false, 'UNKNOWN_TO_POSITIVE');
|
||
roleGrammarCheck($ledger, '固定刺痛仍待核。', false, 'UNKNOWN_TO_POSITIVE');
|
||
roleGrammarCheck($ledger, '固定刺痛耐受、患者无固定刺痛仍待核。', false, 'UNKNOWN_TO_NEGATIVE');
|
||
|
||
// Existing applicability and no-self-changing-medication instructions did not
|
||
// cause the original rejection. They receive no new blanket exemption.
|
||
foreach (['孕育相关不适用于男性', '不擅停换'] as $instruction) {
|
||
roleGrammarCheck($ledger, $instruction . '。', true);
|
||
roleGrammarCheck($ledger, $instruction . ',患者无固定刺痛。', false, 'UNKNOWN_TO_NEGATIVE');
|
||
roleGrammarCheck($ledger, $instruction . '且患者有心悸。', false, 'NEGATIVE_TO_POSITIVE');
|
||
roleGrammarCheck($ledger, $instruction . ',口干未记录。', false, 'POSITIVE_TO_UNKNOWN');
|
||
roleGrammarCheck($ledger, $instruction . ',舌红绛。', false, 'TONGUE_FEATURE_NOT_IN_CURRENT_SOURCE');
|
||
}
|
||
$applicabilityParts = (new ReflectionMethod(Guard::class, 'parts'))->invoke(null, '孕育相关不适用于男性');
|
||
roleGrammarExpect(in_array('negative', array_column($applicabilityParts, 'state'), true),
|
||
'The discomfort lexeme rule does not reinterpret the applicability predicate');
|
||
foreach (['不适用', '不适于', '不适合', '不适应', '不适宜', '不适当'] as $predicate) {
|
||
$predicateParts = (new ReflectionMethod(Guard::class, 'parts'))->invoke(null, '该药物' . $predicate . '此情形');
|
||
roleGrammarExpect(in_array('negative', array_column($predicateParts, 'state'), true),
|
||
'A complete applicability/suitability predicate is not the discomfort lexeme: ' . $predicate);
|
||
}
|
||
|
||
// Portable complete safety units, rebuilt against the actual production
|
||
// source projection. A lexical pass does not certify the safety advice.
|
||
$safetySnapshot = (new ReflectionMethod(PatientAiReportLogic::class, 'buildSourceSnapshotFromRows'))->invoke(null, CompleteTcmCase::rows());
|
||
$safetySubmission = (new ReflectionMethod(PatientAiReportLogic::class, 'sanitizeSnapshotForUpstream'))->invoke(null, AiRequiredSource::submission($safetySnapshot));
|
||
$safetyLedger = Guard::build(CompleteTcmCase::current(), $safetySubmission);
|
||
$safetyUnits = [
|
||
'禁忌与用药复核:本次询问为“无已知药物过敏史”(s5),但新方各成分既往耐受、保健品及其他未记录药物仍待核。'
|
||
. '现有ALT 22U/L、肌酐78μmol/L、eGFR 96mL/min/1.73m²、尿ACR 12mg/g,可作启用前基线,不能替代后续监测。'
|
||
. '孕育相关不适用于男性,但胃肠耐受、肝肾变化及过敏反应仍需核查。',
|
||
'禁忌待核:虽本次询问“无已知药物过敏史”(s5),仍须核实三味药既往耐受、其他药物及保健品;用后监测皮疹、胃肠不适、汗出、口干、气短及肝肾功能。'
|
||
. '现用二甲双胍0.5g每日2次,维持既有医嘱,不擅停换;联用期间继续监测空腹、餐后血糖及低血糖相关症状。'
|
||
. '2020-05-02藿香正气散系胃肠不适旧方,早已停用,不续用。',
|
||
];
|
||
foreach ($safetyUnits as $safetyUnit) roleGrammarCheck($safetyLedger, $safetyUnit, true);
|
||
|
||
// v38: absence of a symptom record, classified unknown, remains a data gap.
|
||
foreach (['无', '没有', '未见'] as $missing) foreach (['记录', '资料'] as $recordNoun) foreach (['属', '为'] as $copula) {
|
||
$tail = $recordNoun . $copula . '未知。';
|
||
roleGrammarCheck($ledger, $missing . '固定刺痛' . $tail, true);
|
||
roleGrammarCheck($ledger, $missing . '口干' . $tail, false, 'POSITIVE_TO_UNKNOWN');
|
||
roleGrammarCheck($ledger, $missing . '心悸' . $tail, false, 'NEGATIVE_TO_UNKNOWN');
|
||
roleGrammarCheck($ledger, $missing . '舌象' . $tail, false, 'RECORDED_TO_UNKNOWN');
|
||
}
|
||
roleGrammarCheck($ledger, '无固定刺痛的记录属于不详。', true);
|
||
roleGrammarCheck($ledger, '无口干信息属未知。', false, 'POSITIVE_TO_UNKNOWN');
|
||
roleGrammarCheck($ledger, '无心悸描述属未知。', false, 'NEGATIVE_TO_UNKNOWN');
|
||
foreach ([',', '且'] as $join) {
|
||
roleGrammarCheck($ledger, '无固定刺痛记录属未知' . $join . '患者有固定刺痛。', false, 'UNKNOWN_TO_POSITIVE');
|
||
roleGrammarCheck($ledger, '无固定刺痛记录属未知' . $join . '患者无固定刺痛。', false, 'UNKNOWN_TO_NEGATIVE');
|
||
roleGrammarCheck($ledger, '无固定刺痛记录属未知' . $join . '口干未记录。', false, 'POSITIVE_TO_UNKNOWN');
|
||
roleGrammarCheck($ledger, '无固定刺痛记录属未知' . $join . '舌红绛。', false, 'TONGUE_FEATURE_NOT_IN_CURRENT_SOURCE');
|
||
}
|
||
roleGrammarCheck($ledger, '患者有固定刺痛,无固定刺痛记录属未知。', false, 'UNKNOWN_TO_POSITIVE');
|
||
roleGrammarCheck($ledger, '自述固定刺痛属待补依据,无固定刺痛记录属未知。', false, 'UNKNOWN_TO_POSITIVE');
|
||
roleGrammarCheck($ledger, '无固定刺痛记录属未知,患者无药物过敏史。', false, 'NEGATION_SCOPE_WIDENED');
|
||
roleGrammarCheck($ledger, '无固定刺痛记录属未知(s999)。', false, 'SOURCE_ANNOTATION_INVALID');
|
||
roleGrammarCheck($ledger, '无固定刺痛记录属未知(2026-10-22,' . $ids['present_illness'] . ')。', false, 'SOURCE_ANNOTATION_INVALID');
|
||
$missingRecordSource = Guard::build(['present_illness' => '无固定刺痛记录属未知。']);
|
||
roleGrammarCheck($missingRecordSource, '无固定刺痛。', false, 'UNKNOWN_TO_NEGATIVE');
|
||
roleGrammarCheck($missingRecordSource, '患者有固定刺痛。', false, 'UNKNOWN_TO_POSITIVE');
|
||
$missingRecordParts = (new ReflectionMethod(Guard::class, 'parts'))->invoke(null, '无固定刺痛记录属未知');
|
||
roleGrammarExpect(array_column($missingRecordParts, 'state') === ['unknown']
|
||
&& array_column($missingRecordParts, 'topic') === ['固定刺痛'], 'Copular missing-record state retains its exact unknown subject');
|
||
|
||
// Explicit formula method/aim fragments are conceptual. The heading alone
|
||
// grants no exemption to observed findings or subsequent unrelated prose.
|
||
$functionAim = '方义上以养津改善口干舌燥';
|
||
foreach ([$functionAim . '。', '方义上以生津缓解口干舌燥。', '方义层面以养津改善口干舌燥。',
|
||
'方义:以补气恢复推动固摄,以养津改善口干舌燥,并以收敛配合控制自汗。'] as $functionText) {
|
||
roleGrammarCheck($ledger, $functionText, true);
|
||
}
|
||
foreach (['患者舌燥。', '以养津改善口干舌燥。', '方义上患者舌燥。', '方义上以养津改善患者舌燥。',
|
||
'方义上以舌燥改善口干。', '方义上舌燥,以养津改善口干。',
|
||
'患者舌燥,' . $functionAim . '。', $functionAim . ',舌燥。',
|
||
$functionAim . ',舌红绛。', '舌红绛,' . $functionAim . '。',
|
||
$functionAim . ',患者口干,以养津改善口干舌燥。'] as $actualExam) {
|
||
roleGrammarCheck($ledger, $actualExam, false, 'TONGUE_FEATURE_NOT_IN_CURRENT_SOURCE');
|
||
}
|
||
foreach ([',', '且', '但', '而', ''] as $join) {
|
||
roleGrammarCheck($ledger, $functionAim . $join . '患者有固定刺痛。', false, 'UNKNOWN_TO_POSITIVE');
|
||
roleGrammarCheck($ledger, $functionAim . $join . '患者无固定刺痛。', false, 'UNKNOWN_TO_NEGATIVE');
|
||
roleGrammarCheck($ledger, $functionAim . $join . '自述固定刺痛属待补依据。', false, 'UNKNOWN_TO_POSITIVE');
|
||
}
|
||
foreach ([',', '且', '但', '而'] as $join) {
|
||
roleGrammarCheck($ledger, $functionAim . $join . '口干未记录。', false, 'POSITIVE_TO_UNKNOWN');
|
||
roleGrammarCheck($ledger, $functionAim . $join . '心悸未记录。', false, 'NEGATIVE_TO_UNKNOWN');
|
||
roleGrammarCheck($ledger, $functionAim . $join . '脉滑。', false, 'PULSE_FEATURE_NOT_IN_CURRENT_SOURCE');
|
||
}
|
||
roleGrammarCheck($ledger, $functionAim . ',患者无药物过敏史。', false, 'NEGATION_SCOPE_WIDENED');
|
||
roleGrammarCheck($ledger, $functionAim . ',舌无紫暗或瘀斑。', true);
|
||
roleGrammarCheck($ledger, $functionAim . ',舌无紫暗或瘀斑(s999)。', false, 'SOURCE_ANNOTATION_INVALID');
|
||
roleGrammarCheck($ledger, $functionAim . '(s999)。', false, 'SOURCE_ANNOTATION_INVALID');
|
||
roleGrammarCheck($ledger, $functionAim . '今晨固定刺痛发作。', false, 'UNKNOWN_TO_POSITIVE');
|
||
roleGrammarCheck($safetyQuantity, $functionAim . '且每次饮水量不多。', false, 'UNKNOWN_TO_POSITIVE');
|
||
roleGrammarCheck($ledger, '方义上以养津改善自述固定刺痛。', false, 'UNKNOWN_TO_POSITIVE');
|
||
roleGrammarCheck($ledger, '方义上以养津改善患者有固定刺痛。', false, 'UNKNOWN_TO_POSITIVE');
|
||
|
||
$recordAndFunctionUnits = [
|
||
'血:面色少华及脉细可作血不足鉴别,但缺少明确头晕、唇甲色淡等资料,尚不能独立定为血虚;无固定刺痛记录属未知(s2),不能据此排除血瘀。',
|
||
'本例支持证据:神疲、语声低微、动则气短、自汗且活动后加重,脉细弱、按之无力,支持益气;口干欲饮而少量频饮、大便稍干、舌面少津及苔少,支持生津。'
|
||
. '方义上以补气恢复推动固摄,以养津改善口干舌燥,并以收敛配合控制自汗;是否采用仍须结合面诊确认汗出性质及津亏程度。',
|
||
];
|
||
foreach ($recordAndFunctionUnits as $recordAndFunctionUnit) roleGrammarCheck($safetyLedger, $recordAndFunctionUnit, true);
|
||
|
||
// v39: an observation joined without a comma is never a formula aim object.
|
||
$shortFunctionAim = '方义上以养津改善口干';
|
||
$joinedObservations = [
|
||
['并诉称固定刺痛', 'UNKNOWN_TO_POSITIVE'],
|
||
['并诊见舌红绛', 'TONGUE_FEATURE_NOT_IN_CURRENT_SOURCE'],
|
||
['同时脉滑', 'PULSE_FEATURE_NOT_IN_CURRENT_SOURCE'],
|
||
['还诉称固定刺痛', 'UNKNOWN_TO_POSITIVE'],
|
||
['又查见舌红绛', 'TONGUE_FEATURE_NOT_IN_CURRENT_SOURCE'],
|
||
['并且报告固定刺痛', 'UNKNOWN_TO_POSITIVE'],
|
||
['另查见脉滑', 'PULSE_FEATURE_NOT_IN_CURRENT_SOURCE'],
|
||
['诉称固定刺痛', 'UNKNOWN_TO_POSITIVE'],
|
||
['诊见舌红绛', 'TONGUE_FEATURE_NOT_IN_CURRENT_SOURCE'],
|
||
['舌质呈红绛', 'TONGUE_FEATURE_NOT_IN_CURRENT_SOURCE'],
|
||
['脉呈滑', 'PULSE_FEATURE_NOT_IN_CURRENT_SOURCE'],
|
||
];
|
||
foreach ($joinedObservations as [$observation, $code]) {
|
||
roleGrammarCheck($safetyLedger, $shortFunctionAim . $observation . '。', false, $code);
|
||
roleGrammarCheck($safetyLedger, $shortFunctionAim . ',' . $observation . '。', false, $code);
|
||
roleGrammarCheck($safetyLedger, $observation . ',' . $shortFunctionAim . '。', false, $code);
|
||
}
|
||
roleGrammarCheck($ledger, $functionAim . '并口干未记录。', false, 'POSITIVE_TO_UNKNOWN');
|
||
roleGrammarCheck($ledger, $functionAim . '同时心悸未记录。', false, 'NEGATIVE_TO_UNKNOWN');
|
||
roleGrammarCheck($ledger, $functionAim . '还舌象未记录。', false, 'RECORDED_TO_UNKNOWN');
|
||
roleGrammarCheck($ledger, $functionAim . '又患者无固定刺痛。', false, 'UNKNOWN_TO_NEGATIVE');
|
||
roleGrammarCheck($ledger, $functionAim . '另无胸痛。', false, 'NEGATION_SCOPE_WIDENED');
|
||
roleGrammarCheck($ledger, $functionAim . '并患者无药物过敏史。', false, 'NEGATION_SCOPE_WIDENED');
|
||
roleGrammarCheck($ledger, $functionAim . '并自述固定刺痛属待补依据。', false, 'UNKNOWN_TO_POSITIVE');
|
||
roleGrammarCheck($ledger, $functionAim . '还诊见舌红绛属待补依据。', false, 'TONGUE_FEATURE_NOT_IN_CURRENT_SOURCE');
|
||
roleGrammarCheck($safetyQuantity, $functionAim . '同时每次饮水量不多。', false, 'UNKNOWN_TO_POSITIVE');
|
||
roleGrammarCheck($ledger, $functionAim . '并舌无紫暗或瘀斑。', true);
|
||
roleGrammarCheck($ledger, $functionAim . '并舌无紫暗或瘀斑(s999)。', false, 'SOURCE_ANNOTATION_INVALID');
|
||
roleGrammarCheck($ledger, $functionAim . '。', true);
|
||
roleGrammarCheck($ledger, '方义上以养津改善口干及舌燥。', true);
|
||
roleGrammarCheck($ledger, '方义上以补气恢复推动固摄,并以养津改善口干舌燥,亦以收敛配合控制自汗。', true);
|
||
|
||
// A diagnostic evidence threshold is UNKNOWN, never a blanket heat/cold denial.
|
||
foreach (['本次没有', '当前无'] as $absence) foreach (['确定', '确认'] as $relation)
|
||
foreach (['热象', '寒象'] as $evidenceHead) {
|
||
$thresholdPrefix = $absence . '足以独立' . $relation;
|
||
$thresholdSuffix = '的阳性' . $evidenceHead . '。';
|
||
roleGrammarCheck($ledger, $thresholdPrefix . '热证' . $thresholdSuffix, true);
|
||
roleGrammarCheck($ledger, $thresholdPrefix . '口干' . $thresholdSuffix, false, 'POSITIVE_TO_UNKNOWN');
|
||
roleGrammarCheck($ledger, $thresholdPrefix . '心悸' . $thresholdSuffix, false, 'NEGATIVE_TO_UNKNOWN');
|
||
roleGrammarCheck($ledger, $thresholdPrefix . '舌象' . $thresholdSuffix, false, 'RECORDED_TO_UNKNOWN');
|
||
}
|
||
$thresholdPhrase = '本次没有足以独立确定热证的阳性热象';
|
||
$thresholdParts = (new ReflectionMethod(Guard::class, 'parts'))->invoke(null, $thresholdPhrase);
|
||
roleGrammarExpect(array_column($thresholdParts, 'state') === ['unknown']
|
||
&& array_column($thresholdParts, 'topic') === ['热证'], 'An explicit independent threshold retains its unknown diagnostic subject');
|
||
foreach (['本次没有足以确定热证的阳性热象。', '本次没有阳性热象。', '本次无独立寒象。',
|
||
'本次亦无独立阳性寒象。', '本次无寒象。'] as $bareSignDenial) {
|
||
roleGrammarCheck($ledger, $bareSignDenial, false, 'NEGATIVE_NOT_IN_SOURCE');
|
||
}
|
||
roleGrammarCheck($ledger, $thresholdPhrase . ',患者有固定刺痛。', false, 'UNKNOWN_TO_POSITIVE');
|
||
roleGrammarCheck($ledger, $thresholdPhrase . ',患者无固定刺痛。', false, 'UNKNOWN_TO_NEGATIVE');
|
||
roleGrammarCheck($ledger, $thresholdPhrase . ',口干未记录。', false, 'POSITIVE_TO_UNKNOWN');
|
||
roleGrammarCheck($ledger, $thresholdPhrase . ',舌红绛。', false, 'TONGUE_FEATURE_NOT_IN_CURRENT_SOURCE');
|
||
roleGrammarCheck($ledger, $thresholdPhrase . ',脉滑。', false, 'PULSE_FEATURE_NOT_IN_CURRENT_SOURCE');
|
||
roleGrammarCheck($ledger, $thresholdPhrase . ',患者无药物过敏史。', false, 'NEGATION_SCOPE_WIDENED');
|
||
roleGrammarCheck($ledger, $thresholdPhrase . '(s999)。', false, 'SOURCE_ANNOTATION_INVALID');
|
||
roleGrammarCheck($ledger, $thresholdPhrase . '(2026-10-22,' . $ids['present_illness'] . ')。', false, 'SOURCE_ANNOTATION_INVALID');
|
||
$knownHeat = Guard::build(array_replace($record, ['clinical_diagnosis' => '本次已确认热证。']));
|
||
roleGrammarCheck($knownHeat, $thresholdPhrase . '。', false, 'POSITIVE_TO_UNKNOWN');
|
||
roleGrammarCheck($ledger, '本次没有足以独立确定患者有固定刺痛的阳性热象。', false, 'UNKNOWN_TO_POSITIVE');
|
||
roleGrammarCheck($ledger, '本次没有足以独立确定热证并诉称固定刺痛的阳性热象。', false);
|
||
|
||
// The optional Qwen grammar repair ends a negative at an explicit gradual
|
||
// postfix occurrence, while the preceding qualifier and new symptom still count.
|
||
$gradualOccurrenceUnit = '表里:病位在里。依据:无恶寒发热、无明显急性感染诱因,症状逐渐出现,非外感表证。限制依据:s2。';
|
||
roleGrammarCheck($safetyLedger, $gradualOccurrenceUnit, true);
|
||
roleGrammarCheck($safetyLedger, str_replace('无明显急性感染诱因', '无急性感染诱因', $gradualOccurrenceUnit), false, 'NEGATION_SCOPE_WIDENED');
|
||
foreach (['逐渐', '渐次', '陆续'] as $gradual) {
|
||
roleGrammarCheck($ledger, '无心悸,固定刺痛' . $gradual . '出现。', false, 'UNKNOWN_TO_POSITIVE');
|
||
roleGrammarCheck($ledger, '无心悸,口干' . $gradual . '出现。', true);
|
||
roleGrammarCheck($ledger, '无心悸,口干' . $gradual . '出现,舌红绛。', false, 'TONGUE_FEATURE_NOT_IN_CURRENT_SOURCE');
|
||
roleGrammarCheck($ledger, '无胸痛,口干' . $gradual . '出现。', false, 'NEGATION_SCOPE_WIDENED');
|
||
}
|
||
roleGrammarCheck($safetyLedger, '患者无急性感染诱因。', false, 'NEGATION_SCOPE_WIDENED');
|
||
roleGrammarCheck($ledger, '无心悸,胸痛,口干逐渐出现。', false, 'NEGATION_SCOPE_WIDENED');
|
||
roleGrammarCheck($ledger, '无心悸,固定刺痛,口干逐渐出现。', false, 'UNKNOWN_TO_NEGATIVE');
|
||
roleGrammarCheck($safetyLedger, '日常记录显示活动后乏力、汗出较明显,无胸痛或晕厥。', false, 'NEGATION_SCOPE_WIDENED');
|
||
roleGrammarCheck($safetyLedger, '2026-10-22活动记录见“活动后乏力、汗出较明显,无胸痛或晕厥”。', true);
|
||
|
||
// v40: a complete evidence-limited exclusion and a locally linked review
|
||
// question must not borrow polarity from an earlier observed statement.
|
||
$syndromeExclusion = '仍不能仅凭证候排除心肺、感染或代谢因素';
|
||
$modalReviewHead = '适用前须确认仍以气虚、自汗、津少为主';
|
||
$linkedReviewQuestion = '并复核气短、自汗有无心肺、感染或代谢原因';
|
||
$linkedReview = $modalReviewHead . ',' . $linkedReviewQuestion;
|
||
roleGrammarCheck($safetyLedger, '自汗及气短虽有虚象,' . $syndromeExclusion . '。', true);
|
||
roleGrammarCheck($safetyLedger, $linkedReview . '。', true);
|
||
roleGrammarCheck($safetyLedger, str_replace('有无', '是否', $linkedReview) . '。', true);
|
||
roleGrammarCheck($safetyLedger, '适用前需核实气短、自汗为主,复核气短有无感染原因。', true);
|
||
roleGrammarCheck($safetyLedger, '适用前应再次确认气虚、自汗为主,再核查气短是否感染原因。', true);
|
||
foreach (['仍不能仅凭证候排除感染。', '仍不得只据证候完全排除感染。',
|
||
'不能仅凭本次资料排除感染。', '不能仅凭当前阴性记录排除感染。'] as $limit) {
|
||
roleGrammarCheck($safetyLedger, $limit, true);
|
||
}
|
||
$reviewAdjacentAssertions = [
|
||
['患者有固定刺痛', 'UNKNOWN_TO_POSITIVE'],
|
||
['患者无固定刺痛', 'UNKNOWN_TO_NEGATIVE'],
|
||
['口干未记录', 'POSITIVE_TO_UNKNOWN'],
|
||
['舌象未记录', 'RECORDED_TO_UNKNOWN'],
|
||
['舌红绛', 'TONGUE_FEATURE_NOT_IN_CURRENT_SOURCE'],
|
||
['脉滑', 'PULSE_FEATURE_NOT_IN_CURRENT_SOURCE'],
|
||
];
|
||
foreach ([$syndromeExclusion, $linkedReview] as $reviewRole) {
|
||
foreach ($reviewAdjacentAssertions as [$assertion, $code]) {
|
||
roleGrammarCheck($safetyLedger, $reviewRole . ',' . $assertion . '。', false, $code);
|
||
roleGrammarCheck($safetyLedger, $assertion . ',' . $reviewRole . '。', false, $code);
|
||
}
|
||
foreach (['并诉称固定刺痛', '还诉称固定刺痛'] as $tail) {
|
||
roleGrammarCheck($safetyLedger, $reviewRole . $tail . '。', false, 'UNKNOWN_TO_POSITIVE');
|
||
}
|
||
foreach (['并诊见舌红绛', '又查见舌红绛'] as $tail) {
|
||
roleGrammarCheck($safetyLedger, $reviewRole . $tail . '。', false, 'TONGUE_FEATURE_NOT_IN_CURRENT_SOURCE');
|
||
}
|
||
roleGrammarCheck($safetyLedger, $reviewRole . '同时脉滑。', false, 'PULSE_FEATURE_NOT_IN_CURRENT_SOURCE');
|
||
foreach (['且', '但', '而', '并', '同时', '还', '又', '另', ''] as $join) {
|
||
roleGrammarCheck($safetyLedger, $reviewRole . $join . '患者有固定刺痛。', false, 'UNKNOWN_TO_POSITIVE');
|
||
}
|
||
foreach (['且', '并', '同时', '还', '又', '另'] as $join) {
|
||
roleGrammarCheck($safetyLedger, $reviewRole . $join . '口干未记录。', false, 'POSITIVE_TO_UNKNOWN');
|
||
roleGrammarCheck($safetyLedger, $reviewRole . $join . '心悸未记录。', false, 'NEGATIVE_TO_UNKNOWN');
|
||
}
|
||
roleGrammarCheck($safetyLedger, $reviewRole . '今晨固定刺痛发作。', false, 'UNKNOWN_TO_POSITIVE');
|
||
roleGrammarCheck($safetyQuantity, $reviewRole . '每次饮水量不多。', false, 'UNKNOWN_TO_POSITIVE');
|
||
roleGrammarCheck($safetyLedger, $reviewRole . '2026-10-23患者无固定刺痛。', false, 'UNKNOWN_TO_NEGATIVE');
|
||
roleGrammarCheck($safetyLedger, $reviewRole . ',患者有感染。', false, 'NEGATIVE_TO_POSITIVE');
|
||
roleGrammarCheck($safetyLedger, $reviewRole . ',患者无急性感染诱因。', false, 'NEGATION_SCOPE_WIDENED');
|
||
roleGrammarCheck($safetyLedger, $reviewRole . ',患者无药物过敏史。', false, 'NEGATION_SCOPE_WIDENED');
|
||
roleGrammarCheck($safetyLedger, $reviewRole . '(s999)。', false, 'SOURCE_ANNOTATION_INVALID');
|
||
roleGrammarCheck($safetyLedger, $reviewRole . '(2026-10-22,s2)。', false, 'SOURCE_ANNOTATION_INVALID');
|
||
}
|
||
roleGrammarCheck($safetyLedger, $linkedReviewQuestion . '。', false, 'NEGATION_SCOPE_WIDENED');
|
||
roleGrammarCheck($safetyLedger, '有无感染。', false, 'NEGATION_SCOPE_WIDENED');
|
||
roleGrammarCheck($safetyLedger, $modalReviewHead . ',复核无固定刺痛。', false, 'UNKNOWN_TO_NEGATIVE');
|
||
roleGrammarCheck($safetyLedger, '适用前须确认患者有固定刺痛,' . $linkedReviewQuestion . '。', false, 'UNKNOWN_TO_POSITIVE');
|
||
roleGrammarCheck($safetyLedger, $modalReviewHead . ',并复核患者有固定刺痛。', false, 'UNKNOWN_TO_POSITIVE');
|
||
roleGrammarCheck($safetyLedger, $modalReviewHead . ',并复核气短有无患者有固定刺痛。', false, 'UNKNOWN_TO_POSITIVE');
|
||
roleGrammarCheck($safetyLedger, $modalReviewHead . ',并复核气短有无感染患者有固定刺痛。', false, 'UNKNOWN_TO_POSITIVE');
|
||
roleGrammarCheck($safetyLedger, $modalReviewHead . ',并复核气短有无感染诊见舌红绛。', false, 'TONGUE_FEATURE_NOT_IN_CURRENT_SOURCE');
|
||
$multipleQuestionProse = (new ReflectionMethod(Guard::class, 'linkedReviewRequestProse'))->invoke(null,
|
||
$modalReviewHead . ',并复核气短有无感染有无心肺原因');
|
||
roleGrammarExpect(preg_match_all('/有无/u', $multipleQuestionProse) === 2,
|
||
'A multiple-interrogative fragment receives no linked-question normalization');
|
||
roleGrammarCheck($safetyLedger, $modalReviewHead . ',并复核气短有无无固定刺痛。', false, 'UNKNOWN_TO_NEGATIVE');
|
||
roleGrammarCheck($safetyLedger, '仍不能仅凭患者无固定刺痛排除感染。', false, 'UNKNOWN_TO_NEGATIVE');
|
||
$reviewQuestionProse = (new ReflectionMethod(Guard::class, 'reportProse'))->invoke(null, $linkedReview);
|
||
roleGrammarExpect(!str_contains($reviewQuestionProse, '有无') && !str_contains($reviewQuestionProse, '感染'),
|
||
'A linked question is recognized while its typed modal context still exists');
|
||
$exclusionProse = (new ReflectionMethod(Guard::class, 'reportProse'))->invoke(null, '自汗及气短虽有虚象,' . $syndromeExclusion);
|
||
roleGrammarExpect(str_contains($exclusionProse, '有虚象') && !str_contains($exclusionProse, '感染'),
|
||
'The observed prefix remains visible while only the bounded exclusion fragment is normalized');
|
||
$reviewAndExclusionUnits = [
|
||
'反证与局限:寒热均未定,且限制依据:(s2);因此不能把本方向解释为治疗阴虚火旺,也不能加入温阳或清热立法。'
|
||
. '便稍干、小便略多还可能受饮水、血糖等影响;自汗及气短虽有虚象,仍不能仅凭证候排除心肺、感染或代谢因素。'
|
||
. '若出现明显胸痛、心悸、喘促、浮肿、意识改变或急性高热,应先行现代医学评估,而非继续按原方向处理。',
|
||
'反证与局限:寒热未定,限制依据:(s2);本方向不能解释为治疗阴虚火旺,亦不据此采用温阳或清热法。'
|
||
. '适用前须确认仍以气虚、自汗、津少为主,并复核气短、自汗有无心肺、感染或代谢原因;症状性质改变时应重新辨证。',
|
||
];
|
||
foreach ($reviewAndExclusionUnits as $reviewAndExclusionUnit) roleGrammarCheck($safetyLedger, $reviewAndExclusionUnit, true);
|
||
|
||
// Actual terminal v41 C2 rejections: an explicit unknown-state noun and the
|
||
// colon immediately before a valid source citation must not become a positive
|
||
// symptom. Neither closed role may erase a known finding or borrow an adjacent
|
||
// assertion, and the stream must preserve the original unmodified units.
|
||
$unknownStateUnits = [
|
||
'血:面色少华、脉细可作血少鉴别,但无血常规、头晕、唇甲色泽等证据,现有资料不足以确定血虚。'
|
||
. '固定刺痛为未记录状态(s2),不能据此排除血瘀。',
|
||
'血:面色少华、脉细仅提示血少鉴别;血常规、头晕、唇甲色泽未记录,现有资料不足以确定血虚。'
|
||
. '固定刺痛属待补依据:(s2),不能判断血瘀与否。',
|
||
];
|
||
foreach ($unknownStateUnits as $unit) roleGrammarCheck($safetyLedger, $unit, true);
|
||
foreach (['为未记录状态', '属待补依据:', '属于未记录状态:'] as $role) {
|
||
roleGrammarCheck($ledger, '固定刺痛' . $role . '。', true);
|
||
roleGrammarCheck($ledger, '固定刺痛' . $role . '(' . $ids['present_illness'] . ')。', true);
|
||
roleGrammarCheck($ledger, '口干' . $role . '。', false, 'POSITIVE_TO_UNKNOWN');
|
||
roleGrammarCheck($ledger, '心悸' . $role . '。', false, 'NEGATIVE_TO_UNKNOWN');
|
||
roleGrammarCheck($ledger, '舌象' . $role . '。', false, 'RECORDED_TO_UNKNOWN');
|
||
roleGrammarCheck($ledger, '脉象' . $role . '。', false, 'RECORDED_TO_UNKNOWN');
|
||
foreach (['自述固定刺痛', '主诉固定刺痛', '今晨固定刺痛发作', '患者有固定刺痛'] as $assertion) {
|
||
roleGrammarCheck($ledger, $assertion . $role . '。', false, 'UNKNOWN_TO_POSITIVE');
|
||
}
|
||
foreach ([
|
||
['患者有固定刺痛', 'UNKNOWN_TO_POSITIVE'],
|
||
['患者无固定刺痛', 'UNKNOWN_TO_NEGATIVE'],
|
||
['口干未记录', 'POSITIVE_TO_UNKNOWN'],
|
||
['舌象未记录', 'RECORDED_TO_UNKNOWN'],
|
||
['舌红绛', 'TONGUE_FEATURE_NOT_IN_CURRENT_SOURCE'],
|
||
['脉滑', 'PULSE_FEATURE_NOT_IN_CURRENT_SOURCE'],
|
||
] as [$assertion, $code]) {
|
||
roleGrammarCheck($ledger, '固定刺痛' . $role . ',' . $assertion . '。', false, $code);
|
||
roleGrammarCheck($ledger, $assertion . ',固定刺痛' . $role . '。', false, $code);
|
||
}
|
||
foreach (['且', '并', '同时', '还', '又', '另', ''] as $join) {
|
||
roleGrammarCheck($ledger, '固定刺痛' . $role . $join . '患者有固定刺痛。', false, 'UNKNOWN_TO_POSITIVE');
|
||
}
|
||
roleGrammarCheck($ledger, '固定刺痛' . $role . '(s999)。', false, 'SOURCE_ANNOTATION_INVALID');
|
||
roleGrammarCheck($ledger, '固定刺痛' . $role . '(2026-10-22,' . $ids['present_illness'] . ')。', false, 'SOURCE_ANNOTATION_INVALID');
|
||
}
|
||
|
||
// Each complete unit preserves byte identity, including split UTF-8 and CRLF.
|
||
$units = [
|
||
'固定刺痛属待补依据(' . $ids['present_illness'] . '),且舌无紫暗或瘀斑(' . $ids['tongue'] . ')。',
|
||
'肾系定位因腰酸耳鸣未记录而待定。每次饮水不多,舌淡红苔少。',
|
||
'舌脉未呈黄腻、滑、紧等所见(' . $ids['tongue'] . '、' . $ids['pulse'] . ')。',
|
||
];
|
||
$wire = implode("\r\n\r\n", $units) . "\r\n\r\n";
|
||
foreach ([1, 2, 7, 31] as $size) {
|
||
$visible = '';
|
||
$stream = new TcmClinicalReportStream($ledger, static function (string $delta) use (&$visible): bool { $visible .= $delta; return true; });
|
||
$pushed = true;
|
||
foreach (str_split($wire, $size) as $chunk) if (!$stream->push($chunk)) { $pushed = false; break; }
|
||
roleGrammarExpect($pushed && $stream->finish() && !$stream->rejected(), 'Complete role units survive byte chunk size ' . $size);
|
||
roleGrammarExpect($visible === $wire, 'Complete accepted role units preserve every byte at chunk size ' . $size);
|
||
}
|
||
|
||
echo 'Clinical fact state bounded role grammar: ' . $checks . " assertions passed (offline, original streaming bytes).\n";
|