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

1127 lines
99 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\TcmClinicalFactStateGuard as Guard;
use app\adminapi\service\TcmClinicalReportStream;
$count = 0;
$guardOnly = in_array('--guard-only', $argv ?? [], true);
$expect = static function (bool $condition, string $message) use (&$count): void {
++$count;
if (!$condition) throw new RuntimeException($message);
};
$snapshot = (new ReflectionMethod(PatientAiReportLogic::class, 'buildSourceSnapshotFromRows'))
->invoke(null, CompleteTcmCase::rows());
$submission = (new ReflectionMethod(PatientAiReportLogic::class, 'sanitizeSnapshotForUpstream'))
->invoke(null, AiRequiredSource::submission($snapshot));
$ledger = Guard::build(CompleteTcmCase::current(), $submission);
$expect($ledger['version'] === 'clinical-source-state-v2', 'The ledger schema/version is unchanged.');
$expect($ledger['sources']['s5']['quote'] === '本次询问无已知药物过敏史'
&& $ledger['sources']['s5']['date'] === '2026-10-23'
&& $ledger['sources']['s5']['scope'] === 'current', 'The real pure fixture projection proves the s5 quote/date/scope.');
$expect($ledger['sources']['s2']['quote'] === CompleteTcmCase::current()['present_illness']
&& $ledger['sources']['s14']['quote'] === CompleteTcmCase::current()['present_illness'],
'Both original rejection source IDs refer to the unchanged actual fixture history.');
$check = static function (string $text, bool $valid, string $label, string $code = '', ?array $caseLedger = null) use ($ledger, $expect, $guardOnly): void {
$targetLedger = $caseLedger ?? $ledger;
$before = $text;
$review = Guard::validate($targetLedger, $text);
$expect($review['valid'] === $valid, $label . ': ' . json_encode($review, JSON_UNESCAPED_UNICODE));
if ($code !== '') $expect(in_array($code, array_column($review['issues'], 'code'), true), $label . ': expected ' . $code . ': ' . json_encode([$text, $review], JSON_UNESCAPED_UNICODE));
$expect($text === $before, $label . ': validation preserves the original string.');
if ($guardOnly) return;
$raw = preg_match('/(?:\r?\n){2}$/D', $text) ? $text : $text . "\n\n";
foreach ([1, 2, 7, 31] as $size) {
$visible = '';
$stream = new TcmClinicalReportStream($targetLedger, static function (string $delta) use (&$visible): bool {
$visible .= $delta;
return true;
});
$pushed = true;
foreach (str_split($raw, $size) as $chunk) if (!$stream->push($chunk)) { $pushed = false; break; }
$finished = $stream->finish();
$expect(($pushed && $finished) === $valid && $stream->rejected() === !$valid, $label . ': stream state, chunk ' . $size);
$expect($visible === ($valid ? $raw : ''), $label . ': exact bytes or zero rejected-unit bytes, chunk ' . $size);
$expect($stream->acceptedContent() === $visible, $label . ': accepted/visible identity, chunk ' . $size);
}
};
// Portable exact retained units, also compared to the ignored artifact when present.
$units = [
"适用前须核实:是否存在感冒、感染或其他急性疾病表现?是否存在舌苔转厚腻、胸脘痞闷等不宜收敛或滋腻的情况?是否另有处方药、保健品或中药同用?\n\n",
"适用前须核实:是否另用处方药、保健品或中药?“本次询问无已知药物过敏史”(s5)仅代表已知范围,新用药仍须审核过敏及相互作用。\n\n",
];
$artifact = dirname(__DIR__, 2) . '/app/artifacts/ai_concurrency/tcm_complete_v43r5_single_openai_20261024_0_openai.rejected.json';
if (is_file($artifact)) {
$saved = json_decode((string) file_get_contents($artifact), true, 512, JSON_THROW_ON_ERROR);
$expect(array_column($saved['rejections'], 'unit') === $units, 'Portable units exactly equal both original retained rejection units.');
}
foreach ($units as $i => $unit) $check($unit, true, 'Exact retained unit ' . $i);
foreach ([
'是否存在心悸?', '是否有固定刺痛?', '是否另有处方药、保健品或中药同用?',
'是否另用处方药、保健品或中药?', '是否使用中药?', '有无心悸或固定刺痛?',
'是否存在舌质红绛、脉滑?', '是否存在不宜运动的情况?',
'适用前须核实:是否存在心悸?',
'适用前须复核:是否有固定刺痛?是否存在舌苔厚腻?',
] as $question) $check($question, true, 'A terminated neutral question owns only its object: ' . $question);
foreach ([
['是否存在心悸。', 'NEGATIVE_TO_POSITIVE'],
['是否有固定刺痛', 'UNKNOWN_TO_POSITIVE'],
['是否存在舌苔厚腻。', 'TONGUE_FEATURE_NOT_IN_CURRENT_SOURCE'],
['适用前须核实:患者有心悸?', 'NEGATIVE_TO_POSITIVE'],
['适用前须核实:患者无固定刺痛?', 'UNKNOWN_TO_NEGATIVE'],
['是否存在患者有心悸?', 'NEGATIVE_TO_POSITIVE'],
['是否有患者无固定刺痛?', 'UNKNOWN_TO_NEGATIVE'],
['是否存在感染、无固定刺痛?', 'UNKNOWN_TO_NEGATIVE'],
['是否存在感染或有心悸?', 'NEGATIVE_TO_POSITIVE'],
] as [$text, $code]) $check($text, false, 'No absent terminator/header/embedded assertion exemption', $code);
$assertions = [
['患者有心悸', 'NEGATIVE_TO_POSITIVE'],
['患者无固定刺痛', 'UNKNOWN_TO_NEGATIVE'],
['患者有固定刺痛', 'UNKNOWN_TO_POSITIVE'],
['心悸未记录', 'NEGATIVE_TO_UNKNOWN'],
['口干未记录', 'POSITIVE_TO_UNKNOWN'],
['舌质红绛', 'TONGUE_FEATURE_NOT_IN_CURRENT_SOURCE'],
['脉滑', 'PULSE_FEATURE_NOT_IN_CURRENT_SOURCE'],
['无五心烦热', 'NEGATION_SCOPE_WIDENED'],
];
foreach ([',', ',', '且', '但', '并', '并且', '同时', '还', '又', '另'] as $joiner) {
foreach ($assertions as [$assertion, $code]) {
$check('适用前须核实:是否存在感染' . $joiner . $assertion . '?', false,
'An assertion cannot borrow a question mark through ' . $joiner . ': ' . $assertion, $code);
}
}
foreach ([
['是否存在感染患者有心悸?', 'NEGATIVE_TO_POSITIVE'],
['是否存在感染并诉称固定刺痛?', 'UNKNOWN_TO_POSITIVE'],
['是否存在感染并诊见舌质红绛?', 'TONGUE_FEATURE_NOT_IN_CURRENT_SOURCE'],
['是否存在感染且需复核,心悸未记录?', 'NEGATIVE_TO_UNKNOWN'],
['是否存在感染且需复核,口干未记录?', 'POSITIVE_TO_UNKNOWN'],
['患者有心悸,是否存在感染?', 'NEGATIVE_TO_POSITIVE'],
[rtrim($units[0]) . '患者无固定刺痛。', 'UNKNOWN_TO_NEGATIVE'],
[rtrim($units[0]) . '心悸未记录。', 'NEGATIVE_TO_UNKNOWN'],
] as [$text, $code]) $check($text, false, 'Actual assertions before/after/inside question fragments survive', $code);
$quote = '“本次询问无已知药物过敏史”(s5)';
foreach (['s9', 's10'] as $id) {
$sourceQuote = '“' . $ledger['sources'][$id]['quote'] . '”(' . $id . ')';
$check($sourceQuote . '仅说明既有记录的范围。', true, 'The full-source citation boundary applies to another recorded field: ' . $id);
$check($sourceQuote . '仅说明既有记录的范围且患者有心悸。', false,
'Another recorded field cannot lend source scope to an actual assertion: ' . $id, 'NEGATIVE_TO_POSITIVE');
}
foreach (['仅代表已知范围', '仅说明既有记录的范围', '新用药仍须复核'] as $comment) {
$check($quote . $comment . '。', true, 'Source-bound whole quote ends before ordinary commentary.');
foreach ($assertions as [$assertion, $code]) {
$check($quote . $comment . ',' . $assertion . '。', false,
'Quote commentary retains an actual clinical tail: ' . $assertion, $code);
}
}
foreach (['且', '但', '并', '并且', '同时', '还', '又', '另'] as $joiner) {
$check($quote . '仅代表已知范围' . $joiner . '患者有心悸。', false,
'No commentary/joiner consumes the actual assertion: ' . $joiner, 'NEGATIVE_TO_POSITIVE');
$check($quote . $joiner . '患者无固定刺痛。', false,
'A direct post-citation assertion stays separate: ' . $joiner, 'UNKNOWN_TO_NEGATIVE');
}
foreach ([
['“本次询问无已知药物过敏史”(s9)仅代表已知范围。', 'SOURCE_ANNOTATION_INVALID'],
['“本次询问无已知药物过敏史”(s9、s10)仅代表已知范围。', 'SOURCE_ANNOTATION_INVALID'],
['“本次询问无已知药物过敏史”(s999)仅代表已知范围。', 'SOURCE_ANNOTATION_INVALID'],
['“本次询问无已知药物过敏史”(2026-10-22,s5)仅代表已知范围。', 'SOURCE_ANNOTATION_INVALID'],
['“本次询问无已知药物过敏史”(2026-13-99,s5)仅代表已知范围。', 'SOURCE_ANNOTATION_INVALID'],
['“本次询问无药物过敏史”(s5)仅代表已知范围。', 'NEGATIVE_NOT_IN_SOURCE'],
['无药物过敏史。', 'NEGATION_SCOPE_WIDENED'],
['“无”(s5)药物过敏史。', 'NEGATION_SCOPE_WIDENED'],
['“本次询问无已知药物过敏史”(s5,患者无固定刺痛)仅代表已知范围。', 'UNKNOWN_TO_NEGATIVE'],
['患者有心悸,' . $quote . '仅代表已知范围。', 'NEGATIVE_TO_POSITIVE'],
[$quote . '仅代表已知范围且口干未记录。', 'POSITIVE_TO_UNKNOWN'],
[$quote . '仅代表已知范围并心悸未记录。', 'NEGATIVE_TO_UNKNOWN'],
[$quote . '仅代表已知范围舌质红绛。', 'TONGUE_FEATURE_NOT_IN_CURRENT_SOURCE'],
[$quote . '新用药仍须复核舌质红绛。', 'TONGUE_FEATURE_NOT_IN_CURRENT_SOURCE'],
[str_replace('(s5)', '(s9)', rtrim($units[1])), 'SOURCE_ANNOTATION_INVALID'],
[str_replace('(s5)', '(2026-10-22,s5)', rtrim($units[1])), 'SOURCE_ANNOTATION_INVALID'],
] as [$text, $code]) $check($text, false, 'A citation boundary requires its own intact source proof', $code);
// Offset-sensitive source/date proof still works after an earlier inserted boundary.
$check($quote . '仅代表已知范围;“本次面诊记录:舌质淡红,舌面少津,苔少;无黄腻苔、紫暗或瘀斑。”(2026-10-23,s9)。',
true, 'Internal boundary insertion precedes quote offsets for every later source.');
$check($quote . '仅代表已知范围;“本次面诊记录:舌质淡红,舌面少津,苔少;无黄腻苔、紫暗或瘀斑。”(2026-10-22,s9)。',
false, 'Later source date proof cannot drift after the inserted boundary.', 'SOURCE_ANNOTATION_INVALID');
// v43r6: source locators carry no clinical polarity; 合并 is a compound, not a list joiner.
$r6Units = [
"血:面色少华、脉细可供血虚鉴别(s3、s10),但舌质仅淡红,且头晕、唇甲色泽等资料未记录,现有资料不足以确定血虚;固定刺痛相关情况见s2,血瘀不立。\n\n",
"适用前须核实:是否存在影响黄芪、防风、白术使用的个体禁忌、近期感染、肝肾功能变化,及与全部在用药、保健品的相互作用?妊娠问题不适用于本例男性;其他合并用药未记录。\n\n",
];
$r6Artifact = dirname(__DIR__, 2) . '/app/artifacts/ai_concurrency/tcm_complete_v43r6_single_openai_20261024_0_openai.rejected.json';
if (is_file($r6Artifact)) {
$saved = json_decode((string) file_get_contents($r6Artifact), true, 512, JSON_THROW_ON_ERROR);
$expect(array_column($saved['rejections'], 'unit') === $r6Units, 'Both r6 portable units equal the retained original bytes.');
}
foreach ($r6Units as $i => $unit) $check($unit, true, 'Exact retained r6 unit ' . $i);
foreach ([
'固定刺痛相关情况见s2。', '口干资料见s2。', '心悸记录参见s2。',
'固定刺痛、心悸相关情况见s2。', '舌象结果见s9。', '脉象记录见s10。',
'2026-10-23固定刺痛相关情况见s2。',
'其他合并用药未记录。', '其他合并用药、保健品未记录。',
'固定刺痛相关情况见s2,其他合并用药未记录。',
'是否另有合并用药?',
] as $text) $check($text, true, 'A proved locator or additional-data gap asserts no new patient finding: ' . $text);
foreach ([
['固定刺痛相关情况见s999。', 'SOURCE_ANNOTATION_INVALID'],
['固定刺痛相关情况见s9。', 'SOURCE_ANNOTATION_INVALID'],
['固定刺痛、头晕相关情况见s2。', 'SOURCE_ANNOTATION_INVALID'],
['舌象结果见s10。', 'SOURCE_ANNOTATION_INVALID'],
['2026-10-22固定刺痛相关情况见s2。', 'SOURCE_ANNOTATION_INVALID'],
['历史固定刺痛相关情况见s2。', 'SOURCE_ANNOTATION_INVALID'],
['患者有固定刺痛相关情况见s2。', 'UNKNOWN_TO_POSITIVE'],
['患者无固定刺痛相关情况见s2。', 'UNKNOWN_TO_NEGATIVE'],
['诉称固定刺痛相关情况见s2。', 'UNKNOWN_TO_POSITIVE'],
['固定刺痛相关情况见s2,须复核,心悸未记录。', 'NEGATIVE_TO_UNKNOWN'],
['固定刺痛相关情况见s2,其他合并用药、口干未记录。', 'POSITIVE_TO_UNKNOWN'],
['其他合并用药、心悸未记录。', 'NEGATIVE_TO_UNKNOWN'],
['其他合并用药未记录而舌质红绛。', 'TONGUE_FEATURE_NOT_IN_CURRENT_SOURCE'],
['其他合并用药未记录并诉称固定刺痛。', 'UNKNOWN_TO_POSITIVE'],
['其他合并用药未记录且合并有心悸。', 'NEGATIVE_TO_POSITIVE'],
['肾系定位因自述固定刺痛未记录而待定。', 'UNKNOWN_TO_POSITIVE'],
['肾系定位因自述固定刺痛未记录而仍待定。', 'UNKNOWN_TO_POSITIVE'],
['二甲双胍未记录。', 'POSITIVE_TO_UNKNOWN'],
['胰岛素未记录。', 'NEGATIVE_TO_UNKNOWN'],
['用药未记录。', 'POSITIVE_TO_UNKNOWN'],
] as [$text, $code]) $check($text, false, 'Reference proof and recorded clinical/medication states stay enforced', $code);
foreach ([',', '且', '但', '并', '并且', '同时', '还', '又', '另'] as $joiner) {
foreach ($assertions as [$assertion, $code]) {
$check('固定刺痛相关情况见s2' . $joiner . $assertion . '。', false,
'A locator cannot swallow a following assertion through ' . $joiner, $code);
$check('其他合并用药未记录' . $joiner . $assertion . '。', false,
'The additional-data gap cannot swallow a following assertion through ' . $joiner, $code);
}
}
$recordedAdditional = Guard::build(['id' => 2, 'diagnosis_date' => '2026-10-23',
'current_medications' => '二甲双胍。现用的其他合并用药为阿卡波糖。未使用胰岛素。']);
foreach ([['其他合并用药未记录。', 'POSITIVE_TO_UNKNOWN'], ['合并用药未记录。', 'POSITIVE_TO_UNKNOWN'],
['阿卡波糖未记录。', 'POSITIVE_TO_UNKNOWN'], ['胰岛素未记录。', 'NEGATIVE_TO_UNKNOWN']] as [$text, $code]) {
$check($text, false, 'Actually recorded additional medication cannot become unknown', $code, $recordedAdditional);
}
$compoundFinding = Guard::build(['id' => 3, 'diagnosis_date' => '2026-10-23', 'present_illness' => '本次合并心悸。']);
$check('心悸未记录。', false, 'The compound correction also retains a recorded non-medication finding', 'POSITIVE_TO_UNKNOWN', $compoundFinding);
// v43r8: an anchored pending list and a complete future precondition are operators,
// rather than new present patient observations. No topic/formula list is consulted.
$r8Units = [
'tcm_complete_v43r8_single_openai_20261024_0_openai' => "适用前须核实:是否存在明显五心烦热、潮热盗汗等表现的近期变化;是否存在畏寒肢冷等寒象变化;是否有咽干、干咳、纳差腹胀及长期便溏等补充证据?\n\n",
'tcm_complete_v43r8_supplement_ooc2_20261024_0_openai' => "适用前须核实:是否另有未记录的处方药、非处方药、保健品或草药;是否存在吞咽困难、胃肠不耐受或近期血糖异常波动;是否有拟用药材或辅料过敏;是否出现感染、脱水或肝肾指标变化。\n\n",
'tcm_complete_v43r8_supplement_ooc2_20261024_1_openai' => "适用条件:医师复诊确认当前仍以乏力、活动后气短、自汗、口干少津、弱脉为核心,且不存在更需优先处理的感染、心肺事件、脱水或明显血糖失控时,方可考虑该方向。\n\n",
];
foreach ($r8Units as $stem => $unit) {
$artifact = dirname(__DIR__, 2) . '/app/artifacts/ai_concurrency/' . $stem . '.rejected.json';
if (is_file($artifact)) {
$saved = json_decode((string) file_get_contents($artifact), true, 512, JSON_THROW_ON_ERROR);
$expect(array_column($saved['rejections'], 'unit') === [$unit], 'Portable r8 unit equals the exact retained bytes: ' . $stem);
}
$check($unit, true, 'Exact r8 pending-question/precondition unit: ' . $stem);
}
foreach ([';', ';'] as $separator) foreach (['?', '?', '。', ''] as $ending) {
$check('适用前须核实:是否存在心悸' . $separator . '是否有固定刺痛' . $separator . '是否出现舌质红绛' . $ending,
true, 'Only consecutive same-line neutral questions inherit the pending operator.');
}
foreach ([
'适用前须核实:是否存在心悸。',
'适用前需复核:是否另有未记录的处方药、非处方药。',
'适用前应确认:是否存在药物不耐受。',
'是否存在胃肠不耐受?',
'适用前须核实:是否有未采集的体征;是否可见舌质红绛。',
'适用条件:医师复诊确认有心悸时,方可考虑该方向。',
'适用条件:复诊确认存在固定刺痛时,才可评估该方向。',
'适用条件:医生在复诊时核实无固定刺痛时,方可讨论后续方案。',
'适用条件:医师复诊确认不存在感染且有口干时,方可考虑该方向。',
'适用条件:医师复诊确认舌质红绛、脉滑时,方可考虑该方向。',
'适用条件:医师复诊确认无五心烦热时,方可考虑该方向。',
'适用条件:医师复诊确认当前仍以固定刺痛为核心时,方可考虑该方向。',
] as $text) $check($text, true, 'A prospective question/condition does not assert current polarity: ' . $text);
foreach ([
['是否存在心悸;是否有固定刺痛。', 'NEGATIVE_TO_POSITIVE'],
["适用前须核实:是否有心悸;\n是否存在固定刺痛。", 'UNKNOWN_TO_POSITIVE'],
['适用前须核实:患者有心悸;是否有固定刺痛。', 'NEGATIVE_TO_POSITIVE'],
['适用前须核实:是否存在感染不伴固定刺痛。', 'UNKNOWN_TO_NEGATIVE'],
['适用前须核实:是否存在感染;患者有固定刺痛。', 'UNKNOWN_TO_POSITIVE'],
['适用前须核实:是否存在感染;目前无固定刺痛。', 'UNKNOWN_TO_NEGATIVE'],
['适用前须核实:是否存在感染;心悸未记录。', 'NEGATIVE_TO_UNKNOWN'],
['适用条件:医师复诊确认患者已存在固定刺痛时,方可考虑该方向。', 'UNKNOWN_TO_POSITIVE'],
['适用条件:医师复诊确认患者已无固定刺痛时,方可考虑该方向。', 'UNKNOWN_TO_NEGATIVE'],
['适用条件:医师复诊确认目前有心悸时,方可考虑该方向。', 'NEGATIVE_TO_POSITIVE'],
['适用条件:医师复诊确认当前有心悸时,方可考虑该方向。', 'NEGATIVE_TO_POSITIVE'],
['适用条件:医师复诊确认本次有心悸时,方可考虑该方向。', 'NEGATIVE_TO_POSITIVE'],
['适用条件:医师复诊确认目前无五心烦热时,方可考虑该方向。', 'NEGATION_SCOPE_WIDENED'],
['适用条件:医师复诊确认口干未记录时,方可考虑该方向。', 'POSITIVE_TO_UNKNOWN'],
['适用条件:医师复诊确认心悸未记录时,方可考虑该方向。', 'NEGATIVE_TO_UNKNOWN'],
['适用条件:医师复诊确认诊见舌质红绛时,方可考虑该方向。', 'TONGUE_FEATURE_NOT_IN_CURRENT_SOURCE'],
['适用条件:医师复诊确认条件满足时,方可考虑患者有心悸。', 'NEGATIVE_TO_POSITIVE'],
['适用条件:医师复诊确认条件满足时,方可考虑心悸未记录。', 'NEGATIVE_TO_UNKNOWN'],
['适用条件:医师复诊确认条件满足时,方可考虑舌质红绛。', 'TONGUE_FEATURE_NOT_IN_CURRENT_SOURCE'],
['适用条件:医师确认存在固定刺痛时,方可考虑该方向。', 'UNKNOWN_TO_POSITIVE'],
['适用条件:医师复诊确认有心悸。', 'NEGATIVE_TO_POSITIVE'],
['患者有固定刺痛,适用条件:医师复诊确认条件满足时,方可考虑该方向。', 'UNKNOWN_TO_POSITIVE'],
['适用条件:医师复诊确认条件满足时,方可考虑该方向,须复核,心悸未记录。', 'NEGATIVE_TO_UNKNOWN'],
['适用前须核实:是否存在感染(s999);是否出现心悸。', 'SOURCE_ANNOTATION_INVALID'],
['适用条件:医师复诊确认条件满足(s999)时,方可考虑该方向。', 'SOURCE_ANNOTATION_INVALID'],
] as [$text, $code]) $check($text, false, 'Pending/conditional operators cannot swallow actual assertions or bad proof', $code);
$futurePrefix = '适用条件:医师复诊确认存在固定刺痛且无五心烦热时,方可考虑该方向';
foreach ([',', '且', '但', '并', '并且', '同时', '还', '又', '另'] as $joiner) {
foreach ($assertions as [$assertion, $code]) {
$check('适用前须核实:是否存在感染' . $joiner . $assertion . ';是否出现心悸。', false,
'A pending semicolon list retains the assertion before it continues: ' . $joiner, $code);
$check($futurePrefix . $joiner . $assertion . '。', false,
'An only-then decision retains the actual following assertion: ' . $joiner, $code);
}
}
// v43r9: UNKNOWN 的 is attributive only at a bounded nominal-item start.
$r9Unit = "适用前须核实:是否存在感染、痰多胸闷、腹胀纳差等可能不宜收敛或滋腻的情况;是否有肝肾功能后续变化及其他未记录的药物过敏。\n\n";
$r9Artifact = dirname(__DIR__, 2) . '/app/artifacts/ai_concurrency/tcm_complete_v43r9_single_openai_20261024_0_openai.rejected.json';
if (is_file($r9Artifact)) {
$saved = json_decode((string) file_get_contents($r9Artifact), true, 512, JSON_THROW_ON_ERROR);
$expect($saved['rejections'][1]['unit'] === $r9Unit, 'The r9 second retained unit is tested without rewriting any byte.');
}
$check($r9Unit, true, 'Exact retained r9 nested attributive-question unit.');
foreach (['尚未记录', '尚未评估', '未曾采集', '未填写', '未询问', '未检查', '未明确',
'不详', '未知', '不清楚', '待核实', '待补充', '待补采', '待补', '待采集', '待查'] as $state) {
foreach ([
$state . '的药物过敏及肝肾功能变化',
'肝肾功能变化及其他' . $state . '的药物过敏',
'用药变更或者' . $state . '的其他禁忌',
'其余' . $state . '的体征以及饮水变化',
'饮水变化、额外' . $state . '的体征',
] as $object) $check('适用前须核实:是否有' . $object . '。', true,
'Attributive record state remains a question across unrelated object/order variants: ' . $state);
}
foreach ([
['药物过敏未记录。', 'NEGATIVE_TO_UNKNOWN'],
['药物过敏不详。', 'NEGATIVE_TO_UNKNOWN'],
['药物过敏未知。', 'NEGATIVE_TO_UNKNOWN'],
['药物过敏不清楚。', 'NEGATIVE_TO_UNKNOWN'],
['适用前须核实:是否有药物过敏未记录。', 'NEGATIVE_TO_POSITIVE'],
['适用前须核实:是否有未知的药物过敏未记录。', 'NEGATIVE_TO_UNKNOWN'],
['是否有未知的药物过敏。', 'NEGATIVE_TO_UNKNOWN'],
['适用前须核实:是否有肝肾变化及其他未记录的药物过敏;药物过敏未记录。', 'NEGATIVE_TO_UNKNOWN'],
['适用前须核实:是否有其他未知的禁忌(s999)。', 'SOURCE_ANNOTATION_INVALID'],
['适用前须核实:是否有其他未记录的资料,患者已存在固定刺痛。', 'UNKNOWN_TO_POSITIVE'],
['适用前须核实:是否有其他未记录的资料,目前无固定刺痛。', 'UNKNOWN_TO_NEGATIVE'],
['适用前须核实:是否有其他未记录的资料,本次诊见舌质红绛。', 'TONGUE_FEATURE_NOT_IN_CURRENT_SOURCE'],
] as [$text, $code]) $check($text, false, 'An attributive question does not waive postfix erasure or current assertions', $code);
foreach ([',', '且', '但', '并', '并且', '同时', '还', '又', '另'] as $joiner) {
foreach ($assertions as [$assertion, $code]) {
$check('适用前须核实:是否有肝肾变化及其他未记录的药物过敏' . $joiner . $assertion . '。', false,
'A nested attributive modifier cannot consume an actual adjacent assertion: ' . $joiner, $code);
}
$check('适用前须核实:是否有未知的药物过敏' . $joiner . '药物过敏未记录。', false,
'A known allergy record stays known after every assertion joiner: ' . $joiner, 'NEGATIVE_TO_UNKNOWN');
}
// v43r10: inference labels, source-opening/suffix quotes and post-subject questions.
$r10Units = [
['tcm_complete_v43r10_supplement_ooc2_20261024_0_openai', 0, "本例支持点:气虚方面见乏力、动则气短、低声、脉细弱无力;津少方面见口干少量频饮、舌少津苔少;卫外不固线索为白天自汗并活动后加重(s1、s2、s3、s9、s10、s29)。\n\n"],
['tcm_complete_v43r10_supplement_ooc2_20261024_0_openai', 1, "本次询问“无已知药物过敏史”(s5),不等于排除首次反应。治疗期间监测口干、汗出、气短、体力、饮水与尿量变化,并复查空腹及餐后血糖、HbA1c、肝肾功能;异常加重及时就医。\n\n"],
['tcm_complete_v43r10_supplement_ooc2_20261024_1_openai', 1, "适用前须核实:是否存在甘草、人参类或五味子相关既往不耐受;是否另服处方药、非处方药、保健品或中药;是否存在影响选方的肝肾功能变化;汗出与气短是否出现新的严重程度或诱因。\n\n"],
];
foreach ($r10Units as [$stem, $index, $unit]) {
$artifact = dirname(__DIR__, 2) . '/app/artifacts/ai_concurrency/' . $stem . '.rejected.json';
if (is_file($artifact)) {
$saved = json_decode((string) file_get_contents($artifact), true, 512, JSON_THROW_ON_ERROR);
$expect($saved['rejections'][$index]['unit'] === $unit, 'Exact r10 retained unit: ' . $stem . ':' . $index);
}
$check($unit, true, 'Exact r10 grammatical-role unit: ' . $stem . ':' . $index);
}
foreach ([
'卫外不固线索为白天自汗并活动后加重。',
'津液不足依据为口干少量频饮。',
'气机不畅证据是乏力。',
'本次询问“无已知药物过敏史”(s5),不等于排除首次反应。',
'本次询问“无已知药物过敏史”(2026-10-23,s5),仅代表已知范围。',
'适用前须核实:是否另服中药;汗出与气短是否出现新的严重程度或诱因。',
'适用前须核实:固定刺痛是否存在;饮水与尿量是否出现新变化。',
'汗出与气短是否出现新的严重程度或诱因?',
'固定刺痛是否存在?',
] as $text) $check($text, true, 'The bounded role retains the independent clinical claims.');
foreach ([
['卫外不固线索为无自汗。', 'POSITIVE_TO_NEGATIVE'],
['无自汗,津液不足依据为口干。', 'POSITIVE_TO_NEGATIVE'],
['固定刺痛不足线索为口干。', 'UNKNOWN_TO_POSITIVE'],
['舌质红绛不润线索为口干。', 'TONGUE_FEATURE_NOT_IN_CURRENT_SOURCE'],
['卫外不固线索为患者有固定刺痛。', 'UNKNOWN_TO_POSITIVE'],
['津液不足依据为患者无固定刺痛。', 'UNKNOWN_TO_NEGATIVE'],
['津液不足线索为口干未记录。', 'POSITIVE_TO_UNKNOWN'],
['本次询问“无药物过敏史”(s5),不等于排除首次反应。', 'NEGATIVE_NOT_IN_SOURCE'],
['本次询问“无已知药物过敏史”(s9),不等于排除首次反应。', 'SOURCE_ANNOTATION_INVALID'],
['本次询问“无已知药物过敏史”(s999),不等于排除首次反应。', 'SOURCE_ANNOTATION_INVALID'],
['本次询问“无已知药物过敏史”(2026-10-22,s5),不等于排除首次反应。', 'SOURCE_ANNOTATION_INVALID'],
['本次询问“无已知药物过敏史”(s5),药物过敏未记录。', 'NEGATIVE_TO_UNKNOWN'],
['患者有固定刺痛,汗出与气短是否出现新的严重程度或诱因?', 'UNKNOWN_TO_POSITIVE'],
['适用前须核实:目前舌质红绛是否出现新变化。', 'TONGUE_FEATURE_NOT_IN_CURRENT_SOURCE'],
['适用前须核实:患者已无固定刺痛是否出现新变化。', 'UNKNOWN_TO_NEGATIVE'],
['适用前须核实:诉称固定刺痛是否出现新变化。', 'UNKNOWN_TO_POSITIVE'],
['汗出与气短是否出现感染。', 'NEGATIVE_TO_POSITIVE'],
] as [$text, $code]) $check($text, false, 'Inference/quote/question roles cannot hide actual state or bad proof', $code);
foreach ([',', '且', '但', '并', '并且', '同时', '还', '又', '另'] as $joiner) {
foreach ($assertions as [$assertion, $code]) {
$check('卫外不固线索为口干' . $joiner . $assertion . '。', false,
'An inference label does not change an actual clue/tail: ' . $joiner, $code);
$check('本次询问“无已知药物过敏史”(s5),不等于排除首次反应' . $joiner . $assertion . '。', false,
'Source-bound quote commentary retains its actual tail: ' . $joiner, $code);
$check('适用前须核实:汗出与气短是否出现新变化' . $joiner . $assertion . '。', false,
'Post-subject questions retain their actual tail: ' . $joiner, $code);
}
}
// v43r12: a prospective prepositional object owns its question predicate.
$r11Unit = "六、中药方药及复核监测\n\n主方方向可考虑生脉散方向,仅供医师复诊辨证后决定,不构成可执行处方,不列新增剂量、频次、疗程、合方或加味。\n\n支持点:气虚证据见s1、s3、s10,津亏证据见s2、s9,自汗另有2026-10-22追踪s29。三组证据与益气、生津、敛汗目标相应,并非依据糖尿病病名选方。\n\n反证与局限:相关限制依据回指s2、s9、s10,不能反向证明本方适用。肺脾定位尚不充分,口渴与血糖波动是否同步、汗量及近期体重变化未记录,故须以复诊主次为准。\n\n适用条件:复诊仍以气虚为主、津亏为兼,且舌脉与本次记录基本一致时,方可考虑。若口渴、汗出或活动耐量变化,或出现新的舌脉资料,应重新辨证。\n\n方义:人参类益气,麦冬类养阴生津,五味子类敛汗生津,共同对应第五节的主次治法;此处仅说明组方结构,不确定药材品种、炮制规格及处方量。\n\n适用前须核实:是否另用处方药、非处方药、保健品或中药?是否对拟用各药材有过敏或不耐受?是否存在影响出汗、口渴及气短判断的疾病或药物因素?\n\n";
$r11Artifact = dirname(__DIR__, 2) . '/app/artifacts/ai_concurrency/tcm_complete_v43r11_single_openai_20261024_0_openai.rejected.json';
if (is_file($r11Artifact)) {
$saved = json_decode((string) file_get_contents($r11Artifact), true, 512, JSON_THROW_ON_ERROR);
$expect($saved['rejections'][1]['unit'] === $r11Unit, 'The complete r11 second retained unit is tested byte-for-byte.');
}
$check($r11Unit, true, 'The exact retained r11 unit no longer asserts a prospective allergy.');
foreach (['对', '针对', '对于'] as $preposition) {
foreach (['拟用', '拟选', '拟选用', '拟采用', '拟使用', '待选', '待用', '待选用', '候选', '计划使用', '准备采用'] as $prospective) {
foreach (['有', '存在', '出现', '可见'] as $predicate) {
foreach (['过敏或不耐受', '心悸', '固定刺痛', '舌质红绛、脉滑', '不耐受'] as $property) {
$check('是否' . $preposition . $prospective . '方案' . $predicate . $property . '?', true,
'Only the bounded prospective operator owns this pending predicate.');
}
}
}
}
foreach ([
'适用前须核实:是否对拟用各药材有过敏或不耐受。',
'适用前须复核:是否另用中药;是否针对待选材料存在不耐受;是否对准备采用方案有心悸。',
'是否对于候选材料出现过敏?是否对拟用方案可见固定刺痛?',
] as $text) $check($text, true, 'A bounded pending context permits neutral prospective objects.');
foreach ([
['是否对拟用方案有过敏。', 'NEGATIVE_TO_POSITIVE'],
['是否针对待选材料有心悸', 'NEGATIVE_TO_POSITIVE'],
['适用前须核实:是否对已用方案有药物过敏?', 'NEGATIVE_TO_POSITIVE'],
['适用前须核实:是否对当前方案有心悸?', 'NEGATIVE_TO_POSITIVE'],
['是否对拟用目前方案有药物过敏?', 'NEGATIVE_TO_POSITIVE'],
['是否对拟用患者已有心悸的方案有过敏?', 'NEGATIVE_TO_POSITIVE'],
['是否对拟用诉称固定刺痛的方案有过敏?', 'UNKNOWN_TO_POSITIVE'],
['是否对拟用药物过敏未记录的方案有心悸?', 'NEGATIVE_TO_UNKNOWN'],
['是否对拟用方案有药物过敏未记录?', 'NEGATIVE_TO_UNKNOWN'],
['是否对拟用方案有口干未记录?', 'POSITIVE_TO_UNKNOWN'],
['是否对拟用方案有患者无固定刺痛?', 'UNKNOWN_TO_NEGATIVE'],
['是否对拟用方案有过敏或有心悸?', 'NEGATIVE_TO_POSITIVE'],
['是否对拟用方案有过敏因已出现心悸?', 'NEGATIVE_TO_POSITIVE'],
['是否对拟用方案有过敏,患者目前已有心悸?', 'NEGATIVE_TO_POSITIVE'],
['是否对拟用方案有过敏,已诊见舌质红绛?', 'TONGUE_FEATURE_NOT_IN_CURRENT_SOURCE'],
['是否对拟用方案有不伴心悸?', 'NEGATION_SCOPE_WIDENED'],
['是否对拟用方案无固定刺痛?', 'UNKNOWN_TO_NEGATIVE'],
['是否对拟用方案有无五心烦热?', 'NEGATION_SCOPE_WIDENED'],
['是否对拟用方案有过敏(s999)?', 'SOURCE_ANNOTATION_INVALID'],
['是否对拟用方案有过敏(2026-10-22,s5)?', 'SOURCE_ANNOTATION_INVALID'],
['对拟用方案有药物过敏。', 'NEGATIVE_TO_POSITIVE'],
['患者目前对拟用方案有药物过敏。', 'NEGATIVE_TO_POSITIVE'],
['适用前须核实:是否对拟用方案有过敏?药物过敏未记录。', 'NEGATIVE_TO_UNKNOWN'],
] as [$text, $code]) $check($text, false, 'Prospective objects do not waive observations, erasure, qualifier or proof errors.', $code);
foreach ([',', ',', '且', '但', '并', '并且', '同时', '还', '又', '另'] as $joiner) {
foreach ($assertions as [$assertion, $code]) {
foreach (['是否对拟用方案有过敏或不耐受', '适用前须核实:是否针对待选材料存在不耐受'] as $question) {
$check($question . $joiner . $assertion . '?', false,
'A prospective object cannot absorb a joined actual assertion: ' . $joiner, $code);
}
}
}
// v43r13: a neutral post-subject question can lead into a bounded contingency.
$r12Tail = "适用前须核实:汗出是否伴胸部不适、心悸、晕厥、持续喘促或意识改变;如有,应优先按急症路径评估。\n\n";
$r12Unit = "六、中药方药及复核监测\n\n主方方向:可由医师评估生脉散方向。原文“动则气短、自汗、口干”(s1)对应益气、生津、敛汗三项目标;细弱无力脉及舌面少津(s9、s10)进一步支持该方向,优先次序与第五节一致。\n\n方义:方中益气部分着重改善神疲、活动耐量及气短;养津部分针对口干、少量频饮和舌面少津;敛汗部分针对自汗、活动后加重。其作用覆盖当前核心目标,但不能据方义反推本例已成立阴虚或肺脾气虚。\n\n反证与局限:寒热未定,脏腑定位仅偏肺;汗出程度缺少客观量化,口干也可能受血糖影响。相关阴性限制见s2、s9、s10,但只能缩小鉴别,不能证明本方必然适用。若复核出现新的实邪、湿热或急性代谢异常证据,应重新辨证,而非机械使用。\n\n适用条件:复诊四诊仍以气虚表现为主,口干少津持续,且出汗、气短不能由低血糖、感染、心肺事件或其他急症解释;服药后能够监测血糖、汗出、口渴、活动耐量及胃肠反应。条件未满足前不得形成可直接执行处方。\n\n适用前须核实:是否存在近期体重明显变化、进食减少、低血糖样发作或血糖剧烈波动?\n\n适用前须核实:是否另用保健品、中草药或其他处方药,以及是否曾对拟用方中成分发生不良反应?\n\n" . $r12Tail;
$r12Artifact = dirname(__DIR__, 2) . '/app/artifacts/ai_concurrency/tcm_complete_v43r12_supplement_ooc2_20261024_0_openai.rejected.json';
if (is_file($r12Artifact)) {
$saved = json_decode((string) file_get_contents($r12Artifact), true, 512, JSON_THROW_ON_ERROR);
$expect($saved['rejections'][0]['unit'] === $r12Unit, 'The exact complete r12 first retained unit is unchanged.');
}
$check($r12Unit, true, 'The entire retained r12 unit passes Guard without rewriting.');
$check($r12Tail, true, 'The isolated actual post-subject question and contingency are prospective.');
foreach (['汗出', '口渴', '固定刺痛'] as $subject) {
foreach (['伴', '伴有'] as $predicate) {
foreach (['心悸', '固定刺痛', '舌质红绛、脉滑'] as $object) {
foreach ([';', ';', '?'] as $separator) {
foreach (['如有', '若有', '如果有'] as $contingency) {
foreach (['应优先评估', '请及时按应急路径评估'] as $action) {
$check('适用前须核实:' . $subject . '是否' . $predicate . $object . $separator
. $contingency . ',' . $action . '。', true,
'A neutral question and immediate conditional action assert no patient finding.');
}
}
}
}
}
}
foreach ([
['汗出是否伴心悸。', 'NEGATIVE_TO_POSITIVE'],
['汗出是否伴固定刺痛;如有,应优先评估。', 'UNKNOWN_TO_POSITIVE'],
['适用前须核实:患者现有心悸是否伴固定刺痛;如有,应评估。', 'NEGATIVE_TO_POSITIVE'],
['适用前须核实:已确定心悸是否伴固定刺痛;如有,应评估。', 'UNKNOWN_TO_POSITIVE'],
['适用前须核实:已确定有心悸是否伴固定刺痛;如有,应评估。', 'NEGATIVE_TO_POSITIVE'],
['适用前须核实:完成检查舌质红绛是否伴心悸;如有,应评估。', 'TONGUE_FEATURE_NOT_IN_CURRENT_SOURCE'],
['适用前须核实:目前舌质红绛是否伴心悸;如有,应评估。', 'TONGUE_FEATURE_NOT_IN_CURRENT_SOURCE'],
['适用前须核实:诉称固定刺痛是否伴心悸;如有,应评估。', 'UNKNOWN_TO_POSITIVE'],
['适用前须核实:汗出是否伴患者有心悸;如有,应评估。', 'NEGATIVE_TO_POSITIVE'],
['适用前须核实:汗出是否伴口干未记录;如有,应评估。', 'POSITIVE_TO_UNKNOWN'],
['适用前须核实:汗出是否伴心悸未记录;如有,应评估。', 'NEGATIVE_TO_UNKNOWN'],
['适用前须核实:汗出是否伴药物过敏未记录;如有,应评估。', 'NEGATIVE_TO_UNKNOWN'],
['适用前须核实:汗出是否伴舌象未记录;如有,应评估。', 'RECORDED_TO_UNKNOWN'],
['适用前须核实:汗出是否伴无五心烦热;如有,应评估。', 'NEGATION_SCOPE_WIDENED'],
['适用前须核实:汗出是否伴心悸(s999);如有,应评估。', 'SOURCE_ANNOTATION_INVALID'],
['适用前须核实:汗出是否伴心悸(2026-10-22,s5);如有,应评估。', 'SOURCE_ANNOTATION_INVALID'],
['适用前须核实:汗出是否伴心悸;如有,现有心悸。', 'NEGATIVE_TO_POSITIVE'],
['适用前须核实:汗出是否伴心悸;如有,应评估,患者有固定刺痛。', 'UNKNOWN_TO_POSITIVE'],
['适用前须核实:汗出是否伴心悸?若有,应评估且患者无固定刺痛。', 'UNKNOWN_TO_NEGATIVE'],
['适用前须核实:汗出是否伴心悸;如果有,应复核,心悸未记录。', 'NEGATIVE_TO_UNKNOWN'],
] as [$text, $code]) $check($text, false, 'Question/contingency roles cannot hide actual or erased facts.', $code);
foreach ([',', ',', '且', '但', '并', '并且', '同时', '还', '又', '另'] as $joiner) {
foreach ($assertions as [$assertion, $code]) {
foreach (['如有', '若有', '如果有'] as $contingency) {
$check('适用前须核实:汗出是否伴心悸;' . $contingency . ',应优先评估'
. $joiner . $assertion . '。', false,
'A conditional action cannot lend its modal role to an actual tail: ' . $joiner, $code);
$check('适用前须核实:汗出是否伴心悸;' . $contingency
. $joiner . $assertion . '。', false,
'A contingency operator cannot absorb its actual adjoining assertion: ' . $joiner, $code);
}
$check('适用前须核实:汗出是否伴心悸' . $joiner . $assertion . ';如有,应评估。', false,
'A post-subject question retains its actual adjoining assertion: ' . $joiner, $code);
}
}
// r13 integration regression: occurrence questions and their own linked inquiry.
$r30Portable = "适用前须核实:复核自汗与气短是否持续呈活动后加重,有无新发感染、胸闷胸痛、心悸、晕厥或气促加剧;补采完整饮水量、尿量、体重变化、腹诊及腰膝耳鸣等资料。若出现急性发热、明显实热、痰湿壅盛或其他实邪表现,应重新辨证,不能机械沿用本方向。\n\n";
$r30Artifact = dirname(__DIR__, 2) . '/app/artifacts/ai_concurrency/tcm_complete_v30_isolated_c4_20261023_3_openai.rejected.json';
if (is_file($r30Artifact)) {
$saved = json_decode((string) file_get_contents($r30Artifact), true, 512, JSON_THROW_ON_ERROR);
$expect($saved['rejections'][0]['unit'] === $r30Portable, 'The portable v30 regression reproduces its entire retained unit exactly.');
}
$check($r30Portable, true, 'The original complete v30 review role remains accepted.');
foreach (['汗出', '口干', '固定刺痛'] as $subject) {
foreach (['持续', '反复发作', '持续呈活动后加重', '加重', '缓解', '减轻', '复发', '发病', '发生', '持续3个月'] as $predicate) {
foreach (['有无心悸或固定刺痛', '是否伴心悸', '是否存在舌质红绛、脉滑'] as $linked) {
$check('适用前须核实:复核' . $subject . '是否' . $predicate . ',' . $linked . '。', true,
'Whether governs its occurrence predicate and each linked question has its own operator.');
}
}
}
$occurrenceReview = '适用前须核实:复核自汗与气短是否持续呈活动后加重,有无新发感染、心悸或气促加剧';
foreach ([
['适用前须核实:患者有心悸是否持续,有无固定刺痛。', 'NEGATIVE_TO_POSITIVE'],
['适用前须核实:目前舌质红绛是否加重,有无心悸。', 'TONGUE_FEATURE_NOT_IN_CURRENT_SOURCE'],
['适用前须核实:已完成诊见舌质红绛是否加重,有无心悸。', 'TONGUE_FEATURE_NOT_IN_CURRENT_SOURCE'],
['适用前须核实:诉称固定刺痛是否持续,有无心悸。', 'UNKNOWN_TO_POSITIVE'],
['适用前须核实:汗出是否口干未记录,有无心悸。', 'POSITIVE_TO_UNKNOWN'],
['适用前须核实:汗出是否心悸未记录,有无固定刺痛。', 'NEGATIVE_TO_UNKNOWN'],
['适用前须核实:汗出是否无五心烦热,有无心悸。', 'NEGATION_SCOPE_WIDENED'],
['适用前须核实:汗出是否持续,有心悸,有无固定刺痛。', 'NEGATIVE_TO_POSITIVE'],
['适用前须核实:汗出是否持续,口干未记录,有无固定刺痛。', 'POSITIVE_TO_UNKNOWN'],
['适用前须核实:汗出是否持续,是否口干未记录。', 'POSITIVE_TO_UNKNOWN'],
['适用前须核实:汗出是否持续,是否心悸未记录。', 'NEGATIVE_TO_UNKNOWN'],
['适用前须核实:汗出是否持续,有无口干未记录。', 'POSITIVE_TO_NEGATIVE'],
['适用前须核实:汗出是否持续,有无心悸未记录。', 'NEGATION_SCOPE_WIDENED'],
['适用前须核实:汗出是否持续,是否舌象未记录。', 'RECORDED_TO_UNKNOWN'],
['适用前须核实:汗出是否持续,无五心烦热,有无固定刺痛。', 'NEGATION_SCOPE_WIDENED'],
['适用前须核实:汗出是否持续,有无心悸(s999)。', 'SOURCE_ANNOTATION_INVALID'],
['适用前须核实:汗出是否持续,有无心悸(2026-10-22,s5)。', 'SOURCE_ANNOTATION_INVALID'],
] as [$text, $code]) $check($text, false, 'An occurrence query and linked inquiry cannot hide a finding, erasure or invalid proof.', $code);
foreach ([',', ',', '且', '但', '并', '并且', '同时', '还', '又', '另'] as $joiner) {
foreach ($assertions as [$assertion, $code]) {
$check($occurrenceReview . $joiner . $assertion . '。', false,
'Linked questions retain their actual or erased tail: ' . $joiner, $code);
$check('适用前须核实:复核汗出是否持续呈活动后加重' . $joiner . $assertion . ',有无心悸。', false,
'An assertion between independent questions stays checked: ' . $joiner, $code);
}
}
// r14: polarity entailment retains degree scope; prospective risk roles stay local.
$r14Units = [
['tcm_complete_v43r13_single_openai_20261024_0_openai', 0, "表里:症状逐渐出现且已持续3个月,并涉及汗出、饮水、二便等整体功能变化(s1、s2),倾向里证。表证所需的独立阳性证据不足,现有资料不足以确定表里兼夹。\n\n"],
['tcm_complete_v43r13_supplement_ooc2_20261024_1_openai', 0, "表里:病程3个月、症状逐渐出现,并涉及汗出、口渴、二便等内在功能表现(s1、s2),结合细弱无力脉(s10),偏里证;表证阳性依据不足,阴性限制见s2、s10。\n\n"],
['tcm_complete_v43r13_supplement_ooc2_20261024_1_openai', 1, "表里:症状持续3个月且逐渐出现,口干、汗出及二便改变均属内在功能失调(s1、s2),故偏里;表证所需的本次阳性证据不足,首节相关阴性仅用于限制判断。\n\n"],
['tcm_complete_v43r13_openai_deadline_20261024_openai_next_full90_openai', 0, "六、中药方药及复核监测\n\n主方方向限一个:生脉散方向,仅供医师在补齐安全条件后辨证复核,不构成可直接执行处方。本例同时存在气虚、津少与自汗证据(s1—s3、s9、s10),其益气、生津、敛汗三项方义可分别对应第五节目标,且以益气、生津为主,敛汗为辅。\n\n方义复核:人参类承担补气,针对神疲、动则气短、低声、脉弱;麦冬类侧重养津,对应口干、少量频饮、舌面少津;五味子类取收敛之意,对应白天自汗及活动后加重。这里只说明方义,不新增剂量、频次、疗程或加味。\n\n反证与局限:寒热总纲未定,阴虚尚未确立,故不能把本方向解释为治疗阴虚火旺;肺脏定位仅为倾向,亦不能据方名反向确认肺气虚。食欲一般且大便稍干(s2),消化耐受性仍需观察。\n\n适用条件:复诊时气虚、津少和自汗仍同时存在,且口干并非短期脱水、急性高血糖或其他可纠正因素所致;若症状性质改变、出现急性病情或辨证转实,应重新评估,不沿用本方向。\n\n适用前须核实:是否存在未记录的中药、保健品或其他处方药使用?\n\n"],
['tcm_complete_v43r13_openai_deadline_20261024_openai_next_full90_openai', 1, "六、中药方药及复核监测\n\n主方方向限一个:生脉散方向。本例气虚、津少、自汗证据并存(s1—s3、s9、s10),其益气、生津、敛汗方义依次对应第五节目标,不作气阴两虚或肺气虚的反向确诊。\n\n方义上,补气针对神疲、动则气短、语声低微及脉弱;养津针对口干少量频饮、舌面少津;收敛仅对应白天及活动后自汗。不得据此自行确定药味、剂量、频次或疗程。\n\n反证与局限:寒热、阴阳及脏腑定位尚未充分确定,不能将本方向解释为滋阴清热或温阳;全天饮水量、口干昼夜差异及汗出量未记录。若核心表现复诊时不再并存,应撤回该方向并重新辨证。\n\n适用前须核实:是否存在未记录的其他处方药、保健品或近期中药暴露?\n\n适用前须核实:是否存在吞咽困难、胃肠耐受问题,或拟用具体药物的既往不良反应?\n\n当前用药原文为2026-10-23“二甲双胍0.5g,每日2次,既有医嘱;未使用胰岛素或中药。”(s6);不得擅停换。既有过敏询问见s5,拟用中药仍须逐味核实。\n\n"],
];
foreach ($r14Units as [$stem, $index, $unit]) {
$path = dirname(__DIR__, 2) . '/app/artifacts/ai_concurrency/' . $stem . '.rejected.json';
if (is_file($path)) {
$saved = json_decode((string) file_get_contents($path), true, 512, JSON_THROW_ON_ERROR);
$expect($saved['rejections'][$index]['unit'] === $unit, 'Exact r14 retained whole unit: ' . $stem . ':' . $index);
}
$check($unit, true, 'The whole retained r14 unit preserves source and grammatical roles.');
}
foreach ([['明显', '腹胀', true], ['剧烈', '腹痛', true], ['大量', '皮疹', true],
['频繁', '心悸', true], ['持续', '咳嗽', true], ['淋漓', '汗出', false], ['严重', '腹泻', false]] as [$degree, $finding, $prefix]) {
$qualifiedFinding = $prefix ? $degree . $finding : $finding . $degree;
$qualifiedLedger = Guard::build(['diagnosis_date' => '2026-10-23', 'present_illness' => '无' . $qualifiedFinding . ';未记录固定刺痛。']);
$broadLedger = Guard::build(['diagnosis_date' => '2026-10-23', 'present_illness' => '无' . $finding . ';未记录固定刺痛。']);
$check('患者有' . $finding . '。', true, 'Qualified absence does not deny every degree of the broader finding.', '', $qualifiedLedger);
$check('患者有' . $qualifiedFinding . '。', false, 'The complete qualified absence remains binding.', 'NEGATIVE_TO_POSITIVE', $qualifiedLedger);
$check('患者有' . $qualifiedFinding . '。', false, 'A broad negative still forbids a specific qualified positive.', 'NEGATIVE_TO_POSITIVE', $broadLedger);
$check('无' . $finding . '。', false, 'Degree removal from a patient negative still widens scope.', 'NEGATION_SCOPE_WIDENED', $qualifiedLedger);
$check('患者有固定刺痛。', false, 'Unknown observations are independent of degree entailment.', 'UNKNOWN_TO_POSITIVE', $qualifiedLedger);
}
$compoundNegative = Guard::build(['diagnosis_date' => '2026-10-23', 'present_illness' => '无胸痛心悸、咳嗽咯痰。']);
foreach (['胸痛', '心悸', '咳嗽', '咯痰'] as $finding) $check('患者有' . $finding . '。', false,
'A compound component is not a removable degree modifier.', 'NEGATIVE_TO_POSITIVE', $compoundNegative);
foreach (['未记录', '尚未采集', '未提供', '待核实', '不详', '未知'] as $gap) {
foreach (['中药、保健品或其他处方药', '设备或外用制剂'] as $target) {
foreach (['使用', '服用', '应用', '启用'] as $action) {
foreach (['', '情况', '记录', '史'] as $noun) $check('适用前须核实:是否存在' . $gap . '的' . $target . $action . $noun . '?',
true, 'A record-gap adjective and nominal action remain a pending inquiry.');
}
}
}
foreach (['拟用', '待选', '计划使用', '准备采用'] as $planned) {
foreach (['逐项核实', '逐味复核', '进一步审核'] as $review) {
$check('既有过敏询问见s5,' . $planned . '方案仍须' . $review . '。', true,
'A postfix prospective review action does not inherit prior presence.');
foreach ($assertions as [$assertion, $code]) $check('既有过敏询问见s5,' . $planned . '方案仍须' . $review . '且' . $assertion . '。', false,
'Prospective review owns only its target, retaining the actual tail.', $code);
}
}
foreach ([
['患者有汗出淋漓。', 'NEGATIVE_TO_POSITIVE'],
['患者有心悸。', 'NEGATIVE_TO_POSITIVE'],
['舌质紫暗。', 'TONGUE_FEATURE_NOT_IN_CURRENT_SOURCE'],
['舌苔黄腻。', 'TONGUE_FEATURE_NOT_IN_CURRENT_SOURCE'],
['舌苔少而非腻。', 'NEGATION_SCOPE_WIDENED'],
['无五心烦热。', 'NEGATION_SCOPE_WIDENED'],
['适用前须核实:是否存在未记录的患者使用中药?', 'NEGATIVE_TO_POSITIVE'],
['适用前须核实:是否存在未记录的中药已使用?', 'NEGATIVE_TO_POSITIVE'],
['中药未记录。', 'NEGATIVE_TO_UNKNOWN'],
['二甲双胍0.5g未记录。', 'POSITIVE_TO_UNKNOWN'],
['适用前须核实:是否存在未记录的中药使用;患者使用中药。', 'NEGATIVE_TO_POSITIVE'],
['既有过敏询问见s5,拟用正在使用中药仍须逐味核实。', 'NEGATIVE_TO_POSITIVE'],
['既有过敏询问见s5,拟用方案已确认患者有心悸。', 'NEGATIVE_TO_POSITIVE'],
] as [$text, $code]) $check($text, false, 'Degree/query/review handling retains actual violations.', $code);
foreach ([',', ',', '且', '但', '并', '并且', '同时', '还', '又', '另'] as $joiner) {
foreach ($assertions as [$assertion, $code]) {
$check('适用前须核实:是否存在未记录的中药使用' . $joiner . $assertion . '?', false,
'The nominal action query retains its actual adjoining assertion.', $code);
$check('既有过敏询问见s5,拟用中药仍须逐味核实' . $joiner . $assertion . '。', false,
'The prospective postfix action retains its actual adjoining assertion.', $code);
}
}
$medicationReference = '当前用药原文为2026-10-23“' . $ledger['sources']['s6']['quote'] . '”(s6);不得擅停换。既有过敏询问见s5,拟用中药仍须逐味核实。';
$check($medicationReference, true, 'The exact known medication baseline remains explicit beside the pending action.');
$check(str_replace('(s6)', '(s999)', $medicationReference), false,
'A future action cannot repair an invalid baseline citation.', 'SOURCE_ANNOTATION_INVALID');
$check(str_replace('(s6)', '(2026-10-22,s6)', $medicationReference), false,
'A future action cannot repair the wrong baseline date.', 'SOURCE_ANNOTATION_INVALID');
// r14 medication aliases: complete field references use source provenance.
$medicationAliasMinimal = Guard::build(['id' => 1, 'diagnosis_date' => '2026-10-23',
'current_medications' => '甲药1g,每日2次;未使用乙药。']);
$medicationAliases = ['用药', '现用药', '在用药', '当前用药', '目前用药', '本次用药', '现用药物', '在用药物',
'用药情况', '用药记录', '用药信息', '现用药情况', '当前用药情况', '目前用药情况', '用药史', '药物使用史'];
foreach ($medicationAliases as $alias) {
foreach (['未记录', '未采集', '不详'] as $gap) {
foreach (['', '拟用中药仍须逐味核实,', '既有过敏询问见s5,拟用中药仍须逐味核实,'] as $prefix) {
$check($prefix . $alias . $gap . '。', false, 'A complete known medication field cannot be erased through an alias.', 'RECORDED_TO_UNKNOWN');
}
$check($alias . $gap . '。', false, 'A minimal medication field needs no unrelated source topic to retain its state.',
'RECORDED_TO_UNKNOWN', $medicationAliasMinimal);
}
}
foreach (['现用药剂量未记录。', '当前用药频次未采集。', '用药起始日期未记录。', '首次用药时间未记录。',
'既往用药不良反应史未采集。', '其他合并用药未记录。', '额外用药未记录。', '拟用中药剂量未记录。'] as $specific) {
$check($specific, true, 'A specific uncollected medication detail is not a whole-field alias.');
$check('拟用中药仍须逐味核实,' . $specific, true, 'A planned-review prefix retains a genuine specific data gap.');
}
foreach ([null, '', '无', '未记录'] as $missing) {
$missingMedication = Guard::build(['diagnosis_date' => '2026-10-23', 'current_medications' => $missing]);
foreach ($medicationAliases as $alias) {
$check($alias . '未记录。', true, 'Missing medication fields remain unknown.', '', $missingMedication);
$check('拟用中药仍须逐味核实,' . $alias . '未记录。', true, 'Pending review does not fill an unknown medication field.', '', $missingMedication);
foreach ([$alias . ':无。', '无' . $alias . '。', '拟用中药仍须逐味核实,' . $alias . ':无。'] as $denial) {
$check($denial, false, 'An unknown medication alias cannot become a patient denial.', 'UNKNOWN_TO_NEGATIVE', $missingMedication);
}
}
$check('当前用药:甲药。', false, 'A missing field cannot acquire an invented active drug.', 'UNKNOWN_TO_POSITIVE', $missingMedication);
$check("现用药:\n\n未记录。", true, 'An unknown medication field stays unknown across a heading boundary.', '', $missingMedication);
foreach ([',', '且', '但', '并', '同时'] as $joiner) {
$check('无现用药' . $joiner . '另有资料。', false,
'An adjacent statement cannot hide an unknown whole-field denial.', 'UNKNOWN_TO_NEGATIVE', $missingMedication);
}
}
$oldMedicationRows = ['required_content' => [['key' => 'prescriptions', 'value' => [['id' => 9,
'prescription_date' => '2020-05-02', 'prescription_name' => '旧方甲', 'usage_instruction' => '已停用']]]]];
$oldOnlyMedication = Guard::build(['id' => 1, 'diagnosis_date' => '2026-10-23'], $oldMedicationRows);
$mixedMedication = Guard::build(['id' => 1, 'diagnosis_date' => '2026-10-23', 'current_medications' => '甲药1g;未使用乙药。'], $oldMedicationRows);
$check('现用药未记录。', true, 'An old prescription does not create a known current medication field.', '', $oldOnlyMedication);
$check('当前用药:无。', false, 'An old prescription cannot turn missing current medications into a denial.', 'UNKNOWN_TO_NEGATIVE', $oldOnlyMedication);
$check('用药史未记录。', false, 'An unqualified whole medication history cannot erase a known old record.', 'RECORDED_TO_UNKNOWN', $oldOnlyMedication);
$check('2019年既往用药史未记录。', true, 'A specific uncollected historical period is distinct from a known later record.', '', $oldOnlyMedication);
$check('既往用药史未采集。', true, 'A current medication field alone does not fill specific historical collection.', '', $medicationAliasMinimal);
$check('现用药未记录。', false, 'Historical material cannot erase the known current medication field.', 'RECORDED_TO_UNKNOWN', $mixedMedication);
$check('现用药未记录(2020-05-02,s2)。', false, 'An old citation cannot change an explicitly current field reference.', 'RECORDED_TO_UNKNOWN', $mixedMedication);
$check('现用药:无。', false, 'A recorded active medication field cannot become a blanket denial.', 'POSITIVE_TO_NEGATIVE', $medicationAliasMinimal);
$partialNegativeMedication = Guard::build(['diagnosis_date' => '2026-10-23', 'current_medications' => '未使用乙药。']);
$wholeNegativeMedication = Guard::build(['diagnosis_date' => '2026-10-23', 'current_medications' => '未使用任何药物。']);
$check('现用药:无。', false, 'Non-use of one item cannot establish an empty whole medication field.', 'NEGATION_SCOPE_WIDENED', $partialNegativeMedication);
$check('现用药:无。', true, 'An explicit whole-current-field denial retains its original negative meaning.', '', $wholeNegativeMedication);
$check('用药史:无。', false, 'Current non-use does not establish a whole lifetime medication-history denial.', 'NEGATION_SCOPE_WIDENED', $wholeNegativeMedication);
foreach ([',', ',', '且', '但', '并', '并且', '同时', '还', '又', '另'] as $joiner) {
foreach ($assertions as [$assertion, $code]) {
$check('现用药剂量未记录' . $joiner . $assertion . '。', false,
'A specific data gap does not exempt adjacent actual assertions.', $code);
$check('拟用中药仍须逐味核实' . $joiner . '现用药未记录。', false,
'An action joiner cannot exempt whole-field medication erasure.', 'RECORDED_TO_UNKNOWN');
}
}
if (!$guardOnly) {
foreach ([$ledger, $medicationAliasMinimal, $oldOnlyMedication] as $caseIndex => $caseLedger) {
foreach ([1, 2, 7, 31] as $size) {
$header = "现用药:\n\n";
$tail = ($caseIndex < 2 ? '未记录' : '无') . "。\n\n";
$visible = '';
$stream = new TcmClinicalReportStream($caseLedger, static function (string $delta) use (&$visible): bool { $visible .= $delta; return true; });
$pushed = true;
foreach (str_split($header . $tail, $size) as $chunk) if (!$stream->push($chunk)) { $pushed = false; break; }
$expect(!$pushed && $stream->rejected(), 'A carried medication field rejects the invalid value, chunk ' . $size);
$expect($visible === $header && $stream->acceptedContent() === $header,
'The accepted heading is exact and zero rejected value bytes become visible, chunk ' . $size);
}
}
}
// Medication note provenance: a same-time explicit field value is still a source.
$noteMedicationData = ['id' => 900000044, 'diagnosis_date' => strtotime('2026-10-23'),
'current_medications' => null, 'remark' => '当前用药:使用甲药。'];
$noteMedication = Guard::build($noteMedicationData);
$expect($noteMedication['sources']['s1']['quote'] === $noteMedicationData['remark']
&& $noteMedication['sources']['s1']['path'] === 'current.remark'
&& $noteMedication['sources']['s1']['scope'] === 'current'
&& $noteMedication['sources']['s1']['date'] === '2026-10-23', 'The exact root reproduction retains its original source ID/quote/scope/date.');
$check('当前用药:使用甲药。', true, 'Exact root reproduction: an explicit same-time remark remains valid.', '', $noteMedication);
foreach (['remark', 'doctor_advice', 'present_illness'] as $noteField) {
foreach (['使用甲药', '服用甲药', '甲药1g'] as $value) {
$case = Guard::build(['id' => 1, 'diagnosis_date' => '2026-10-23', 'current_medications' => null,
$noteField => '当前用药:' . $value . ',每日2次。']);
$check('当前用药:' . $value . '。', true, 'An explicit actual field value can occur in another submitted source.', '', $case);
foreach (['现用药未记录。', '当前用药未记录。', '用药史未记录。'] as $erasure) $check($erasure, false,
'A known labelled note cannot be erased by another whole-field alias.', 'RECORDED_TO_UNKNOWN', $case);
}
}
$negativeMedicationNote = Guard::build(array_replace($noteMedicationData, ['remark' => '当前用药:未使用乙药。']));
$emptyMedicationNote = Guard::build(array_replace($noteMedicationData, ['remark' => '当前用药:无。']));
$check('当前用药:未使用乙药。', true, 'A bounded negative note value keeps its exact original meaning.', '', $negativeMedicationNote);
$check('当前用药:使用乙药。', false, 'Explicit non-use in a labelled note still forbids use of that item.', 'NEGATIVE_TO_POSITIVE', $negativeMedicationNote);
$check('当前用药:无。', false, 'A partial non-use note cannot establish a blanket denial.', 'NEGATION_SCOPE_WIDENED', $negativeMedicationNote);
$check('现用药:无。', true, 'A labelled whole-field negative is an actual negative source.', '', $emptyMedicationNote);
$check('当前用药未记录。', false, 'A labelled negative cannot become missing information.', 'RECORDED_TO_UNKNOWN', $emptyMedicationNote);
foreach (['当前用药:未记录。', '当前用药:不详。', '当前用药:暂无。',
'当前用药:未记录,患者有心悸。', '当前用药:未记录;患者有心悸。',
'当前用药:未记录。使用甲药。', "当前用药:\n患者有心悸。", '当前用药:需核实甲药。',
'当前用药:拟用甲药。', '当前用药:医生建议使用甲药。', '当前用药:可考虑使用甲药。',
'医师建议复核当前用药。使用甲药。', '当前用药相关问题待核实。神疲口干。'] as $unknownNote) {
$case = Guard::build(array_replace($noteMedicationData, ['remark' => $unknownNote]));
$check('当前用药:使用甲药。', false, 'A missing/plan/mention/unrelated note does not establish an active medication field.', 'UNKNOWN_TO_POSITIVE', $case);
$check('现用药未记录。', true, 'A label without an actual value remains unknown.', '', $case);
$check('当前用药未记录。', true, 'A metadata heading is not a phantom positive medication fact.', '', $case);
$check('当前用药:无。', false, 'Unknown note labels cannot become observed medication absence.', 'UNKNOWN_TO_NEGATIVE', $case);
}
foreach (['当前用药:既往使用甲药。', '当前用药:2026-10-22使用甲药。'] as $datedNote) {
$case = Guard::build(array_replace($noteMedicationData, ['remark' => $datedNote]));
$check('当前用药:使用甲药。', false, 'Historical or mismatched-time note content cannot prove current medication state.', 'UNKNOWN_TO_POSITIVE', $case);
}
$historyMedicationNote = Guard::build(['id' => 1, 'diagnosis_date' => '2026-10-23'], ['required_content' => [['key' => 'doctor_image_notes',
'value' => [['diagnosis_id' => 2, 'note_date' => '2020-05-02', 'content' => '当前用药:使用甲药。']]]]]);
$check('当前用药:使用甲药。', false, 'A historical source with a current-looking label remains historical.', 'UNKNOWN_TO_POSITIVE', $historyMedicationNote);
$check('现用药未记录。', true, 'An old labelled note does not fill the current field.', '', $historyMedicationNote);
$check('用药史未记录。', false, 'A generic history label still retains a known historical note.', 'RECORDED_TO_UNKNOWN', $historyMedicationNote);
$sameDayMedicationNote = Guard::build(['id' => 1, 'diagnosis_date' => '2026-10-23'], ['required_content' => [['key' => 'doctor_image_notes',
'value' => [['note_date' => '2026-10-23', 'content' => '当前用药:使用甲药。']]]]]);
$check('当前用药:使用甲药。', true, 'An explicitly labelled same-day source remains eligible without changing its unspecified scope.', '', $sameDayMedicationNote);
$expect($sameDayMedicationNote['sources']['s1']['scope'] === 'unspecified', 'Recognizing a note does not rewrite its stored scope.');
$wrongDayMedicationNote = Guard::build(['id' => 1, 'diagnosis_date' => '2026-10-23'], ['required_content' => [['key' => 'doctor_image_notes',
'value' => [['diagnosis_id' => 1, 'note_date' => '2026-10-22', 'content' => '当前用药:使用甲药。']]]]]);
$check('当前用药:使用甲药。', false, 'A mismatched note date cannot prove current medication state even with a matching visit ID.', 'UNKNOWN_TO_POSITIVE', $wrongDayMedicationNote);
$historyLabelOnly = Guard::build(array_replace($noteMedicationData, ['remark' => '用药史:使用甲药。']));
$check('当前用药:使用甲药。', false, 'A history-only label is not a current-field label.', 'UNKNOWN_TO_POSITIVE', $historyLabelOnly);
$check('用药史未记录。', false, 'An actual history-labelled source remains recorded.', 'RECORDED_TO_UNKNOWN', $historyLabelOnly);
$qualifiedMedicationNote = Guard::build(array_replace($noteMedicationData, ['remark' => '当前用药:未使用其他药物。']));
$check('当前用药:未使用其他药物。', true, 'The exact negative qualifier in a note stays usable.', '', $qualifiedMedicationNote);
$check('当前用药:未使用药物。', false, 'A note-source qualifier cannot be widened into blanket non-use.', 'NEGATION_SCOPE_WIDENED', $qualifiedMedicationNote);
$expect($qualifiedMedicationNote['sources']['s1']['quote'] === '当前用药:未使用其他药物。', 'Field recognition retains the full original qualified quote.');
$noteClinicalLedger = Guard::build(array_replace(CompleteTcmCase::current(), ['current_medications' => null,
'remark' => '当前用药:使用甲药。']), $submission);
foreach ([',', '且', '但', '并', '同时'] as $joiner) {
foreach ($assertions as [$assertion, $code]) {
$check('当前用药:使用甲药' . $joiner . $assertion . '。', false,
'Actual statements adjacent to a known note value remain independently checked.', $code, $noteClinicalLedger);
}
$check('拟用中药仍须逐味核实' . $joiner . '现用药未记录。', false,
'A planned-role prefix cannot erase an explicitly labelled known source.', 'RECORDED_TO_UNKNOWN', $noteMedication);
}
// r15 record predicates and future causal criteria: retained model bytes are immutable.
$r15RecordUnit = "寒热:口干、少津、苔少仅提示津液受损,不能单独成立热证;寒热相关限制见s2。舌质淡红且未记红绛,现有资料不足以确定寒或热。\n\n";
$r15CausalParagraph = "适用条件:复核后仍以气虚为主、津亏为兼,且汗出并非低血糖、感染、心肺疾病或药物因素所致;若口渴、多尿或乏力随血糖明显波动,应先评估代谢原因。\n\n";
$r15DeadlineUnit = "六、中药方药及复核监测\n\n主方方向限一个:生脉散方向,须由医师面诊审核后方可决定,不给出新增剂量、频次或疗程。本例取其益气、生津、敛汗的整体方义,与第五节治法次序一致。\n\n支持点:气短乏力、自汗、语声低微、脉细弱无力构成益气依据;口干少量频饮、舌面少津苔少构成生津依据;活动汗增支持兼顾敛汗(s1、s2、s3、s9、s10)。\n\n局限与反证:阴虚、虚热及明确肺脏证名均未充分成立;寒热限制信息见s2,舌脉限制信息见s9、s10。因此该方向只覆盖“气虚津亏、汗出”层面,不据此扩写阴虚方证。\n\n" . $r15CausalParagraph;
foreach ([
['tcm_complete_v43r14_single_openai_20261024_0_openai', 0, $r15RecordUnit],
['tcm_complete_v43r14_openai_deadline_20261024_openai_next_full90_openai', 1, $r15DeadlineUnit],
] as [$name, $index, $unit]) {
$artifact = dirname(__DIR__, 2) . '/app/artifacts/ai_concurrency/' . $name . '.rejected.json';
if (is_file($artifact)) {
$saved = json_decode((string) file_get_contents($artifact), true, 512, JSON_THROW_ON_ERROR);
$expect($saved['rejections'][$index]['unit'] === $unit, 'r15 portable retained unit equals the original artifact byte for byte.');
}
$expect(Guard::validate($ledger, $unit)['valid'], 'r15 exact whole retained unit has no old-Guard contradiction; this does not complete the report contract.');
}
$check($r15RecordUnit, true, 'The exact retained record-predicate paragraph preserves its complete wire bytes.');
$check($r15CausalParagraph, true, 'The exact retained future-causal paragraph preserves its complete wire bytes.');
foreach (['舌质淡红且未记红绛。', '舌质淡红,未记裂纹。', '脉细弱,未记结代。',
'未记固定刺痛。', '固定刺痛未记。', '未记载固定刺痛。'] as $text) {
$check($text, true, 'An abbreviated record predicate expresses missing information, not observed absence.');
}
foreach ([
['口干未记。', 'POSITIVE_TO_UNKNOWN'], ['未记淡红。', 'POSITIVE_TO_UNKNOWN'],
['心悸未记。', 'NEGATIVE_TO_UNKNOWN'], ['未记紫暗。', 'NEGATIVE_TO_UNKNOWN'],
['未记舌象。', 'RECORDED_TO_UNKNOWN'], ['脉象未记。', 'RECORDED_TO_UNKNOWN'],
['现用药未记。', 'RECORDED_TO_UNKNOWN'], ['药物过敏史未记。', 'RECORDED_TO_UNKNOWN'],
['舌质淡红且未见红绛。', 'TONGUE_FEATURE_NOT_IN_CURRENT_SOURCE'],
['舌质红绛且未记裂纹。', 'TONGUE_FEATURE_NOT_IN_CURRENT_SOURCE'],
['舌质淡红且未记红绛(s999)。', 'SOURCE_ANNOTATION_INVALID'],
] as [$text, $code]) $check($text, false, 'An abbreviated record predicate cannot erase known values or create an observed denial.', $code);
$recordPredicate = new ReflectionMethod(Guard::class, 'reportRecordPredicateProse');
foreach (['未记录固定刺痛', '未记得固定刺痛', '未记忆固定刺痛', '未记住固定刺痛', '未记清固定刺痛'] as $text) {
$expect($recordPredicate->invoke(null, $text) === $text, 'A complete record predicate or distinct recollection verb is not rewritten.');
}
$r15Generic = Guard::build(['diagnosis_date' => '2026-10-23',
'present_illness' => '无明显甲因素诱因。有乙症。未记录丙项。']);
$r15CausalFrame = '适用条件:复核后仍以丁判断为主,且乙症并非甲因素或其他因素所致';
$check($r15CausalFrame . '。', true, 'A future causal criterion has the same role with unrelated subject/cause vocabulary.', '', $r15Generic);
foreach (['复查', '复诊确认', '复诊复核', '重新核实'] as $review) {
$check(str_replace('复核后', $review . '后', $r15CausalFrame) . '。', true,
'An explicit future reassessment operator scopes only its closed causal criterion.', '', $r15Generic);
}
foreach ([
'乙症并非甲因素或其他因素所致。',
'适用条件:乙症并非甲因素或其他因素所致。',
'适用条件:目前乙症并非甲因素或其他因素所致。',
'适用条件:复核后仍以丁判断为主,且目前乙症并非甲因素或其他因素所致。',
'适用条件:复核后仍以丁判断为主,且患者乙症并非甲因素或其他因素所致。',
'适用条件:复核后仍以丁判断为主,且已经确认乙症并非甲因素或其他因素所致。',
] as $text) $check($text, false, 'A label, present subject or completed confirmation cannot borrow a future operator.', 'NEGATION_SCOPE_WIDENED', $r15Generic);
$futureCriterion = new ReflectionMethod(Guard::class, 'futureCausalCriterionProse');
foreach ([
'适用条件:复核后仍以患者有固定刺痛为主,且汗出并非感染所致',
'适用条件:复核后仍以舌质红绛为主,且汗出并非感染所致',
'适用条件:复核后仍以气虚为主,且汗出并非患者有心悸所致',
'适用条件:复核后仍以气虚为主,且汗出并非口干未记录所致',
'适用条件:复核后仍以气虚为主,且舌质红绛并非感染所致',
'适用条件:复核后仍以气虚为主,且汗出并非感染所致的现症',
'适用条件:复核后仍以气虚为主,目前患者有心悸,且汗出并非感染所致',
'适用条件:复核后仍以气虚为主,口干未记录,且汗出并非感染所致',
] as $text) $expect($futureCriterion->invoke(null, $text) === $text,
'An actual/unknown/examination assertion or incomplete causal complement is never removed as a future criterion.');
$r15Future = '适用条件:复核后仍以气虚为主、津亏为兼,且汗出并非低血糖、感染、心肺疾病或药物因素所致';
foreach ([',', ',', '且', '但', '并', '并且', '同时', '还', '又', '另'] as $joiner) {
foreach ($assertions as [$assertion, $code]) {
$check('舌质淡红且未记红绛' . $joiner . $assertion . '。', false,
'A record predicate cannot swallow any adjoining actual assertion.', $code);
$check($r15Future . $joiner . $assertion . '。', false,
'A closed future causal relation cannot swallow any adjoining actual assertion.', $code);
}
$check($r15Future . $joiner . '固定刺痛不存在。', false,
'An adjacent postfix absence remains an actual assertion after a removed future relation.', 'UNKNOWN_TO_POSITIVE');
$check($r15Future . $joiner . '现用药未记录。', false,
'Future causal criteria cannot erase the known medication field.', 'RECORDED_TO_UNKNOWN');
}
$check($r15Future . '(s999)。', false, 'Future role parsing cannot repair an invalid source ID.', 'SOURCE_ANNOTATION_INVALID');
$check($r15Future . ',“本次询问无已知药物过敏史”(2026-10-22,s5)。', false,
'Future role parsing cannot repair a mismatched source date.', 'SOURCE_ANNOTATION_INVALID');
$expect(hash('sha256', json_encode($ledger, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES))
=== '2ebbec0b558347bf4ee128dfb5a9e63fc4e355d58f7226486a2ae96098bd57f6',
'r15 leaves every actual fixture source ID, quote, field, date, role and ledger byte unchanged.');
// r16 assertion/reference boundaries: no diagnostic-term exceptions or changed source ledger.
$r16SingleUnit = "主证可暂拟“气虚津少证,偏肺气不足、卫外不固”。“气虚、津少”分别由s1、s2、s3、s9、s10支持;“肺气不足、卫外不固”仅为功能定位倾向,主要依据气短、低声及自汗,尚缺更完整肺系资料。\n\n";
$r16C2Unit = "表里:自汗、活动后加重可涉及卫表不固,但病程三个月、神疲及脉按之无力更支持内在功能不足。缺少外感起病及表证阳性依据,故以内证倾向为主,表证现有资料不足以确定(s1、s2、s10)。\n\n";
$r16ReferenceParagraph = "现用药记录见s6;仅复核依从性、胃肠反应及血糖变化,不擅自停换二甲双胍。既有过敏询问见s5,但拟用中药及辅料的个体暴露仍须逐项核实。\n\n";
$r16SingleFinal = "六、中药方药及复核监测\n\n本次暂不列具体主方、药味或新剂量。虽有益气固表、生津的治疗方向,但脾肺定位尚未完全落实,寒热未定,且津少尚不足成立阴虚;直接套用益气固表方或益气养阴方,均可能超出本例证据。\n\n适用前须核实:是否存在反复易感、畏风及汗出后的冷热感,以复核卫表不固的范围?\n\n适用前须核实:是否有腹胀便溏、食后困倦等脾气不足特征,或咳嗽、声音持续低弱等肺系表现?\n\n适用前须核实:全天饮水量、口干昼夜差异及汗量是否可量化,近期体重变化与血糖波动是否同步?\n\n" . $r16ReferenceParagraph;
foreach ([
['tcm_complete_v43r15_single_openai_20261024_0_openai', 0, $r16SingleUnit],
['tcm_complete_v43r15_single_openai_20261024_0_openai', 1, $r16SingleFinal],
['tcm_complete_v43r15_supplement_ooc2_20261024_1_openai', 0, $r16C2Unit],
] as [$name, $index, $unit]) {
$artifact = dirname(__DIR__, 2) . '/app/artifacts/ai_concurrency/' . $name . '.rejected.json';
if (is_file($artifact)) {
$saved = json_decode((string) file_get_contents($artifact), true, 512, JSON_THROW_ON_ERROR);
$expect($saved['rejections'][$index]['unit'] === $unit, 'r16 portable unit is exactly the retained original model output.');
}
$expect(Guard::validate($ledger, $unit)['valid'], 'r16 whole retained unit clears only old-Guard lexical rejection, not final/clinical acceptance.');
}
foreach ([$r16SingleUnit, $r16C2Unit, $r16ReferenceParagraph] as $unit) {
$check($unit, true, 'r16 exact affected paragraph preserves all original bytes at every chunk size.');
}
$r16Generic = Guard::build(['diagnosis_date' => '2026-10-23',
'present_illness' => '有甲项及乙项。无丙项及丁项。戊项未记录。']);
foreach (['但', '但是', '然而', '不过'] as $adversative) {
$check('甲类不定,' . $adversative . '甲项及乙项。', true,
'Adversative boundary restarts assertion polarity with unrelated vocabulary.', '', $r16Generic);
$check('无丙项及丁项,' . $adversative . '甲项及乙项。', true,
'A genuine inherited negative list ends at the adversative operator.', '', $r16Generic);
foreach ([['无甲项及乙项', 'POSITIVE_TO_NEGATIVE'], ['有丙项', 'NEGATIVE_TO_POSITIVE'],
['有戊项', 'UNKNOWN_TO_POSITIVE'], ['无戊项', 'UNKNOWN_TO_NEGATIVE'],
['甲项未记录', 'POSITIVE_TO_UNKNOWN'], ['丙项未记录', 'NEGATIVE_TO_UNKNOWN']] as [$assertion, $code]) {
$check('甲类不定,' . $adversative . $assertion . '。', false,
'Every independent actual assertion retains its own polarity after an adversative.', $code, $r16Generic);
}
foreach ($assertions as [$assertion, $code]) {
$check('卫外不固,' . $adversative . $assertion . '。', false,
'An inference label cannot shield an adjacent actual fixture assertion.', $code);
$check('适用前须核实:是否有固定刺痛,' . $adversative . '须复核,' . $assertion . '。', false,
'An adversative boundary does not end the strict scope of an earlier question.', $code);
}
}
foreach (['及', '以及', '、', '或', ','] as $listJoiner) {
$check('无丙项' . $listJoiner . '丁项。', true, 'Genuine inherited negative lists retain their scope.', '', $r16Generic);
$check('无甲项' . $listJoiner . '乙项。', false, 'A denied positive list remains a contradiction.', 'POSITIVE_TO_NEGATIVE', $r16Generic);
}
foreach (['主要依据', '具体依据', '直接依据', '主要根据', '具体根据', '直接根据'] as $intro) {
$check('甲类不定,' . $intro . '甲项及乙项。', true,
'An affirmative evidence introducer starts an independent finding list.', '', $r16Generic);
foreach ($assertions as [$assertion, $code]) {
$check('卫外不固,' . $intro . $assertion . '。', false,
'The evidence introducer preserves negative/unknown/value and qualifier checks.', $code);
}
$check('卫外不固,' . $intro . '无口干及乏力。', false,
'An explicit denial after an evidence introducer keeps its true negative meaning.', 'POSITIVE_TO_NEGATIVE');
}
foreach (['用药', '现用药', '在用药', '当前用药', '目前用药', '本次用药', '现用药物', '在用药物',
'用药情况', '用药记录', '用药信息', '现用药情况', '当前用药情况', '目前用药情况'] as $alias) {
$check($alias . '记录见s6。', true, 'A complete medication field alias can locate its actual schema source.');
$check($alias . '记录见s5。', false, 'A medication alias cannot bind an allergy source.', 'SOURCE_ANNOTATION_INVALID');
$check($alias . '记录见s23。', false, 'An incidental medication mention cannot supply medication-field provenance.', 'SOURCE_ANNOTATION_INVALID');
}
foreach (['现用药记录见s999。', '现用药记录见s16。', '现用药记录见s30。', '2026-10-22现用药记录见s6。',
'现用药记录见s6(2026-10-22,s6)。'] as $text) {
$check($text, false, 'A medication field locator requires an existing same-time source with matching provenance.', 'SOURCE_ANNOTATION_INVALID');
}
foreach (['拟用中药及辅料的个体暴露仍须逐项核实', '待选材料及辅料的相关暴露仍需逐项复核',
'计划使用甲材料及乙材料的记录仍应审核'] as $plan) {
$check('既有过敏询问见s5,但' . $plan . '。', true,
'A neutral planned nominal target with an extended data head retains its postfix review role.');
foreach ([',', ',', '且', '但', '并', '并且', '同时', '还', '又', '另'] as $joiner) {
foreach ($assertions as [$assertion, $code]) {
$check('既有过敏询问见s5,但' . $plan . $joiner . $assertion . '。', false,
'A planned target cannot swallow any actual assertion after a joiner.', $code);
}
$check('现用药记录见s6' . $joiner . '现用药未记录。', false,
'A neutral locator cannot erase the medication baseline in its adjoining prose.', 'RECORDED_TO_UNKNOWN');
}
}
foreach ([
['但拟用目前已使用中药的个体暴露仍须逐项核实。', 'NEGATIVE_TO_POSITIVE'],
['但拟用患者有固定刺痛的资料仍须逐项核实。', 'UNKNOWN_TO_POSITIVE'],
['但拟用患者无固定刺痛的资料仍须逐项核实。', 'UNKNOWN_TO_NEGATIVE'],
['但拟用已诊见舌质红绛的资料仍须逐项核实。', 'TONGUE_FEATURE_NOT_IN_CURRENT_SOURCE'],
['但拟用中药的个体暴露仍须逐项核实,现用药未记录。', 'RECORDED_TO_UNKNOWN'],
] as [$text, $code]) $check($text, false, 'Current/completed/embedded predicates cannot become a neutral planned target.', $code);
$check('**表里:偏里证。**症状已持续3个月并逐渐出现,以“神疲乏力”“轻微活动后气短”“白天自汗”为主;本次又记录“无恶寒发热”,脉“不浮”,现有证据更支持内在功能失调,而非急性表证。但这只反映本次接诊状态,后续若新出现恶寒、发热、咽痛等,仍须重新判断。', true,
'The original portable v15 full paragraph retains its exact source facts and future qualification.');
$check('舌脉未呈黄腻、滑、紧等所见,但不能仅凭这些否定完全排除夹实。', true,
'A complete negative exam list remains complete before an adversative commentary boundary.');
$check('舌脉未呈黄腻、滑、紧等所见,但不能仅凭这些否定完全排除夹实,患者有固定刺痛。', false,
'Commentary after a complete negative exam list cannot exempt an adjoining patient assertion.', 'UNKNOWN_TO_POSITIVE');
$expect(hash('sha256', json_encode($ledger, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES))
=== '2ebbec0b558347bf4ee128dfb5a9e63fc4e355d58f7226486a2ae96098bd57f6',
'r16 keeps every immutable ledger source ID, quote, field, date, role and schema byte unchanged.');
// r17 exact-atom triage: questions already pass; record locator and future trigger roles were missing.
$r17RecordUnit = "血:面色少华可提示血分失养,但非特异;固定刺痛的资料状态见s2,舌质亦无足够血虚或血瘀阳性特征。血虚、血瘀均待定。\n\n";
$r17Questions = [
"适用前须核实:是否另有未记录的处方药、非处方药、保健品及肝肾疾病变化?是否存在吞咽困难、明显脱水或血糖急剧波动等需优先处理情况?\n\n",
"适用前须核实:是否还在使用其他处方药、保健品或中草药?是否存在肝肾功能近期变化?是否出现低血糖样不适或胃肠反应?是否有拟用药既往不耐受?\n\n",
];
$r17Triage = [
"监测乏力、气短、自汗、口干、饮水及二便变化,并继续记录空腹与餐后血糖;若启用中药,复核血糖趋势、胃肠耐受及肝肾指标。出现意识异常、持续明显气短或其他急性变化时,应及时按西医急症路径评估。\n\n",
"如医师采用该方向,应监测乏力、气短、汗出、口干、食欲、大便及血糖变化;症状加重或出现新的胸闷、心悸、意识改变等情况,应先行医学评估,不以中药替代必要检查。\n\n",
];
foreach (['tcm_complete_v43r16_supplement_ooc2_20261024_0_openai',
'tcm_complete_v43r16_openai_deadline_20261024_openai_next_full90_openai'] as $i => $name) {
$artifact = dirname(__DIR__, 2) . '/app/artifacts/ai_concurrency/' . $name . '.rejected.json';
if (is_file($artifact)) {
$saved = json_decode((string) file_get_contents($artifact), true, 512, JSON_THROW_ON_ERROR);
if ($i === 0) $expect($saved['rejections'][0]['unit'] === $r17RecordUnit,
'The r17 portable record-state paragraph is byte-identical to its retained unit.');
$unit = $saved['rejections'][1]['unit'];
$original = $unit;
$expect(str_contains($unit, $r17Questions[$i]) && str_contains($unit, $r17Triage[$i]),
'The exact supplied question list and actual offending triage paragraph remain in the original whole unit.');
$expect(Guard::validate($ledger, $unit)['valid'] && $unit === $original,
'The exact whole section clears only old-Guard lexical errors, with no source/model/output substitution.');
}
}
foreach (array_merge([$r17RecordUnit], $r17Questions, $r17Triage) as $text) {
$check($text, true, 'r17 retained paragraph passes lexical validation and, when enabled, exact wire checks.');
}
foreach (['资料状态', '记录状态', '信息状态', '资料的状态', '记录的状态'] as $role) {
$check('固定刺痛的' . $role . '见s2。', true, 'A neutral cited record-state locator does not assert its subject is present.');
$check('舌象的' . $role . '见s9。', true, 'A neutral named-exam locator points to its actual field without erasing its values.');
foreach ([',', ',', '且', '但', '并', '并且', '同时', '还', '又', '另'] as $joiner) {
foreach ($assertions as [$assertion, $code]) {
$check('固定刺痛的' . $role . '见s2' . $joiner . $assertion . '。', false,
'A record-state locator cannot consume an adjacent actual assertion.', $code);
}
$check('固定刺痛的' . $role . '见s2' . $joiner . '现用药未记录。', false,
'A locator cannot erase the current medication baseline.', 'RECORDED_TO_UNKNOWN');
}
}
foreach ([
['固定刺痛的资料状态见s999。', 'SOURCE_ANNOTATION_INVALID'],
['固定刺痛的资料状态见s5。', 'SOURCE_ANNOTATION_INVALID'],
['2026-10-22固定刺痛的资料状态见s2。', 'SOURCE_ANNOTATION_INVALID'],
['固定刺痛的资料状态见s2(2026-10-22,s2)。', 'SOURCE_ANNOTATION_INVALID'],
['舌象的资料状态见s10。', 'SOURCE_ANNOTATION_INVALID'],
['患者有固定刺痛的资料状态见s2。', 'UNKNOWN_TO_POSITIVE'],
['患者无固定刺痛的资料状态见s2。', 'UNKNOWN_TO_NEGATIVE'],
['舌质红绛的资料状态见s9。', 'TONGUE_FEATURE_NOT_IN_CURRENT_SOURCE'],
] as [$text, $code]) $check($text, false, 'Locator syntax cannot manufacture source provenance, dates or actual clinical values.', $code);
$r17Generic = Guard::build(['diagnosis_date' => '2026-10-23',
'present_illness' => '有甲项。无乙项。丙项未记录。']);
foreach (['甲项', '乙项', '丙项'] as $subject) {
$check($subject . '的记录状态见s1。', true,
'The same locator grammar preserves positive, negative and unknown record states with unrelated vocabulary.', '', $r17Generic);
}
foreach (['出现', '发生'] as $event) {
foreach (['时', '等情况'] as $ending) {
foreach (['应', '须', '需'] as $modal) {
foreach (['评估', '就医', '检查', '处理'] as $action) {
$check($event . '乙项' . $ending . ',' . $modal . '立即' . $action . '。', true,
'Only a closed event trigger linked to a mandatory future action is prospective.', '', $r17Generic);
}
}
}
}
$check('事件恶化或发生新的乙项等情况,应先行评估。', true,
'A neutral event progression can introduce an alternative future occurrence.', '', $r17Generic);
$check('出现固定刺痛时,应立即评估。', true, 'A future trigger does not turn unknown data into a current positive.');
$check('出现舌质红绛时,应立即评估。', true, 'An explicitly hypothetical trigger is distinct from a current examination value.');
foreach ([
['出现意识异常时。', 'NEGATIVE_TO_POSITIVE'],
['出现意识异常,应立即评估。', 'NEGATIVE_TO_POSITIVE'],
['出现意识异常时,已经评估。', 'NEGATIVE_TO_POSITIVE'],
['患者已出现意识异常时,应立即评估。', 'NEGATIVE_TO_POSITIVE'],
['目前出现意识异常时,应立即评估。', 'NEGATIVE_TO_POSITIVE'],
['出现已经确认意识异常时,应立即评估。', 'NEGATIVE_TO_POSITIVE'],
['患者症状已加重或出现意识异常等情况,应立即评估。', 'NEGATIVE_TO_POSITIVE'],
['出现意识异常且患者有固定刺痛时,应立即评估。', 'UNKNOWN_TO_POSITIVE'],
['患者已出现舌质红绛时,应立即评估。', 'TONGUE_FEATURE_NOT_IN_CURRENT_SOURCE'],
] as [$text, $code]) $check($text, false,
'A current/completed/embedded assertion or incomplete future frame cannot borrow the event-trigger role.', $code);
foreach ([',', ',', '且', '但', '并', '并且', '同时', '还', '又', '另'] as $joiner) {
foreach ($assertions as [$assertion, $code]) {
$check('出现意识异常时,应立即评估' . $joiner . $assertion . '。', false,
'The future action cannot hide a factual assertion adjoining its trigger.', $code);
}
$check('出现意识异常时,应立即评估' . $joiner . '现用药未记录。', false,
'A prospective trigger/action pair cannot erase the known medication field.', 'RECORDED_TO_UNKNOWN');
}
$triggerRole = new ReflectionMethod(Guard::class, 'prospectiveTriggerProse');
foreach (['出现口干未记录时', '出现患者有固定刺痛时', '出现2026-10-22意识异常时',
'出现诊见舌质红绛时', '出现曾经意识异常时'] as $text) {
$expect($triggerRole->invoke(null, $text, ',应立即评估。') === $text,
'A record-state, attributed, dated or completed predicate is never removed as a neutral trigger.');
}
$check('出现意识异常时(s999),应立即评估。', false,
'A future trigger cannot repair an invalid source ID.', 'SOURCE_ANNOTATION_INVALID');
$check('出现意识异常时(2026-10-22,s2),应立即评估。', false,
'A future trigger cannot repair a mismatched source date.', 'SOURCE_ANNOTATION_INVALID');
$expect(hash('sha256', json_encode($ledger, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES))
=== '2ebbec0b558347bf4ee128dfb5a9e63fc4e355d58f7226486a2ae96098bd57f6',
'r17 preserves every actual source ID, quote, field, scope, date and ledger schema byte.');
// r17 epistemic followup: a bounded absence of evidence is UNKNOWN, never a patient denial.
$r17EpistemicUnit = "寒热:口干、少津、苔少及大便稍干(s2、s9)可提示津少,尚不足单独成立热证;寒象亦无本次独立阳性证据。寒热属性现有资料不足以确定。\n\n";
$artifact = dirname(__DIR__, 2) . '/app/artifacts/ai_concurrency/tcm_complete_v43r16_openai_deadline_20261024_openai_next_full90_openai.rejected.json';
if (is_file($artifact)) {
$saved = json_decode((string) file_get_contents($artifact), true, 512, JSON_THROW_ON_ERROR);
$expect($saved['rejections'][0]['unit'] === $r17EpistemicUnit,
'The epistemic followup uses the exact retained successor unit without substitution.');
}
$check($r17EpistemicUnit, true, 'The complete retained epistemic paragraph passes old-Guard validation without output rewriting.');
$check('寒象亦无本次独立阳性证据。', true, 'The exact bounded epistemic clause is not an observed clinical absence.');
$epistemicGeneric = Guard::build(['diagnosis_date' => '2026-10-23',
'present_illness' => '有甲项。无乙项。丙项未记录。']);
foreach (['', '亦', '也', '尚', '仍', '均', '并'] as $adverb) {
foreach (['无', '没有', '未见'] as $negative) {
foreach (['', '本次独立阳性', '明确可靠的', '现有充分相应'] as $modifier) {
foreach (['证据', '依据'] as $noun) {
$phrase = $adverb . $negative . $modifier . $noun . '。';
$check('丁诊断' . $phrase, true,
'A generic diagnostic noun plus closed evidential role asserts no patient absence.', '', $epistemicGeneric);
$check('甲项' . $phrase, false,
'Evidence-role wording cannot erase a recorded positive.', 'POSITIVE_TO_UNKNOWN', $epistemicGeneric);
$check('乙项' . $phrase, false,
'Evidence-role wording cannot erase a recorded negative.', 'NEGATIVE_TO_UNKNOWN', $epistemicGeneric);
$check('丙项' . $phrase, true,
'A source-unknown subject stays unknown, without becoming an observed absence.', '', $epistemicGeneric);
}
}
}
}
foreach ([
['口干亦无本次独立阳性证据。', 'POSITIVE_TO_UNKNOWN'],
['心悸亦无本次独立阳性证据。', 'NEGATIVE_TO_UNKNOWN'],
['舌质淡红亦无本次独立阳性证据。', 'POSITIVE_TO_UNKNOWN'],
['舌质紫暗亦无本次独立阳性证据。', 'NEGATIVE_TO_UNKNOWN'],
['舌象亦无本次独立阳性证据。', 'RECORDED_TO_UNKNOWN'],
['脉象亦无本次独立阳性证据。', 'RECORDED_TO_UNKNOWN'],
['现用药亦无本次独立阳性证据。', 'RECORDED_TO_UNKNOWN'],
['寒象亦无恶寒、肢冷等证据。', 'NEGATION_SCOPE_WIDENED'],
['患者有心悸亦无本次独立阳性证据。', 'NEGATIVE_NOT_IN_SOURCE'],
['寒象亦无本次独立阳性证据(s999)。', 'SOURCE_ANNOTATION_INVALID'],
['寒象亦无本次独立阳性证据(2026-10-22,s2)。', 'SOURCE_ANNOTATION_INVALID'],
] as [$text, $code]) $check($text, false,
'Epistemic grammar preserves known facts, examination roles, qualifiers and source/date checks.', $code);
$check('固定刺痛亦无本次独立阳性证据。', true, 'Explicitly unrecorded clinical information remains unknown.');
$check('“无咳嗽咯痰、胸痛心悸、浮肿或腹泻”(s2)。', true,
'An exact source-backed current negative list keeps its original clinical meaning.');
foreach ([',', ',', '且', '但', '并', '并且', '同时', '还', '又', '另'] as $joiner) {
foreach ($assertions as [$assertion, $code]) {
$check('寒象亦无本次独立阳性证据' . $joiner . $assertion . '。', false,
'A bounded evidence noun cannot consume an adjacent actual assertion.', $code);
}
$check('寒象亦无本次独立阳性证据' . $joiner . '现用药未记录。', false,
'A local epistemic statement cannot erase an adjoining known medication field.', 'RECORDED_TO_UNKNOWN');
}
foreach (['望诊', '闻诊', '问诊', '切诊', '舌象', '脉象'] as $role) {
foreach ($assertions as [$assertion, $code]) {
$check($role . ':寒象亦无本次独立阳性证据且' . $assertion . '。', false,
'A report-role heading cannot turn evidence-role grammar into an examination/factual waiver.', $code);
}
}
$epistemicRole = new ReflectionMethod(Guard::class, 'reportEvidenceGapProse');
foreach (['患者有心悸亦无本次独立阳性证据', '患者无固定刺痛', '寒象亦无恶寒、肢冷等证据',
'舌质红绛且寒象无独立证据', '诊见舌质红绛无独立证据',
'寒象无本次独立阳性证据支持患者无固定刺痛', '寒象无完整独立充分足够明确直接可靠相应阳性证据'] as $text) {
$expect($epistemicRole->invoke(null, $text) === $text,
'Clinical predicates, adjacent assertions and non-closed evidence syntax are never removed by this grammar.');
}
$unknownView = (new ReflectionMethod(Guard::class, 'reportClauses'))->invoke(null, $ledger, '固定刺痛亦无本次独立阳性证据');
$unknownParts = (new ReflectionMethod(Guard::class, 'parts'))->invoke(null, $unknownView[0]['text']);
$expect(count($unknownParts) === 1 && $unknownParts[0]['state'] === 'unknown',
'The evidence-absence validation view is explicitly UNKNOWN, never a negative observation.');
$expect(hash('sha256', json_encode($ledger, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES))
=== '2ebbec0b558347bf4ee128dfb5a9e63fc4e355d58f7226486a2ae96098bd57f6',
'The epistemic followup leaves all actual source IDs, quotes, scopes, dates and schema bytes unchanged.');
if ($guardOnly) {
$expect(!class_exists('app\\adminapi\\logic\\tcm\\DiagnosisAiLogic', false)
&& !class_exists(TcmClinicalReportStream::class, false)
&& !class_exists('app\\adminapi\\service\\TcmClinicalEvidenceContract', false),
'Guard-only verification does not load mutable peer classes.');
}
echo "Clinical question/citation boundary: {$count} assertions passed (pure actual fixture; "
. ($guardOnly ? 'Guard only; streaming deferred' : 'exact 1/2/7/31-byte streaming') . ").\n";