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

1587 lines
142 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
declare(strict_types=1);
require dirname(__DIR__) . '/vendor/autoload.php';
require __DIR__ . '/fixtures/CompleteTcmCase.php';
use app\adminapi\service\AiRequiredSource;
use app\adminapi\service\TcmClinicalFactStateGuard;
use app\adminapi\service\TcmEvidenceGuard;
use app\adminapi\service\TcmClinicalReportStream;
use app\adminapi\logic\tcm\PatientAiReportLogic;
$failures = [];
$assertions = 0;
function factStateExpect(bool $condition, string $message): void
{
++$GLOBALS['assertions'];
if (!$condition) $GLOBALS['failures'][] = $message;
}
function factStateCode(array $result, string $code): bool
{
return in_array($code, array_column($result['issues'], 'code'), true);
}
function factStateCheck(array $ledger, string $text, bool $valid, string $label, string $code = ''): void
{
$result = TcmClinicalFactStateGuard::validate($ledger, $text);
factStateExpect($result['valid'] === $valid, $label . ': ' . json_encode($result, JSON_UNESCAPED_UNICODE));
if ($code !== '') factStateExpect(factStateCode($result, $code), $label . ': expected ' . $code);
}
function factStateStreamCheck(array $ledger, string $text, bool $valid, string $label, string $code = '', string $forbidden = ''): void
{
factStateCheck($ledger, $text, $valid, $label, $code);
$visible = '';
$stream = new TcmClinicalReportStream($ledger, static function (string $delta) use (&$visible): bool {
$visible .= $delta;
return true;
});
foreach (mb_str_split($text . "\n\n", 2, 'UTF-8') as $chunk) {
if (!$stream->push($chunk)) break;
}
$finished = $stream->finish();
factStateExpect($finished === $valid && $stream->rejected() === !$valid, $label . ': stream result matches validation');
if ($valid) factStateExpect($visible === $text . "\n\n", $label . ': accepted source bytes are preserved');
else factStateExpect(!str_contains($visible, $forbidden !== '' ? $forbidden : $text), $label . ': rejected observation never reaches the callback');
}
$current = ['id' => 51, 'diagnosis_date' => '2026-10-23', 'chief_complaint' => '气短', 'present_illness' => '活动后气短,无胸痛;未记录固定刺痛或耳鸣。',
'tongue' => '舌淡红,苔少;无黄腻苔、紫暗或瘀斑。', 'pulse' => '脉细弱,按之无力;不浮、不滑。'];
$ledger = TcmClinicalFactStateGuard::build($current);
factStateExpect(count(array_filter($ledger['assertions'], static fn (array $row): bool => $row['state'] === 'unknown')) === 2,
'unknown list items are individually indexed without losing source text');
factStateExpect(in_array('positive', array_column($ledger['assertions'], 'state'), true)
&& in_array('negative', array_column($ledger['assertions'], 'state'), true), 'all three explicit source states coexist');
factStateCheck($ledger, '活动后气短。已记录“无胸痛”;固定刺痛或耳鸣未记录。', true, 'positive, explicit negative and unknown remain distinct');
factStateCheck($ledger, '无固定刺痛。', false, 'unknown is not an observed negative', 'UNKNOWN_TO_NEGATIVE');
factStateCheck($ledger, '无耳鸣。', false, 'enumerated unknown is not an observed negative', 'UNKNOWN_TO_NEGATIVE');
factStateCheck($ledger, '患者存在固定刺痛。', false, 'unknown is not a positive observation', 'UNKNOWN_TO_POSITIVE');
factStateCheck($ledger, '未见活动后气短。', false, 'recorded positive is not negative', 'POSITIVE_TO_NEGATIVE');
factStateCheck($ledger, '患者有胸痛。', false, 'recorded negative is not positive', 'NEGATIVE_TO_POSITIVE');
factStateCheck($ledger, '舌苔少而非腻。', false, 'a narrower negated compound cannot exclude the whole attribute', 'NEGATION_SCOPE_WIDENED');
factStateCheck($ledger, '无黄腻苔,舌无紫暗或瘀斑。', true, 'exact qualified source negation remains usable');
factStateCheck($ledger, '无黄腻。', true, 'omitting only the repeated object noun keeps its adjective scope');
factStateCheck($ledger, '皮肤未见异常皮疹。', false, 'an explicitly claimed physical negative needs a source even without an unknown label', 'NEGATIVE_NOT_IN_SOURCE');
factStateCheck($ledger, '肢体无颤抖或拘挛。', false, 'generic unsupported observed negatives are not invented to fill examination headings', 'NEGATIVE_NOT_IN_SOURCE');
factStateCheck($ledger, '若出现固定刺痛,应重新评估。', true, 'hypothetical future symptom is not asserted to exist');
factStateCheck($ledger, '请补问固定刺痛或耳鸣,不能把未记录视为无。', true, 'collection advice is not an observed negative');
factStateCheck($ledger, '当前无足够证据支持血瘀。', true, 'absence of evidence is not absence of a patient symptom');
factStateCheck($ledger, '因寒热属性不显,不宜把大苦寒清热、辛温助阳作为当前主方向。', true,
'qualitative interpretation does not turn a treatment list into observed negative findings');
factStateCheck($ledger, '脉细弱,按之无力;不浮、不滑。', true, 'lexical force adjective is not a separate clinical absence');
factStateCheck($ledger, '脉弦滑有力。', false, 'a new explicit pulse feature cannot be invented', 'PULSE_FEATURE_NOT_IN_CURRENT_SOURCE');
factStateCheck($ledger, '舌质红绛,苔黄腻。', false, 'new or polarity-reversed tongue features are rejected', 'TONGUE_FEATURE_NOT_IN_CURRENT_SOURCE');
factStateCheck($ledger, '脉弦、数尚未记录。', true, 'new pulse feature names can remain explicitly unknown');
factStateCheck($ledger, '结合语声低微、脉细弱按之无力,说明现有依据。', true, 'ordinary 结合 and voice 低微 do not invent pulse 结 or 微');
factStateCheck($ledger, '若后续脉弦滑、舌红绛,应重新辨证。', true, 'conditional future examinations are not current findings');
factStateCheck($ledger, '患者有轻微胸痛。', false, 'a qualified positive still conflicts with broad explicit source absence', 'NEGATIVE_TO_POSITIVE');
factStateStreamCheck($ledger, '本次无固定刺痛,若后续不适应复诊。', false,
'a conditional tail cannot exempt an actual earlier patient assertion', 'UNKNOWN_TO_NEGATIVE', '无固定刺痛');
factStateStreamCheck($ledger, '无固定刺痛,可作为鉴别依据。', false,
'an evidence tail cannot exempt an actual earlier patient assertion', 'UNKNOWN_TO_NEGATIVE', '无固定刺痛');
factStateStreamCheck($ledger, '无固定刺痛是鉴别依据。', false,
'an evidence word inside the same part cannot exempt a patient assertion', 'UNKNOWN_TO_NEGATIVE', '无固定刺痛');
factStateStreamCheck($ledger, '无固定刺痛,无专门描述。', false,
'a later missing-description phrase cannot exempt the earlier patient negative', 'UNKNOWN_TO_NEGATIVE', '无固定刺痛');
factStateStreamCheck($ledger, '固定刺痛须补问。', true, 'a postfix collection requirement is not a positive observation');
factStateStreamCheck($ledger, '本次不单凭病名选方。', true, 'a reasoning policy is not a physical negative');
factStateStreamCheck($ledger, '不移作闻诊所见。', true, 'an attribution policy is not a physical negative');
factStateStreamCheck($ledger, '若出现固定刺痛,应重新评估。', true, 'a true hypothetical is still not a patient observation');
factStateStreamCheck($ledger, '当前无足够证据支持血瘀。', true, 'pure absence of evidence remains a valid uncertainty statement');
$allergyUnknown = TcmClinicalFactStateGuard::build($current + ['allergy_history_text' => '未记录']);
factStateStreamCheck($allergyUnknown, '过敏史:无药物过敏史。', false, 'Chinese allergy labels retain field-level unknown', 'UNKNOWN_TO_NEGATIVE', '无药物过敏史');
factStateStreamCheck($allergyUnknown, '过敏史:无。', false, 'a standalone absent value cannot erase a field unknown', 'UNKNOWN_TO_NEGATIVE', '无');
factStateStreamCheck($allergyUnknown, "过敏史:\n\n无。", false, 'a paragraph break cannot separate an unknown field from its false absent value', 'UNKNOWN_TO_NEGATIVE', '无');
factStateStreamCheck($ledger, '无药物过敏史。', false, 'a missing allergy field is unknown rather than a negative', 'UNKNOWN_TO_NEGATIVE', '无药物过敏史');
$examUnknown = TcmClinicalFactStateGuard::build(array_replace($current, ['tongue' => '未记录', 'pulse' => '不详']));
factStateStreamCheck($examUnknown, '舌正常。', false, 'a short tongue alias cannot turn unknown into normal', 'UNKNOWN_TO_POSITIVE', '正常');
factStateStreamCheck($examUnknown, '脉正常。', false, 'a short pulse alias cannot turn unknown into normal', 'UNKNOWN_TO_POSITIVE', '正常');
factStateStreamCheck($examUnknown, "舌:\n\n正常。", false, 'a tongue title and a value across paragraphs remain bound', 'UNKNOWN_TO_POSITIVE', '正常');
factStateStreamCheck($examUnknown, "### 脉象\n\n正常。", false, 'a Markdown field title also retains its value state', 'UNKNOWN_TO_POSITIVE', '正常');
factStateStreamCheck($allergyUnknown, '过敏史:未记录。', true, 'a truthful unknown field is accepted');
factStateStreamCheck($allergyUnknown, '无药物过敏史记录。', true, 'absence of an allergy record is not an observed absence of allergy');
factStateStreamCheck($allergyUnknown, '过敏史:没有记录。', true, 'a field may explicitly state that its record is unavailable');
factStateStreamCheck($allergyUnknown, '无论药物过敏史如何,均需核实。', true, 'regardless-of wording is not an observed negative');
factStateStreamCheck($examUnknown, "舌:\n\n未记录。", true, 'a truthful unknown remains accepted across paragraphs');
$allergyDenied = TcmClinicalFactStateGuard::build($current + ['allergy_history_text' => '本次询问无已知药物过敏史']);
factStateStreamCheck($allergyDenied, '本次询问无已知药物过敏史。', true, 'an explicitly recorded denial is not recast as missing');
factStateStreamCheck($allergyDenied, "过敏史:\n\n无。", true, 'an explicitly denied history can retain its original field meaning');
$fieldDenied = TcmClinicalFactStateGuard::build($current + ['allergy_history_text' => '否认']);
factStateStreamCheck($fieldDenied, '过敏史:无。', true, 'an explicit denial retains the context of its history field');
$fieldUnknownWithAdvice = TcmClinicalFactStateGuard::build($current + ['allergy_history_text' => '本次过敏史未记录,已告知复诊核实。']);
factStateStreamCheck($fieldUnknownWithAdvice, '过敏史:无。', false,
'an administrative instruction does not turn an unknown history field into a recorded finding', 'UNKNOWN_TO_NEGATIVE', '无');
factStateStreamCheck($ledger, '家族史:无。', false, 'a missing family history is also unknown', 'UNKNOWN_TO_NEGATIVE', '无');
factStateStreamCheck($ledger, '手术史:无。', false, 'a missing surgery history is also unknown', 'UNKNOWN_TO_NEGATIVE', '无');
$familyDenied = TcmClinicalFactStateGuard::build($current + ['family_history_text' => '否认']);
factStateStreamCheck($familyDenied, '家族史:无。', true, 'a clear text denial alias is preserved without interpreting a numeric code');
$emptyAliasDenied = TcmClinicalFactStateGuard::build($current + ['allergy_history' => '否认', 'allergy_history_text' => '']);
factStateStreamCheck($emptyAliasDenied, '过敏史:无。', true, 'an empty display alias cannot hide an explicit raw field denial');
$surgeryUnknown = TcmClinicalFactStateGuard::build($current + ['surgery_history_text' => '未记录']);
factStateStreamCheck($surgeryUnknown, "手术史\n\n无。", false, 'a text alias preserves unknown across a field heading', 'UNKNOWN_TO_NEGATIVE', '无');
$qualified = TcmClinicalFactStateGuard::build(['present_illness' => '无明显腹胀;未记录皮疹或手麻。']);
factStateCheck($qualified, '无腹胀。', false, 'removing a degree qualifier widens the negative', 'NEGATION_SCOPE_WIDENED');
factStateCheck($qualified, '无明显腹胀。', true, 'preserved degree qualifier is accepted');
factStateCheck($qualified, '没有皮疹。', false, 'different symptoms and negation wording use the same generic rule', 'UNKNOWN_TO_NEGATIVE');
factStateCheck($qualified, '不伴手麻。', false, 'generic alternative negation wording is checked', 'UNKNOWN_TO_NEGATIVE');
$history = AiRequiredSource::submission(['diagnoses' => [
['id' => 51, 'diagnosis_date' => '2026-10-23', 'present_illness' => '未记录头晕。'],
['id' => 9, 'diagnosis_date' => '2020-05-02', 'present_illness' => '无头晕。'],
]]);
$temporal = TcmClinicalFactStateGuard::build(['id' => 51, 'diagnosis_date' => '2026-10-23', 'present_illness' => '未记录头晕。'], $history);
factStateCheck($temporal, '本次无头晕。', false, 'historical negative cannot fill current unknown', 'UNKNOWN_TO_NEGATIVE');
factStateCheck($temporal, '既往2020-05-02无头晕,本次未记录头晕。', true, 'a dated historical quotation remains historical');
factStateCheck($temporal, '头晕本次未记录。', true, 'postfix unknown is preserved');
factStateCheck($temporal, '2020-05-02无头晕。', true, 'an explicit historical date is not silently applied to the current visit');
factStateCheck($temporal, "头晕未记录。\n\n无头晕。", false, 'correct uncertainty in one paragraph cannot excuse a later contradiction', 'UNKNOWN_TO_NEGATIVE');
$datedCurrent = ['id' => 61, 'diagnosis_date' => '2026-10-23', 'present_illness' => '无胸痛心悸;未记录耳鸣。'];
$datedSubmission = ['required_content' => [['key' => 'daily_tracking', 'value' => ['exercise' => [
['record_date' => '2026-10-22', 'note' => '活动后乏力、汗出较明显,无胸痛或晕厥;无耳鸣。'],
]]]]];
$dated = TcmClinicalFactStateGuard::build($datedCurrent, $datedSubmission);
factStateCheck($dated, '2026-10-22活动记录见“活动后乏力、汗出较明显,无胸痛或晕厥”。', true,
'a full historical calendar date excludes the incompatible current compound on the next day');
factStateCheck($dated, '2026年10月22日活动记录见“活动后乏力、汗出较明显,无胸痛或晕厥”。', true,
'Chinese calendar notation has the same exact-day scope');
factStateCheck($dated, '2026-10-23无胸痛。', false, 'same-year historical support cannot widen a different current negative', 'NEGATION_SCOPE_WIDENED');
factStateCheck($dated, '2026-10-23本次“无耳鸣”。', false, 'a mismatched-date old quote cannot erase current uncertainty', 'UNKNOWN_TO_NEGATIVE');
factStateCheck($dated, '本次“无耳鸣”。', false, 'an undated historical quote cannot erase current uncertainty', 'UNKNOWN_TO_NEGATIVE');
factStateCheck($dated, '2026-10-22活动记录“无耳鸣”。', true, 'a correctly dated historical negative remains usable as history');
$sameDay = TcmClinicalFactStateGuard::build(['id' => 62, 'diagnosis_date' => '2026-10-23', 'present_illness' => '无明显腹胀。'],
['required_content' => [['key' => 'daily_tracking', 'value' => ['exercise' => [
['record_date' => '2026-10-23', 'note' => '活动后无腹胀或头痛。'],
]]]]]);
factStateCheck($sameDay, '2026-10-23活动记录“活动后无腹胀或头痛”。', true,
'a complete verified dated source quote is compared with its own scope rather than another context on that day');
factStateCheck($sameDay, '2026-10-23活动记录“无腹胀”。', false,
'a truncated source quote cannot authorize widened negation', 'NEGATION_SCOPE_WIDENED');
factStateCheck($sameDay, '2026-10-23活动记录“活动后无腹胀或头痛”;本次无腹胀。', false,
'source quote attribution never leaks to an unquoted current assertion', 'NEGATION_SCOPE_WIDENED');
$mixed = TcmClinicalFactStateGuard::build(['present_illness' => '偶有头晕,胸痛未记录,未提供手麻情况。']);
factStateCheck($mixed, '无胸痛。', false, 'postfix unknown after a positive clause is indexed independently', 'UNKNOWN_TO_NEGATIVE');
factStateCheck($mixed, '有胸痛。', false, 'postfix unknown cannot become positive either', 'UNKNOWN_TO_POSITIVE');
factStateCheck($mixed, '患者有头晕,胸痛未记录。', true, 'adjacent mixed clauses preserve their own states');
$medications = TcmClinicalFactStateGuard::build(['current_medications' => '使用二甲双胍;未使用胰岛素或中药。']);
factStateCheck($medications, '当前使用胰岛素。', false, 'explicit non-use is not silently rewritten as active therapy', 'NEGATIVE_TO_POSITIVE');
factStateCheck($medications, '使用二甲双胍,未使用胰岛素或中药。', true, 'recorded current treatment and non-use survive together');
$sourceOnly = TcmEvidenceGuard::assess($current);
$menu = json_encode($sourceOnly['facts'], JSON_UNESCAPED_UNICODE);
factStateExpect(str_contains($menu, '活动后气短,无胸痛;未记录固定刺痛或耳鸣。'),
'source menu no longer drops mixed positive/negative/unknown fields');
factStateExpect($sourceOnly['restricted'] === false && isset($sourceOnly['fact_state_ledger']),
'ledger presence does not change established examination completeness');
$unknownExams = TcmEvidenceGuard::assess(['chief_complaint' => '乏力', 'tongue' => '未记录', 'pulse' => '不详']);
factStateExpect($unknownExams['restricted'] === true, 'retaining unknown quotations does not make an examination complete');
factStateCheck($unknownExams['fact_state_ledger'], '舌象无异常。', false, 'unknown examination is not normal', 'UNKNOWN_TO_NEGATIVE');
factStateCheck($unknownExams['fact_state_ledger'], '舌淡红,脉细。', false, 'missing examination text cannot produce observed tongue or pulse features', 'TONGUE_FEATURE_NOT_IN_CURRENT_SOURCE');
factStateCheck($unknownExams['fact_state_ledger'], '舌象未记录,脉象不详。', true, 'missing examination text remains unknown');
$fixture = CompleteTcmCase::current();
$fixtureLedger = TcmClinicalFactStateGuard::build($fixture, AiRequiredSource::submission(CompleteTcmCase::rows()));
$artifactRoot = dirname(__DIR__, 2) . '/app/artifacts/ai_concurrency/';
$replayLocalArtifacts = getenv('TCM_TEST_LOCAL_REPLAYS') !== '0';
$samples = [
'tcm_complete_v13_single_1_openai.md' => true,
'tcm_complete_v13_concurrent4_2_openai.md' => false,
'tcm_complete_v13_concurrent4_3_openai.md' => false,
];
if (!$replayLocalArtifacts) $samples = [];
foreach ($samples as $file => $valid) {
$path = $artifactRoot . $file;
if (!is_file($path)) continue;
$report = (string) file_get_contents($path);
$result = TcmClinicalFactStateGuard::validate($fixtureLedger, $report);
factStateExpect($result['valid'] === $valid, 'prior full report lexical state gate: ' . $file . ' ' . json_encode($result));
if (str_contains($file, 'concurrent4_2')) factStateExpect(factStateCode($result, 'UNKNOWN_TO_NEGATIVE'), 'prior unknown drift is explicitly rejected');
if (str_contains($file, 'concurrent4_3')) factStateExpect(factStateCode($result, 'NEGATION_SCOPE_WIDENED'), 'prior partial-negation drift is explicitly rejected');
foreach ($result['issues'] as $issue) factStateExpect(array_keys($issue) === ['code', 'source_id']
&& preg_match('/^[A-Z_]+$/D', $issue['code']) === 1 && preg_match('/^(?:s[1-9][0-9]*|report)$/D', $issue['source_id']) === 1,
'issue metadata carries static codes and local IDs only');
}
// Reproduce the private pilot's pure canonical source construction. These two
// methods project/redact the supplied fixture only; they do not load records.
$snapshotMethod = new ReflectionMethod(PatientAiReportLogic::class, 'buildSourceSnapshotFromRows');
$redactMethod = new ReflectionMethod(PatientAiReportLogic::class, 'sanitizeSnapshotForUpstream');
$pilotSubmission = $redactMethod->invoke(null, AiRequiredSource::submission($snapshotMethod->invoke(null, CompleteTcmCase::rows())));
$pilotLedger = TcmClinicalFactStateGuard::build($fixture, $pilotSubmission);
// Required regressions are self-contained synthetic literals. Local artifacts
// below are optional additional replays, never a clean-checkout dependency.
factStateStreamCheck($pilotLedger, '本次舌质淡红,舌面少津,苔少;无黄腻苔、紫暗或瘀斑。脉细弱,按之无力;不浮、不滑、不紧。未记录腰酸耳鸣或固定刺痛。', true,
'independent literal preserves positive, negative and unknown source states');
factStateStreamCheck($pilotLedger, '血分辨析:未记录腰酸耳鸣或固定刺痛。现无固定刺痛。', false,
'independent prior drift literal still rejects unknown-to-negative', 'UNKNOWN_TO_NEGATIVE', '无固定刺痛');
factStateStreamCheck($pilotLedger, '食欲一般、苔少而非腻。', false,
'independent prior drift literal still rejects widened negation', 'NEGATION_SCOPE_WIDENED', '非腻');
factStateStreamCheck($pilotLedger, '闻诊方面,语声低微,无喘促及异常气味。听其呼吸,平稳但气息不足,未见痰鸣音。', false,
'independent unsupported-observation literal is still rejected', 'NEGATIVE_NOT_IN_SOURCE', '未见痰鸣音');
factStateStreamCheck($pilotLedger, '**闻诊:**原文记“语声低微,无喘促及异常气味”。其他声音、气味未记录;“无咳嗽咯痰”为问诊原文,不移作闻诊所见。', true,
'independent attribution-policy literal no longer rejects faithful source quotations');
// v16: independent synthetic regressions; no local replay artifact is required.
$instructionLedger = TcmClinicalFactStateGuard::build([
'id' => 71, 'diagnosis_date' => '2026-10-23',
'chief_complaint' => '乏力', 'present_illness' => '无恶寒发热;未记录固定刺痛。',
'tongue' => '舌淡红,苔少;无黄腻苔。', 'pulse' => '脉细弱,不浮。',
'remark' => '无附件,不从图片或心率推断舌脉;未记录眼痛;无明显腹胀。',
]);
$ruleSourceIds = array_keys(array_filter($instructionLedger['sources'],
static fn (array $source): bool => $source['path'] === 'current.remark'));
factStateExpect(!array_filter($instructionLedger['assertions'], static fn (array $assertion): bool =>
in_array($assertion['source_id'], $ruleSourceIds, true)
&& preg_match('/图片|心率|推断/u', $assertion['topic']) === 1),
'an inference rule in a source note never becomes a clinical polarity assertion');
factStateStreamCheck($instructionLedger, '本次“无恶寒发热”,脉“不浮”。', true,
'a pulse label starts its own assertion rather than inheriting the preceding absent marker');
factStateStreamCheck($instructionLedger, '本次“无恶寒发热”,脉细弱。', true,
'a positive pulse phrase also starts its own assertion');
factStateStreamCheck($instructionLedger, '当前无眼痛。', false,
'removing a source rule never removes adjacent unknown clinical notes', 'UNKNOWN_TO_NEGATIVE', '无眼痛');
factStateStreamCheck($instructionLedger, '绝无腹胀。', false,
'an actual widened degree negation in the same source note remains rejected', 'NEGATION_SCOPE_WIDENED', '绝无腹胀');
factStateStreamCheck($instructionLedger, '舌苔少而非腻。', false,
'an actual widened coating negation remains rejected', 'NEGATION_SCOPE_WIDENED', '非腻');
foreach (['后续若', '今后若', '此后若', '后续如果'] as $futurePrefix) {
factStateStreamCheck($instructionLedger, '本次无恶寒发热,' . $futurePrefix . '新出现恶寒、发热或咽痛,应重新评估。', true,
'a localized future condition does not invent present findings: ' . $futurePrefix);
factStateStreamCheck($instructionLedger, '本次无固定刺痛,' . $futurePrefix . '新出现咽痛,应重新评估。', false,
'a future tail cannot exempt a preceding current unknown-to-negative: ' . $futurePrefix,
'UNKNOWN_TO_NEGATIVE', '无固定刺痛');
factStateStreamCheck($instructionLedger, '本次出现恶寒发热,' . $futurePrefix . '出现咽痛,应重新评估。', false,
'a future tail cannot exempt a preceding actual current positive: ' . $futurePrefix,
'NEGATIVE_TO_POSITIVE', '出现恶寒发热');
}
factStateStreamCheck($pilotLedger, '**表里:偏里证。**症状已持续3个月并逐渐出现,以“神疲乏力”“轻微活动后气短”“白天自汗”为主;本次又记录“无恶寒发热”,脉“不浮”,现有证据更支持内在功能失调,而非急性表证。但这只反映本次接诊状态,后续若新出现恶寒、发热、咽痛等,仍须重新判断。', true,
'independent full synthetic paragraph retains its source facts and future qualification');
// v17: source attribution regressions use only the committed synthetic case.
factStateStreamCheck($pilotLedger, '访谈另记“没有最近高热、恶寒、腹泻”。', true,
'a complete interview quote binds to chat rather than a different current compound');
factStateStreamCheck($pilotLedger, '运动记录为“无胸痛或晕厥”。', true,
'a complete exercise quote binds to its exercise record');
factStateStreamCheck($pilotLedger, '运动记录另载“无胸痛或晕厥”。', true,
'an explicitly attributed exercise quote retains its original scope');
factStateStreamCheck($pilotLedger, '2026-10-22运动记录为“无胸痛或晕厥”。', true,
'an exact record date retains the exercise-source binding');
factStateStreamCheck($pilotLedger, '2026年10月22日运动记录另载“无胸痛或晕厥”。', true,
'the existing full-date syntax retains the same exercise-source binding');
factStateStreamCheck($pilotLedger, '腰酸耳鸣、固定刺痛为“未记录”,不能视为明确无,待补采。', true,
'the unknown-status explanation is not the source of the quotation conflict');
factStateStreamCheck($pilotLedger, '本次已记录阴性包括:“无恶寒发热”“无高热烦渴、汗出淋漓或意识异常”“无明显五心烦热、潮热盗汗”“无畏寒肢冷”“无咳嗽咯痰、胸痛心悸、浮肿或腹泻”。', true,
'unchanged compound current-source quotations retain their original negation scope');
factStateStreamCheck($pilotLedger, '运动记录为“无胸痛”。', false,
'a truncated exercise quote cannot widen the original compound', 'ATTRIBUTED_SOURCE_QUOTE_INVALID', '无胸痛');
factStateStreamCheck($pilotLedger, '运动记录为“无胸痛或呼吸困难”。', false,
'a modified exercise quote is rejected instead of gaining attribution', 'ATTRIBUTED_SOURCE_QUOTE_INVALID', '呼吸困难');
factStateStreamCheck($pilotLedger, '运动记录为“无胸痛或晕厥或手麻”。', false,
'an expanded exercise quote is rejected instead of gaining attribution', 'ATTRIBUTED_SOURCE_QUOTE_INVALID', '手麻');
factStateStreamCheck($pilotLedger, '运动记录为“没有最近高热、恶寒、腹泻”。', false,
'a chat quote cannot be relabeled as an exercise record', 'ATTRIBUTED_SOURCE_QUOTE_INVALID', '没有最近高热');
factStateStreamCheck($pilotLedger, '访谈另记“没有最近高热”。', false,
'a truncated interview scope is not a complete source-bound negative', 'ATTRIBUTED_SOURCE_QUOTE_INVALID', '没有最近高热');
factStateStreamCheck($pilotLedger, '当前运动记录为“无胸痛或晕厥”。', false,
'an explicit current claim cannot borrow a historical-source exception', 'NEGATION_SCOPE_WIDENED', '无胸痛');
factStateStreamCheck($pilotLedger, '运动记录为“无胸痛或晕厥”,当前无胸痛。', false,
'historical quote attribution cannot extend to the following current claim', 'NEGATION_SCOPE_WIDENED', '当前无胸痛');
factStateStreamCheck($pilotLedger, '访谈另记“没有最近高热、恶寒、腹泻”,当前无固定刺痛。', false,
'a faithful interview quote cannot exempt a current unknown-to-negative', 'UNKNOWN_TO_NEGATIVE', '当前无固定刺痛');
factStateStreamCheck($pilotLedger, '2026-10-21运动记录为“无胸痛或晕厥”。', false,
'an incorrect explicit record date cannot acquire a source binding', 'ATTRIBUTED_SOURCE_QUOTE_INVALID', '无胸痛');
factStateStreamCheck($qualified, '绝无腹胀。', false,
'degree qualifier broadening remains rejected', 'NEGATION_SCOPE_WIDENED', '绝无腹胀');
factStateStreamCheck($ledger, '舌苔少而非腻。', false,
'coating qualifier broadening remains rejected', 'NEGATION_SCOPE_WIDENED', '非腻');
// v18: a bounded conceptual comparison does not assert its right-hand tongue feature.
factStateStreamCheck($pilotLedger, '舌淡红不等同于舌淡白。', true,
'only the explicit conceptual comparison target is excluded from observations');
factStateStreamCheck($pilotLedger, '舌淡红不等同于舌质淡白,不能据此确立血虚或阳虚。', true,
'a named tongue-quality comparison has the same local boundary');
factStateStreamCheck($pilotLedger, '**舌象:**本次面诊为“舌质淡红,舌面少津,苔少;无黄腻苔、紫暗或瘀斑”。少津、苔少支持津液不足倾向;舌淡红不等同于舌淡白,不能据此确立血虚或阳虚。', true,
'independent full synthetic comparison paragraph preserves grounded observations');
factStateStreamCheck($pilotLedger, '本例舌淡白。', false,
'an actual unrecorded pale-white tongue remains rejected', 'TONGUE_FEATURE_NOT_IN_CURRENT_SOURCE', '舌淡白');
factStateStreamCheck($pilotLedger, '本例舌淡白不等同于舌淡红。', false,
'an unsupported left-hand observation cannot borrow the target exception', 'TONGUE_FEATURE_NOT_IN_CURRENT_SOURCE', '舌淡白');
factStateStreamCheck($pilotLedger, '舌淡红不等同于舌淡白,但本例舌淡白。', false,
'the same feature asserted later is still checked', 'TONGUE_FEATURE_NOT_IN_CURRENT_SOURCE', '本例舌淡白');
factStateStreamCheck($pilotLedger, '舌淡红不等同于舌淡白;当前舌质红绛。', false,
'a later current feature across a clause boundary remains checked', 'TONGUE_FEATURE_NOT_IN_CURRENT_SOURCE', '当前舌质红绛');
factStateStreamCheck($pilotLedger, '本例舌淡白,不能据此确立血虚或阳虚。', false,
'an arbitrary cannot-conclude tail never exempts an actual observation', 'TONGUE_FEATURE_NOT_IN_CURRENT_SOURCE', '舌淡白');
factStateStreamCheck($pilotLedger, '舌淡红不等同于舌淡白红绛。', false,
'an unbounded target cannot consume additional feature tokens', 'TONGUE_FEATURE_NOT_IN_CURRENT_SOURCE', '红绛');
factStateStreamCheck($pilotLedger, '舌质淡红而非红绛。', false,
'a newly asserted negative tongue feature remains ungrounded', 'TONGUE_FEATURE_NOT_IN_CURRENT_SOURCE', '非红绛');
factStateStreamCheck($pilotLedger, '舌质淡红而非明确红绛。', false,
'an explicit degree word does not turn a new negative into a conceptual comparison', 'TONGUE_FEATURE_NOT_IN_CURRENT_SOURCE', '非明确红绛');
factStateStreamCheck($examUnknown, '舌淡红不等同于舌淡白。', false,
'a conceptual target cannot invent its unknown left-hand observation', 'TONGUE_FEATURE_NOT_IN_CURRENT_SOURCE', '舌淡红');
factStateStreamCheck($examUnknown, '舌淡红不等同于舌淡白;当前脉细。', false,
'unknown tongue and pulse still reject actual observations', 'PULSE_FEATURE_NOT_IN_CURRENT_SOURCE', '当前脉细');
factStateStreamCheck($pilotLedger, '本次无五心烦热、潮热盗汗。', false,
'the genuine removal of an observed-negative degree qualifier stays rejected', 'NEGATION_SCOPE_WIDENED', '无五心烦热');
factStateStreamCheck($pilotLedger, '望诊:活动后乏力、汗出较明显,无胸痛或晕厥。', false,
'unattributed historical exercise negatives cannot be moved into current inspection', 'NEGATION_SCOPE_WIDENED', '无胸痛');
// Bounded advice cannot inherit an earlier observed negative or become an exam.
factStateStreamCheck($pilotLedger, '本次舌质淡红,舌面少津,苔少;无黄腻苔、紫暗或瘀斑。无舌像附件,不能补充裂纹、齿痕等未记录内容。', true,
'a strict data-invention prohibition is not an observed tongue feature');
factStateStreamCheck($pilotLedger, '此处“无喘促”指面诊当时闻诊表现,不能替代活动后呼吸困难程度或心肺功能评价。', true,
'an assessment limit never inherits the quoted absent marker');
factStateStreamCheck($pilotLedger, '不能补充裂纹等未记录内容,但本例舌裂纹。', false,
'a following actual finding is not exempt', 'TONGUE_FEATURE_NOT_IN_CURRENT_SOURCE', '舌裂纹');
factStateStreamCheck($pilotLedger, '本例舌裂纹,不能补充其他未记录内容。', false,
'a preceding actual finding is not exempt', 'TONGUE_FEATURE_NOT_IN_CURRENT_SOURCE', '舌裂纹');
factStateStreamCheck($examUnknown, '不能补充舌裂纹等未记录内容;当前脉滑。', false,
'unknown pulse stays unknown after an advisory', 'PULSE_FEATURE_NOT_IN_CURRENT_SOURCE', '当前脉滑');
factStateStreamCheck($pilotLedger, '当前无固定刺痛,不能替代后续检查。', false,
'an advisory cannot exempt a current unknown-to-negative', 'UNKNOWN_TO_NEGATIVE', '当前无固定刺痛');
factStateStreamCheck($pilotLedger, '不能补充裂纹等未记录内容且本例舌裂纹。', false,
'only a full bounded advisory can be exempt', 'TONGUE_FEATURE_NOT_IN_CURRENT_SOURCE', '舌裂纹');
factStateStreamCheck($pilotLedger, '望诊:未提供皮肤润燥资料,无舌照片或其他附件。', true,
'missing attachment metadata is not a patient negative finding');
factStateStreamCheck($pilotLedger, '望诊:无舌照片,当前舌质淡白。', false,
'attachment absence cannot exempt a following actual tongue claim', 'TONGUE_FEATURE_NOT_IN_CURRENT_SOURCE', '舌质淡白');
factStateStreamCheck($pilotLedger, '闻诊:无其他附件,未见痰鸣音。', false,
'attachment absence cannot exempt an unsupported clinical negative', 'NEGATIVE_NOT_IN_SOURCE', '未见痰鸣音');
factStateStreamCheck($pilotLedger, '当前无固定刺痛,无其他附件。', false,
'attachment metadata cannot change the unknown clinical state', 'UNKNOWN_TO_NEGATIVE', '当前无固定刺痛');
foreach (['聊天记录另载', '聊天内容记载', '访谈记载'] as $label) {
factStateStreamCheck($pilotLedger, $label . '“没有最近高热、恶寒、腹泻”。', true,
'standard chat attribution aliases bind only complete original quotations');
}
factStateStreamCheck($pilotLedger, '运动时“无胸痛或晕厥”。', true,
'an explicitly attributed exercise-event quotation binds only its complete original source');
factStateStreamCheck($pilotLedger, '聊天记录另载“没有最近高热”。', false,
'chat aliases cannot exempt a truncated source quote', 'ATTRIBUTED_SOURCE_QUOTE_INVALID', '没有最近高热');
factStateStreamCheck($pilotLedger, '运动时“无胸痛或晕厥”,当前无固定刺痛。', false,
'an event quote cannot exempt a following current unknown-to-negative', 'UNKNOWN_TO_NEGATIVE', '当前无固定刺痛');
factStateStreamCheck($pilotLedger, '当前运动时“无胸痛或晕厥”。', false,
'a historical event quote cannot establish a current absence', 'NEGATION_SCOPE_WIDENED', '无胸痛');
// v23: provenance/capability text does not become a clinical observation.
$hearingSource = array_values(array_filter($pilotLedger['sources'], static fn (array $row): bool => $row['path'] === 'current.symptoms'))[0];
foreach (['现病史', 'symptoms', 'current.symptoms', $hearingSource['id']] as $sourceLabel) {
factStateStreamCheck($pilotLedger, '闻诊:语声低微,无喘促及异常气味(来源:' . $sourceLabel . ')。', true,
'a complete annotation names an existing group, field, path or source ID: ' . $sourceLabel);
}
factStateStreamCheck($pilotLedger, '闻诊:语声低微,无喘促及异常气味(来源:现病史)。', true,
'ASCII parentheses use the same bounded metadata grammar');
factStateStreamCheck($pilotLedger, '闻诊:无皮疹(来源:现病史)。', false,
'valid provenance never supplies an unrecorded clinical absence', 'NEGATIVE_NOT_IN_SOURCE', '无皮疹');
factStateStreamCheck($pilotLedger, '闻诊:语声低微,无喘促及异常气味(来源:不存在分组)。', false,
'an unknown source label is not silently stripped', 'NEGATIVE_NOT_IN_SOURCE', '无喘促');
factStateStreamCheck($pilotLedger, '闻诊:语声低微,无喘促及异常气味(来源:现病史)(患者无固定刺痛)。', false,
'clinical content in a second parenthesis is still checked', 'UNKNOWN_TO_NEGATIVE', '无固定刺痛');
factStateStreamCheck($pilotLedger, '闻诊:语声低微,无喘促及异常气味(来源:现病史,患者无固定刺痛)。', false,
'a source prefix cannot turn a mixed clinical parenthesis into metadata', 'UNKNOWN_TO_NEGATIVE', '无固定刺痛');
factStateStreamCheck($pilotLedger, '一、四诊
望诊:神疲乏力,面色少华(来源:现病史)。
闻诊:语声低微,无喘促及异常气味(来源:现病史)。
问诊:主诉乏力、动则气短、自汗、口干3个月。现病史见轻微活动后气短,白天自汗、活动后加重,口干欲饮、少量频饮。食欲一般,大便稍干、每日1次,小便量较以往略多、夜尿1次。睡眠一般。
明确阴性:无恶寒发热,无高热烦渴、汗出淋漓或意识异常。无明显五心烦热、潮热盗汗,无畏寒肢冷。无咳嗽咯痰、胸痛心悸、浮肿或腹泻。未记录腰酸耳鸣或固定刺痛(来源:现病史)。
舌象:舌质淡红,舌面少津,苔少;无黄腻苔、紫暗或瘀斑(来源:诊断信息)。
脉象:脉细弱,按之无力;不浮、不滑、不紧(来源:诊断信息)。', true,
'portable full synthetic four-diagnosis paragraph preserves all stated source scopes');
factStateStreamCheck($pilotLedger, '**舌象:**本次文字记录为“舌质淡红,舌面少津,苔少”;已记录阴性为“无黄腻苔、紫暗或瘀斑”。因无舌照,不能复核舌体胖瘦、齿痕、裂纹及舌下络脉。', true,
'portable full synthetic tongue paragraph distinguishes observations from a review limit');
factStateStreamCheck($examUnknown, '因无舌照,不能复核舌体胖瘦、齿痕、裂纹及舌下络脉。', true,
'a strict review limit neither fabricates nor fills an unknown examination');
factStateStreamCheck($examUnknown, '因无舌照,不能复核齿痕,当前舌质淡红。', false,
'a review limit cannot fill an unknown tongue with a later observation', 'TONGUE_FEATURE_NOT_IN_CURRENT_SOURCE', '舌质淡红');
factStateStreamCheck($pilotLedger, '因无舌照,不能复核齿痕,当前舌无裂纹。', false,
'actual absence after a bounded review limit remains unsupported', 'TONGUE_FEATURE_NOT_IN_CURRENT_SOURCE', '舌无裂纹');
factStateStreamCheck($pilotLedger, '因无舌照,不能复核齿痕,当前舌有齿痕。', false,
'actual presence after a bounded review limit remains unsupported', 'TONGUE_FEATURE_NOT_IN_CURRENT_SOURCE', '舌有齿痕');
factStateStreamCheck($pilotLedger, '本例舌无裂纹,因无舌照,不能复核齿痕。', false,
'a preceding actual absence cannot borrow a later review limit', 'TONGUE_FEATURE_NOT_IN_CURRENT_SOURCE', '舌无裂纹');
factStateStreamCheck($pilotLedger, '不能复核齿痕且本例舌裂纹。', false,
'the review-limit grammar cannot consume an asserted feature tail', 'TONGUE_FEATURE_NOT_IN_CURRENT_SOURCE', '舌裂纹');
$recordedReview = TcmClinicalFactStateGuard::build(['tongue' => '舌质淡红,有齿痕,无裂纹。']);
factStateStreamCheck($recordedReview, '舌质淡红,有齿痕;无裂纹。因无舌照,不能复核齿痕、裂纹。', true,
'inability to recheck does not relabel known positive or negative features as unknown');
factStateStreamCheck($pilotLedger, '四诊所示阴虚热象不突出,舌质淡红。', true,
'a new tongue subject does not inherit a reasoning predicate');
factStateStreamCheck($pilotLedger, '四诊所示阴虚热象不突出,舌质并非淡红。', false,
'an actual new tongue negation still contradicts the recorded positive', 'POSITIVE_TO_NEGATIVE', '并非淡红');
factStateStreamCheck($pilotLedger, '四诊所示阴虚热象不突出,舌质淡红而非明显红绛。', false,
'fixing a preceding reasoning scope does not approve a new red-crimson negative', 'TONGUE_FEATURE_NOT_IN_CURRENT_SOURCE', '非明显红绛');
factStateStreamCheck($pilotLedger, '乏力、气短和出汗也可能存在贫血、甲状腺、心肺或自主神经等非中医证候原因,中药不能替代必要排查。', true,
'a different-subject safety sentence is not current herbal use');
factStateStreamCheck($pilotLedger, '当前使用中药,中药不能替代必要排查。', false,
'actual herbal use preceding a safety statement remains a contradiction', 'NEGATIVE_TO_POSITIVE', '使用中药');
factStateStreamCheck($pilotLedger, '可能存在贫血,中药不能替代必要排查,当前使用中药。', false,
'actual herbal use following a safety statement remains a contradiction', 'NEGATIVE_TO_POSITIVE', '使用中药');
factStateStreamCheck($pilotLedger, '患者未记录心悸等其他常见相关表现,但这不否定现有主方向。', false,
'an explicitly denied named symptom cannot be recast as unknown', 'NEGATIVE_TO_UNKNOWN', '未记录心悸');
factStateStreamCheck($pilotLedger, '其他声音、心悸未记录。', false,
'a general other-findings prefix cannot exempt a named known symptom', 'NEGATIVE_TO_UNKNOWN', '未记录');
factStateStreamCheck($pilotLedger, '胸痛心悸未记录。', false,
'an entire known negative compound cannot be erased', 'NEGATIVE_TO_UNKNOWN', '未记录');
factStateStreamCheck($pilotLedger, '口干未记录。', false,
'a known positive symptom cannot be recast as unknown', 'POSITIVE_TO_UNKNOWN', '未记录');
factStateStreamCheck($pilotLedger, '舌象未记录。', false,
'an exactly named known examination field cannot become unknown', 'RECORDED_TO_UNKNOWN', '未记录');
factStateStreamCheck($pilotLedger, '固定刺痛未记录;脉亦未记录数象。', true,
'genuine missing symptoms and pulse features retain unknown state');
factStateStreamCheck($pilotLedger, '本次无固定刺痛。', false,
'reverse-state checks do not weaken unknown-to-negative rejection', 'UNKNOWN_TO_NEGATIVE', '无固定刺痛');
factStateStreamCheck($pilotLedger, '舌质并非鲜红,现有舌象不足以确定明显阴虚内热。', false,
'the reproduced fresh-red negative is not silently outside the vocabulary', 'TONGUE_FEATURE_NOT_IN_CURRENT_SOURCE', '并非鲜红');
$freshRedRecorded = TcmClinicalFactStateGuard::build(['tongue' => '舌质鲜红。']);
factStateStreamCheck($freshRedRecorded, '舌质鲜红。', true,
'the new surface term remains usable when explicitly recorded');
factStateStreamCheck($freshRedRecorded, '鲜红未记录。', false,
'a known fresh-red positive also retains its recorded state', 'POSITIVE_TO_UNKNOWN', '未记录');
factStateStreamCheck($freshRedRecorded, '舌质并非鲜红。', false,
'a new surface term also retains source polarity', 'POSITIVE_TO_NEGATIVE', '并非鲜红');
$freshRedDenied = TcmClinicalFactStateGuard::build(['tongue' => '舌质淡红;无鲜红。']);
factStateStreamCheck($freshRedDenied, '舌质淡红;无鲜红。', true,
'an explicit fresh-red negative is allowed when the source actually records it');
factStateStreamCheck($pilotLedger, '**反证与局限:**四诊所示阴虚热象不突出,舌质淡红而非明显红绛,脉亦未记录数象,因此不应将本方解释为治疗阴虚火旺。患者未记录心悸等其他常见相关表现,但这不否定现有主方向。乏力、气短和出汗也可能存在贫血、甲状腺、心肺或自主神经等非中医证候原因,中药不能替代必要排查。', false,
'the portable full v21-like paragraph remains invalid after removing its parser false positives', 'NEGATIVE_TO_UNKNOWN', '未记录心悸');
// v24: attachment nouns, image capability limits and a genuine bare 待补.
factStateStreamCheck($pilotLedger, '**望诊:**本次面诊见“神疲、面色少华”。生命体征:体温36.5℃,血压124/76mmHg,心率78次/分;身高170cm、体重65kg、BMI 22.5kg/m²。无舌照或其他图像附件,不能据图片补充面色、形体及局部体征。', true,
'portable full synthetic observation paragraph keeps the attachment capability limit local');
factStateStreamCheck($pilotLedger, '**舌象:**本次医师记录“舌质淡红,舌面少津,苔少;无黄腻苔、紫暗或瘀斑”。无图像可供复核,舌体胖瘦、齿痕、裂纹及舌下络脉待补。', true,
'portable full synthetic tongue paragraph uses bare postfix missing-data state');
factStateStreamCheck($examUnknown, '无舌照或其他图像附件,不能据图片补充面色、形体及局部体征。', true,
'file absence and a capability limit do not fabricate unknown examinations');
factStateStreamCheck($pilotLedger, '无图像可供复核,齿痕、裂纹待补。', true,
'a preceding file absence does not turn missing features into observed negatives');
factStateStreamCheck($pilotLedger, '无舌照或其他图像附件,不能据图片补充面色、形体及局部体征,当前舌无裂纹。', false,
'a following actual negative is outside the bounded capability limit', 'TONGUE_FEATURE_NOT_IN_CURRENT_SOURCE', '舌无裂纹');
factStateStreamCheck($pilotLedger, '本例舌有裂纹,无舌照或其他图像附件,不能据图片补充面色。', false,
'a preceding actual positive is outside the file metadata exemption', 'TONGUE_FEATURE_NOT_IN_CURRENT_SOURCE', '舌有裂纹');
factStateStreamCheck($pilotLedger, '不能据图片补充面色且本例舌裂纹。', false,
'an appended actual clause cannot masquerade as a capability list', 'TONGUE_FEATURE_NOT_IN_CURRENT_SOURCE', '舌裂纹');
factStateStreamCheck($pilotLedger, '望诊:无其他图像附件,患者无固定刺痛。', false,
'attachment noun suffixes do not exempt a subsequent unknown-to-negative', 'UNKNOWN_TO_NEGATIVE', '无固定刺痛');
factStateStreamCheck($pilotLedger, '望诊:无其他图像附件及皮疹。', false,
'a metadata list cannot consume a clinical negative item', 'NEGATIVE_NOT_IN_SOURCE', '皮疹');
factStateStreamCheck($pilotLedger, '无图像可供复核,齿痕、裂纹待补,舌有裂纹。', false,
'postfix missing-data scope ends before a later actual feature', 'TONGUE_FEATURE_NOT_IN_CURRENT_SOURCE', '舌有裂纹');
factStateStreamCheck($pilotLedger, '无图像可供复核,齿痕待补,患者有心悸。', false,
'postfix missing-data scope cannot conceal a known-negative symptom becoming positive', 'NEGATIVE_TO_POSITIVE', '有心悸');
factStateStreamCheck($pilotLedger, '齿痕待补,患者无固定刺痛。', false,
'postfix missing-data scope cannot conceal an unknown symptom becoming negative', 'UNKNOWN_TO_NEGATIVE', '无固定刺痛');
factStateStreamCheck($pilotLedger, '心悸待补。', false,
'bare postfix missing state cannot overwrite an explicitly denied symptom', 'NEGATIVE_TO_UNKNOWN', '待补');
factStateStreamCheck($pilotLedger, '口干待补。', false,
'bare postfix missing state cannot overwrite a positive symptom', 'POSITIVE_TO_UNKNOWN', '待补');
factStateStreamCheck($pilotLedger, '舌象待补。', false,
'a recorded tongue field cannot become bare missing data', 'RECORDED_TO_UNKNOWN', '待补');
factStateStreamCheck($pilotLedger, '脉待补。', false,
'a recorded short pulse alias cannot become bare missing data', 'RECORDED_TO_UNKNOWN', '待补');
factStateStreamCheck($pilotLedger, '舌象:待补。', false,
'a field label does not turn missing-data wording into blanket collection advice', 'RECORDED_TO_UNKNOWN', '待补');
factStateStreamCheck($pilotLedger, '待补脉象。', false,
'a prefix bare missing marker also retains the known field state', 'RECORDED_TO_UNKNOWN', '待补');
factStateStreamCheck($pilotLedger, "舌象:" . chr(10) . chr(10) . "待补。", false,
'a known field title remains bound across a paragraph break', 'RECORDED_TO_UNKNOWN', '待补');
factStateStreamCheck($examUnknown, "舌象:" . chr(10) . chr(10) . "待补。", true,
'a genuinely unknown field can retain bare missing state across paragraphs');
factStateStreamCheck($pilotLedger, '固定刺痛待补;待补齿痕。', true,
'genuine missing source items retain unknown state in either word order');
factStateStreamCheck($recordedReview, '齿痕、裂纹待补。', false,
'bare missing state does not erase explicitly recorded positive or negative features', 'POSITIVE_TO_UNKNOWN', '待补');
factStateExpect(factStateCode(TcmClinicalFactStateGuard::validate($recordedReview, '齿痕、裂纹待补。'), 'NEGATIVE_TO_UNKNOWN'),
'a known negative feature in the same list also retains its recorded state');
factStateStreamCheck($pilotLedger, '舌质淡红非青紫。', false,
'a new color negative remains rejected after attachment fixes', 'TONGUE_FEATURE_NOT_IN_CURRENT_SOURCE', '非青紫');
factStateStreamCheck($pilotLedger, '本例无痰多。', false,
'a new patient sputum negative remains rejected after attachment fixes', 'NEGATIVE_NOT_IN_SOURCE', '无痰多');
factStateStreamCheck($pilotLedger, '舌质淡红而非明确红绛。', false,
'a new explicit red-crimson negative remains rejected', 'TONGUE_FEATURE_NOT_IN_CURRENT_SOURCE', '非明确红绛');
// v25: strict bare citations, copular unknowns and a closed hearing reference.
foreach (['(s3)', '(s2, s3)', '(s2, s3)', '(2026-10-23,[s3])', '(s3,2026年10月23日)', '(2026-10-23)'] as $citation) {
factStateStreamCheck($pilotLedger, '闻诊:“无喘促及异常气味”' . $citation . '。', true,
'a complete source annotation contains only existing IDs and a verified date: ' . $citation);
}
foreach (['(s999)', '(s3,s999)', '(2026-10-22,[s3])', '(2026-13-99,s3)', '(2026-10-23,2026-10-22,s3)', '(2026-10-22)'] as $citation) {
factStateStreamCheck($pilotLedger, '闻诊:“无喘促及异常气味”' . $citation . '。', false,
'unknown IDs or inconsistent dates cannot be removed as trusted metadata: ' . $citation,
'SOURCE_ANNOTATION_INVALID', '无喘促');
}
factStateStreamCheck($pilotLedger, '闻诊:无喘促及异常气味(s3,患者无固定刺痛)。', false,
'a mixed citation cannot strip a patient unknown-to-negative', 'UNKNOWN_TO_NEGATIVE', '无固定刺痛');
factStateStreamCheck($pilotLedger, '闻诊:无喘促及异常气味(s3,患者有心悸)。', false,
'a mixed citation cannot strip an actual positive assertion', 'NEGATIVE_TO_POSITIVE', '有心悸');
factStateStreamCheck($pilotLedger, '闻诊:无喘促及异常气味([s3,患者有心悸])。', false,
'nested citation brackets do not erase clinical content inside a mixed parenthesis', 'NEGATIVE_TO_POSITIVE', '有心悸');
factStateStreamCheck($pilotLedger, '闻诊:无喘促及异常气味([s3,患者无固定刺痛])。', false,
'nested brackets do not erase an unknown-to-negative inside a mixed parenthesis', 'UNKNOWN_TO_NEGATIVE', '无固定刺痛');
factStateStreamCheck($pilotLedger, '闻诊:语声低微,无喘促及异常气味(s2, s3)。本次舌质淡红,舌面少津,苔少(s9);无黄腻苔、紫暗或瘀斑(s9)。未记录腰酸耳鸣或固定刺痛(s2)。', true,
'portable synthetic negative list retains its source states beside bare existing IDs');
$historicalSource = array_values(array_filter($dated['sources'], static fn (array $row): bool => str_starts_with($row['path'], 'daily_tracking.exercise.')))[0];
factStateStreamCheck($dated, '运动记录为“无胸痛或晕厥”(2026-10-22,' . $historicalSource['id'] . ')。', true,
'a correct historical citation retains its event date and original compound scope');
factStateStreamCheck($dated, '运动记录为“无胸痛或晕厥”(2026-10-23,' . $historicalSource['id'] . ')。', false,
'a historical source cannot be assigned the current date', 'SOURCE_ANNOTATION_INVALID', '无胸痛');
factStateStreamCheck($dated, '本次无耳鸣(2026-10-22,' . $historicalSource['id'] . ')。', false,
'a valid historical citation cannot wash an explicitly current patient absence', 'NEGATIVE_NOT_IN_SOURCE', '无耳鸣');
factStateStreamCheck($dated, '运动记录为“无胸痛”(2026-10-22,' . $historicalSource['id'] . ')。', false,
'valid metadata does not approve a truncated historical negative quote', 'ATTRIBUTED_SOURCE_QUOTE_INVALID', '无胸痛');
foreach (['属', '为', '属于'] as $copula) {
factStateStreamCheck($pilotLedger, '腰酸耳鸣、固定刺痛' . $copula . '“未记录腰酸耳鸣或固定刺痛”(s2),属于未知,不能作为肯定或否定依据。', true,
'a copular explanation binds exactly the same named unknown subjects: ' . $copula);
}
factStateStreamCheck($pilotLedger, '腰酸耳鸣、固定刺痛属“未记录腰酸耳鸣或固定刺痛”,是未知而非否认。', true,
'the alternate closed unknown explanation keeps its full tail');
factStateStreamCheck($pilotLedger, '腰酸耳鸣、固定刺痛为未记录腰酸耳鸣或固定刺痛,属于未知。', true,
'unknown-state binding does not depend on decorative quotation marks');
factStateStreamCheck($pilotLedger, '患者有腰酸耳鸣、固定刺痛为“未记录腰酸耳鸣或固定刺痛”。', false,
'a copula cannot erase an original explicit positive predicate', 'UNKNOWN_TO_POSITIVE', '有腰酸耳鸣');
factStateStreamCheck($pilotLedger, '患者出现固定刺痛属“未记录固定刺痛”。', false,
'an occurrence predicate remains an actual assertion before the copula', 'UNKNOWN_TO_POSITIVE', '出现固定刺痛');
factStateStreamCheck($pilotLedger, '固定刺痛属“未记录固定刺痛”,患者有固定刺痛。', false,
'a following explicit positive cannot borrow the unknown explanation', 'UNKNOWN_TO_POSITIVE', '有固定刺痛');
factStateStreamCheck($pilotLedger, '固定刺痛属“未记录固定刺痛”,本例舌质淡白。', false,
'an implicit actual examination after the comma stays outside unknown scope', 'TONGUE_FEATURE_NOT_IN_CURRENT_SOURCE', '舌质淡白');
factStateStreamCheck($pilotLedger, '腰酸耳鸣为“未记录固定刺痛”。', false,
'different subject and unknown payload cannot acquire the copular binding', 'UNKNOWN_TO_POSITIVE', '腰酸耳鸣');
factStateStreamCheck($pilotLedger, '心悸为“未记录心悸”,属于未知。', false,
'same-subject binding still checks a known negative recast as unknown', 'NEGATIVE_TO_UNKNOWN', '未记录心悸');
factStateStreamCheck($pilotLedger, '口干为“未记录口干”。', false,
'same-subject binding also preserves known positives', 'POSITIVE_TO_UNKNOWN', '未记录口干');
$hearingReference = '此处仅表示闻诊当时未见上述表现';
factStateStreamCheck($pilotLedger, '**闻诊:**本次记载语声低微,可作为气虚倾向的支持。“无喘促及异常气味”(2026-10-23,[s3]);' . $hearingReference . ',不能替代活动状态下呼吸评估。', true,
'portable full hearing paragraph binds a closed reference to the adjacent verified negative');
factStateStreamCheck($pilotLedger, '“无喘促及异常气味”(s3);' . $hearingReference . '。', true,
'an adjacent exact current-source negative supports the closed reference');
factStateStreamCheck($pilotLedger, $hearingReference . '。', false,
'the closed reference cannot appear without an antecedent', 'UNSUPPORTED_NEGATIVE_REFERENCE', '未见上述表现');
factStateStreamCheck($pilotLedger, '“语声低微”(s3);' . $hearingReference . '。', false,
'a positive quoted antecedent cannot become a negative by reference', 'UNSUPPORTED_NEGATIVE_REFERENCE', '未见上述表现');
factStateStreamCheck($pilotLedger, '“未记录固定刺痛”(s2);' . $hearingReference . '。', false,
'an unknown quoted antecedent cannot become a negative by reference', 'UNSUPPORTED_NEGATIVE_REFERENCE', '未见上述表现');
factStateStreamCheck($pilotLedger, '“无喘促及异常气味”(s2);' . $hearingReference . '。', false,
'a known but wrong source ID cannot verify the hearing antecedent', 'UNSUPPORTED_NEGATIVE_REFERENCE', '未见上述表现');
factStateStreamCheck($pilotLedger, '“无喘促及异常气味”(2026-10-22,s3);' . $hearingReference . '。', false,
'a mismatched date never gets washed away before reference validation', 'SOURCE_ANNOTATION_INVALID', '无喘促');
factStateStreamCheck($pilotLedger, '“无喘促及异常气味”(s3);' . $hearingReference . '(2026-10-22,s3)。', false,
'the referring clause cannot switch the supported source date', 'SOURCE_ANNOTATION_INVALID', '未见上述表现');
factStateStreamCheck($pilotLedger, '“无喘促及异常气味”(s3);' . $hearingReference . '(s2)。', false,
'the referring clause cannot switch to a different existing source', 'UNSUPPORTED_NEGATIVE_REFERENCE', '未见上述表现');
factStateStreamCheck($pilotLedger, '“无喘促及异常气味”(s3);食欲一般;' . $hearingReference . '。', false,
'an intervening statement clears the closed antecedent', 'UNSUPPORTED_NEGATIVE_REFERENCE', '未见上述表现');
factStateStreamCheck($pilotLedger, '“无喘促及异常气味”(s3);' . $hearingReference . ',患者有心悸。', false,
'the reference cannot exempt a later actual positive assertion', 'NEGATIVE_TO_POSITIVE', '有心悸');
factStateStreamCheck($pilotLedger, '“无喘促及异常气味”(s3);' . $hearingReference . ',患者无固定刺痛。', false,
'the reference cannot exempt a later actual unknown-to-negative', 'UNKNOWN_TO_NEGATIVE', '无固定刺痛');
// v26: the three parser fixes remain portable without retained live artifacts.
$dyspneaReference = '该记录支持气短尚未表现为闻诊可见的喘促';
$dyspneaParagraph = '**闻诊:**语声低微。四诊已记录阴性:“无喘促及异常气味”(s3,2026-10-23)。'
. $dyspneaReference . ',但不能替代活动时呼吸频率、语句连续性及肺部听诊。';
factStateStreamCheck($pilotLedger, $dyspneaParagraph, true,
'portable full hearing paragraph keeps a bounded non-occurrence interpretation tied to its source');
factStateStreamCheck($pilotLedger, '“无喘促及异常气味”(2026-10-23,s3);' . $dyspneaReference . '。', true,
'a verified hearing negative supports only the closed dyspnea interpretation');
foreach (['', '“语声低微”(s3);', '“未记录固定刺痛”(s2);', '“无喘促及异常气味”(s2);', '“无喘促及异常气味”(s3);食欲一般;'] as $antecedent) {
factStateStreamCheck($pilotLedger, $antecedent . $dyspneaReference . '。', false,
'a closed dyspnea interpretation requires an adjacent exact hearing negative: ' . $antecedent,
'UNSUPPORTED_NEGATIVE_REFERENCE', '尚未表现为');
}
factStateStreamCheck($pilotLedger, '“无喘促及异常气味”(2026-10-22,s3);' . $dyspneaReference . '。', false,
'an incorrect antecedent date is not discarded before dyspnea reference binding', 'SOURCE_ANNOTATION_INVALID', '无喘促');
factStateStreamCheck($pilotLedger, '“无喘促及异常气味”(s3);' . $dyspneaReference . '(s2)。', false,
'the dyspnea interpretation cannot change its hearing source', 'UNSUPPORTED_NEGATIVE_REFERENCE', '尚未表现为');
factStateStreamCheck($pilotLedger, '“无喘促及异常气味”(s3);' . $dyspneaReference . '(2026-10-22,s3)。', false,
'the dyspnea interpretation cannot change its source date', 'SOURCE_ANNOTATION_INVALID', '尚未表现为');
$qualifiedHearing = TcmClinicalFactStateGuard::build(['chief_complaint' => '气短', 'symptoms' => '闻诊无明显喘促及异常气味。']);
factStateStreamCheck($qualifiedHearing, '“无明显喘促及异常气味”;' . $dyspneaReference . '。', false,
'the reference cannot delete the hearing source degree qualifier', 'UNSUPPORTED_NEGATIVE_REFERENCE', '尚未表现为');
$otherHearing = TcmClinicalFactStateGuard::build(['chief_complaint' => '气短', 'symptoms' => '闻诊无异常气味。']);
factStateStreamCheck($otherHearing, '“无异常气味”;' . $dyspneaReference . '。', false,
'a different hearing absence cannot supply an absent dyspnea target', 'UNSUPPORTED_NEGATIVE_REFERENCE', '尚未表现为');
$hearingWithoutDyspnea = TcmClinicalFactStateGuard::build(['symptoms' => '闻诊无喘促及异常气味。']);
factStateStreamCheck($hearingWithoutDyspnea, '“无喘促及异常气味”;' . $dyspneaReference . '。', false,
'a closed interpretation cannot invent its positive gas-shortness subject', 'UNSUPPORTED_NEGATIVE_REFERENCE', '尚未表现为');
factStateStreamCheck($pilotLedger, '“无喘促及异常气味”(s3);' . $dyspneaReference . ',患者有心悸。', false,
'a following patient positive cannot borrow the dyspnea reference', 'NEGATIVE_TO_POSITIVE', '有心悸');
factStateStreamCheck($pilotLedger, '“无喘促及异常气味”(s3);' . $dyspneaReference . ',患者无固定刺痛。', false,
'a following patient negative cannot borrow the dyspnea reference', 'UNKNOWN_TO_NEGATIVE', '无固定刺痛');
factStateStreamCheck($pilotLedger, '“无喘促及异常气味”(s3);' . $dyspneaReference . ',但不能替代后续检查,患者有心悸。', false,
'an advisory tail after the closed reference does not hide a later actual patient positive', 'NEGATIVE_TO_POSITIVE', '有心悸');
factStateStreamCheck($pilotLedger, '“无喘促及异常气味”(s3);' . $dyspneaReference . '且患者有喘促。', false,
'an asserted tail does not fit the closed reference grammar', 'NEGATIVE_TO_POSITIVE', '有喘促');
factStateStreamCheck($pilotLedger, '**切诊:**汗出及四肢温度的触诊、腹诊、按诊未记录,待补。', true,
'a portable complete palpation paragraph names missing examination records rather than absent sweat symptoms');
factStateStreamCheck($pilotLedger, '汗出的触诊未记录。', true,
'an explicit method noun does not relabel the recorded symptom itself');
factStateStreamCheck($pilotLedger, '口干及心悸的触诊、腹诊未记录。', true,
'a coordinated method subject retains its own record scope');
factStateStreamCheck($pilotLedger, '汗出未记录。', false,
'without a method noun an explicit source symptom still cannot be recast as unknown', 'NEGATIVE_TO_UNKNOWN', '未记录');
factStateStreamCheck($pilotLedger, '汗出及四肢温度未记录。', false,
'a coordinated symptom list without a method noun retains source state checks', 'NEGATIVE_TO_UNKNOWN', '未记录');
factStateStreamCheck($pilotLedger, '汗出及四肢温度的触诊、心悸未记录。', false,
'a method noun cannot absorb a later known negative symptom', 'NEGATIVE_TO_UNKNOWN', '未记录');
factStateStreamCheck($pilotLedger, '口干、汗出及四肢温度的触诊未记录。', false,
'a method noun cannot absorb an earlier known positive symptom', 'POSITIVE_TO_UNKNOWN', '未记录');
factStateStreamCheck($pilotLedger, '心悸的触诊及口干未记录。', false,
'a symptom after the method noun is not hidden in its coordinated subject', 'POSITIVE_TO_UNKNOWN', '未记录');
factStateStreamCheck($pilotLedger, '汗出及四肢温度的触诊未记录,患者无固定刺痛。', false,
'a missing method record never authorizes an actual negative tail', 'UNKNOWN_TO_NEGATIVE', '无固定刺痛');
factStateStreamCheck($pilotLedger, '患者有心悸,汗出及四肢温度的触诊未记录。', false,
'an actual positive before the method record is checked independently', 'NEGATIVE_TO_POSITIVE', '有心悸');
$knownMethod = TcmClinicalFactStateGuard::build(['present_illness' => '记录汗出的触诊。']);
factStateStreamCheck($knownMethod, '汗出的触诊未记录。', false,
'a method record explicitly present in the source also cannot be recast as unknown', 'POSITIVE_TO_UNKNOWN', '未记录');
$sameDayUnknownParagraph = '**问诊:**主症持续3个月,为神疲乏力、轻微活动后气短、白天自汗且活动后加重,并有口干欲饮、少量频饮。'
. '食欲一般,大便稍干、每日1次,小便量较以往略多、夜尿1次,睡眠一般。四诊已记录阴性:“无恶寒发热,”(2026-10-23,s2);'
. '“无高热烦渴、汗出淋漓或意识异常”(同日,s2);“无明显五心烦热、潮热盗汗,”(同日,s2);“无畏寒肢冷”(同日,s2);'
. '“无咳嗽咯痰、胸痛心悸、浮肿或腹泻”(同日,s2);“无明显急性感染诱因”(同日,s2)。'
. '腰酸耳鸣、固定刺痛为“未记录腰酸耳鸣或固定刺痛”(同日,s2),属于未知,不能作为阴性。';
factStateStreamCheck($pilotLedger, $sameDayUnknownParagraph, true,
'portable full question paragraph binds relative-date metadata before normalizing the identical unknown subject');
$portableWire = $dyspneaParagraph . "\r\n\r\n" . '**切诊:**汗出及四肢温度的触诊、腹诊、按诊未记录,待补。'
. "\r\n\r\n" . $sameDayUnknownParagraph;
$portableVisible = '';
$portableStream = new TcmClinicalReportStream($pilotLedger, static function (string $delta) use (&$portableVisible): bool {
$portableVisible .= $delta;
return true;
});
$portablePushed = true;
// Byte chunks deliberately split UTF-8 characters and annotation/CRLF tokens.
foreach (str_split($portableWire, 7) as $chunk) {
if (!$portableStream->push($chunk)) { $portablePushed = false; break; }
}
factStateExpect($portablePushed && $portableStream->finish() && !$portableStream->rejected(),
'all three portable paragraphs stream through split UTF-8 and CRLF byte boundaries');
factStateExpect($portableVisible === $portableWire,
'normalization cannot alter any accepted original byte including CRLF and a final undelimited paragraph');
foreach (['(同日,s2)', '(同日, s2)', '(同日,[s2])'] as $annotation) {
factStateStreamCheck($pilotLedger, '“无恶寒发热”(2026-10-23,s2);固定刺痛为“未记录固定刺痛”' . $annotation . ',属于未知。', true,
'a same-day citation inherits only the preceding verified calendar date: ' . $annotation);
}
factStateStreamCheck($pilotLedger, '“无恶寒发热”(s2);固定刺痛为“未记录固定刺痛”(同日,s2)。', false,
'same-day metadata without a preceding full calendar date is rejected', 'SOURCE_ANNOTATION_INVALID', '未记录固定刺痛');
factStateStreamCheck($pilotLedger, '“无恶寒发热”(2026-10-23,s2);固定刺痛为“未记录固定刺痛”(同日,s999)。', false,
'a relative date cannot make an unknown source ID trusted', 'SOURCE_ANNOTATION_INVALID', '未记录固定刺痛');
factStateStreamCheck($pilotLedger, '“无恶寒发热”(2026-10-23,s2);固定刺痛为“未记录固定刺痛”(同日,患者有心悸)。', false,
'clinical text inside a same-day parenthesis remains a patient assertion', 'NEGATIVE_TO_POSITIVE', '有心悸');
factStateStreamCheck($pilotLedger, '“无恶寒发热”(2026-10-23,s2);固定刺痛为“未记录固定刺痛”(同日,[s2,患者无固定刺痛])。', false,
'nested brackets in relative-date metadata cannot strip a clinical negative', 'UNKNOWN_TO_NEGATIVE', '无固定刺痛');
factStateStreamCheck($pilotLedger, '“无恶寒发热”(2026-10-23,s2);心悸为“未记录心悸”(同日,s2),属于未知。', false,
'valid relative-date metadata does not erase a known negative', 'NEGATIVE_TO_UNKNOWN', '未记录心悸');
factStateStreamCheck($pilotLedger, '“无恶寒发热”(2026-10-23,s2);口干为“未记录口干”(同日,s2)。', false,
'valid relative-date metadata does not erase a known positive', 'POSITIVE_TO_UNKNOWN', '未记录口干');
factStateStreamCheck($pilotLedger, '“无恶寒发热”(2026-10-23,s2);患者有固定刺痛为“未记录固定刺痛”(同日,s2)。', false,
'valid relative-date metadata does not exempt a positive copular subject', 'UNKNOWN_TO_POSITIVE', '有固定刺痛');
factStateStreamCheck($pilotLedger, '“无恶寒发热”(2026-10-23,s2);腰酸耳鸣为“未记录固定刺痛”(同日,s2)。', false,
'relative-date parsing does not bind different unknown subjects', 'UNKNOWN_TO_POSITIVE', '腰酸耳鸣');
factStateStreamCheck($pilotLedger, '“无恶寒发热”(2026-10-23,s2);固定刺痛为“未记录固定刺痛”(同日,s2),患者无固定刺痛。', false,
'a later actual absence stays outside the same-day unknown explanation', 'UNKNOWN_TO_NEGATIVE', '无固定刺痛');
factStateStreamCheck($pilotLedger, '“无恶寒发热”(2026-10-23,s2)。' . "\n\n" . '固定刺痛为“未记录固定刺痛”(同日,s2)。', false,
'an ambiguous same-day antecedent cannot carry across paragraph boundaries', 'SOURCE_ANNOTATION_INVALID', '未记录固定刺痛');
factStateStreamCheck($dated, '运动记录为“无胸痛或晕厥”(2026-10-22,' . $historicalSource['id'] . ');运动记录为“无胸痛或晕厥”(同日,' . $historicalSource['id'] . ')。', true,
'a relative historical date remains attached to the correctly dated event');
factStateStreamCheck($dated, '运动记录为“无胸痛或晕厥”(2026-10-22,' . $historicalSource['id'] . ');本次耳鸣未记录(同日,s1)。', false,
'a same-day annotation cannot move a historical event onto a current source date', 'SOURCE_ANNOTATION_INVALID', '未记录');
factStateStreamCheck($dated, '“无胸痛心悸”(2026-10-23,s1);运动记录为“无胸痛或晕厥”(同日,' . $historicalSource['id'] . ')。', false,
'a same-day annotation cannot move a historical source onto the current day', 'SOURCE_ANNOTATION_INVALID', '无胸痛或晕厥');
factStateStreamCheck($dated, '运动记录为“无胸痛或晕厥”(2026-10-22,' . $historicalSource['id'] . ');运动记录为“无胸痛”(同日,' . $historicalSource['id'] . ')。', false,
'a valid relative historical date still requires a complete original negative scope', 'ATTRIBUTED_SOURCE_QUOTE_INVALID', '无胸痛');
foreach (['无急性感染诱因。', '无五心烦热、潮热盗汗。', '无烦渴。'] as $broadened) {
factStateStreamCheck($pilotLedger, $broadened, false,
'the real v25 degree or compound-scope deletion remains rejected: ' . $broadened,
'NEGATION_SCOPE_WIDENED', $broadened);
}
// v27: report structure, location references and file-availability explanations
// retain their roles. These complete literals need no retained local artifacts.
$pulseLocationParagraph = '**切诊:**脉诊结果见下方独立脉象行;腹诊、肌肤温度及按诊资料未记录。';
$unknownIndexParagraph = '**显式未知/待补:**1.“未记录腰酸耳鸣或固定刺痛”(s2,2026-10-23),二者均保持未知;'
. '还需补采体位性变化、汗出时冷热感、饮水总量、口渴昼夜规律及完整腹诊。'
. '附件缺失只表示不能图像复核,不代表舌体不存在其他异常。';
$attachmentRoleParagraph = "**明确未知/待补:** \n1. “未记录腰酸耳鸣或固定刺痛”(s2,2026-10-23),两项均保持未知。 \n"
. '2. “无附件”(s11,2026-10-23),表示没有可复核图像或报告,并非患者无异常。'
. '氧饱和度、腰围及部分系统查体亦未记录。';
foreach ([$pulseLocationParagraph, $unknownIndexParagraph, $attachmentRoleParagraph] as $index => $paragraph) {
factStateStreamCheck($pilotLedger, $paragraph, true,
'portable complete v26 rejected unit retains its report role: ' . $index);
}
foreach (['脉诊结果见下方独立脉象行。', '切诊:脉象结果见下文脉象行。'] as $reference) {
factStateStreamCheck($pilotLedger, $reference, true,
'a closed location reference does not invent pulse 结 from 结果');
factStateStreamCheck($examUnknown, $reference, true,
'a location reference supplies no observed value even when the pulse field is unknown');
}
factStateStreamCheck($pilotLedger, '脉诊结果见下方独立脉象行,脉细弱,按之无力。', true,
'a location reference can precede a faithful actual pulse value');
foreach (['脉结。', '脉滑。', '脉诊结果见下方独立脉象行,患者脉滑。',
'患者脉滑,脉诊结果见下方独立脉象行。', '脉诊结果见下方独立脉象行且患者脉滑。'] as $observation) {
factStateStreamCheck($pilotLedger, $observation, false,
'actual pulse findings remain visible beside or outside the closed reference',
'PULSE_FEATURE_NOT_IN_CURRENT_SOURCE', str_contains($observation, '脉滑') ? '脉滑' : '脉结');
}
factStateStreamCheck($pilotLedger, '脉诊结果见下方独立脉象行,患者无固定刺痛。', false,
'a reference cannot exempt an actual patient negative tail', 'UNKNOWN_TO_NEGATIVE', '无固定刺痛');
factStateStreamCheck($pilotLedger, '患者有心悸,脉诊结果见下方独立脉象行。', false,
'a reference cannot exempt an earlier actual patient positive', 'NEGATIVE_TO_POSITIVE', '有心悸');
factStateStreamCheck($pilotLedger, '脉诊结果见下方独立脉象行,口干未记录。', false,
'a reference cannot erase an already recorded positive', 'POSITIVE_TO_UNKNOWN', '未记录');
$numberedLedger = TcmClinicalFactStateGuard::build(['present_illness' => 'eGFR 96mL/min/1.73m²;口干;无胸痛;未记录耳鸣。']);
foreach (['显式未知/待补:1.未记录耳鸣。', '明确未知/待补:1. 未记录耳鸣。',
"明确未知/待补:\n1. 未记录耳鸣。", '未知/待补:2、耳鸣未记录。'] as $heading) {
factStateStreamCheck($numberedLedger, $heading, true,
'structural heading and list number cannot become a fragment of a recorded lab value');
}
factStateStreamCheck($numberedLedger, '显式未知/待补:1.口干未记录。', false,
'number removal retains an actual known positive to unknown', 'POSITIVE_TO_UNKNOWN', '未记录');
factStateStreamCheck($numberedLedger, '明确未知/待补:1.胸痛未记录。', false,
'number removal retains an actual known negative to unknown', 'NEGATIVE_TO_UNKNOWN', '未记录');
factStateStreamCheck($numberedLedger, '显式未知/待补:1.患者有耳鸣。', false,
'an uncertainty heading cannot exempt a positive unknown symptom', 'UNKNOWN_TO_POSITIVE', '有耳鸣');
factStateStreamCheck($numberedLedger, '1.73m²未记录。', false,
'a real decimal topic is not consumed as a numbered-list marker', 'POSITIVE_TO_UNKNOWN', '未记录');
$attachmentExplanation = '无附件,表示没有可复核图像或报告,并非患者无异常';
$recheckExplanation = '附件缺失只表示不能图像复核,不代表舌体不存在其他异常';
foreach ([$attachmentExplanation, $recheckExplanation,
'未提供舌照,仅表示没有可复核图片或报告,不代表本例没有其他异常'] as $metadata) {
factStateStreamCheck($pilotLedger, $metadata . '。', true,
'a bounded metadata role does not assert the nested patient absence');
factStateStreamCheck($pilotLedger, $metadata . ',脉细弱。', true,
'a metadata explanation can coexist with a faithful observed pulse');
factStateStreamCheck($pilotLedger, $metadata . ',患者无固定刺痛。', false,
'metadata roles do not hide a following actual unknown-to-negative', 'UNKNOWN_TO_NEGATIVE', '无固定刺痛');
factStateStreamCheck($pilotLedger, '患者有心悸,' . $metadata . '。', false,
'metadata roles do not hide an earlier actual negative-to-positive', 'NEGATIVE_TO_POSITIVE', '有心悸');
factStateStreamCheck($pilotLedger, $metadata . ',口干未记录。', false,
'metadata roles do not erase a later known positive', 'POSITIVE_TO_UNKNOWN', '未记录');
factStateStreamCheck($pilotLedger, '心悸未记录,' . $metadata . '。', false,
'metadata roles do not erase an earlier known negative', 'NEGATIVE_TO_UNKNOWN', '未记录');
factStateStreamCheck($pilotLedger, $metadata . ',舌质红绛。', false,
'metadata roles do not hide a new tongue feature', 'TONGUE_FEATURE_NOT_IN_CURRENT_SOURCE', '舌质红绛');
factStateStreamCheck($pilotLedger, $metadata . ',患者脉滑。', false,
'metadata roles do not hide a new pulse feature', 'PULSE_FEATURE_NOT_IN_CURRENT_SOURCE', '脉滑');
}
factStateStreamCheck($pilotLedger, '无附件,表示没有可复核图像或报告,患者无异常。', false,
'an actual broad patient absence is not the closed logical limitation', 'NEGATION_SCOPE_WIDENED', '无异常');
factStateStreamCheck($pilotLedger, $attachmentExplanation . '且患者有心悸。', false,
'an unbounded clinical suffix cannot fit the metadata explanation', 'NEGATIVE_TO_POSITIVE', '有心悸');
factStateStreamCheck($pilotLedger, '无附件(s999),表示没有可复核图像或报告,并非患者无异常。', false,
'metadata roles cannot make an invalid citation trusted', 'SOURCE_ANNOTATION_INVALID', '无附件');
$nullAliasCurrent = ['id' => 7301, 'diagnosis_date' => '2026-10-23', 'chief_complaint' => '乏力',
'present_illness' => null, 'tongue' => null, 'pulse' => null, 'current_medications' => null,
'allergy_history' => 0, 'allergy_history_text' => '未记录',
'family_history' => 0, 'family_history_text' => '未记录',
'surgery_history' => 0, 'surgery_history_text' => '未记录',
'trauma_history' => 0, 'trauma_history_text' => '未记录',
'pregnancy_history' => 0, 'pregnancy_history_text' => '未记录'];
$nullAliasSubmission = AiRequiredSource::submission(['diagnoses' => [$nullAliasCurrent]]);
$nullAliasLedger = TcmClinicalFactStateGuard::build($nullAliasCurrent, $nullAliasSubmission);
factStateExpect(!array_filter($nullAliasLedger['sources'], static fn (array $source): bool =>
in_array($source['field'], ['present_illness', 'tongue', 'pulse', 'current_medications'], true)),
'complete-source nested nulls do not fabricate source findings');
foreach (['allergy_history' => '过敏史', 'family_history' => '家族史', 'surgery_history' => '手术史',
'trauma_history' => '外伤史', 'pregnancy_history' => '孕产史'] as $field => $label) {
factStateExpect($nullAliasLedger['field_states'][$field]['state'] === 'unknown',
'legacy zero and its explicit unknown alias retain an unknown field state: ' . $field);
factStateStreamCheck($nullAliasLedger, $label . ':未记录。', true,
'complete-source explicit unknown alias remains usable: ' . $field);
factStateStreamCheck($nullAliasLedger, $label . ':无。', false,
'complete-source zero/null representation cannot authorize a patient denial: ' . $field,
'UNKNOWN_TO_NEGATIVE', '无');
}
factStateStreamCheck($nullAliasLedger, '舌象未记录,脉象未记录。', true,
'null current examinations remain unknown after complete-source projection');
factStateStreamCheck($nullAliasLedger, '脉滑。', false,
'a null current examination never supplies a new pulse feature', 'PULSE_FEATURE_NOT_IN_CURRENT_SOURCE', '脉滑');
$explicitAliasCurrent = array_replace($nullAliasCurrent, ['allergy_history_text' => '本次询问否认药物过敏史']);
$explicitAliasLedger = TcmClinicalFactStateGuard::build($explicitAliasCurrent,
AiRequiredSource::submission(['diagnoses' => [$explicitAliasCurrent]]));
factStateStreamCheck($explicitAliasLedger, '本次询问否认药物过敏史。', true,
'an explicit source denial remains valid beside a raw legacy zero');
factStateStreamCheck($explicitAliasLedger, '过敏史未记录。', false,
'an explicit text denial cannot be erased by the legacy representation', 'NEGATIVE_TO_UNKNOWN', '未记录');
$v27Wire = $pulseLocationParagraph . "\r\n\r\n" . $unknownIndexParagraph . "\r\n\r\n" . $attachmentRoleParagraph;
foreach ([1, 7, 19] as $byteSize) {
$v27Visible = '';
$v27Stream = new TcmClinicalReportStream($pilotLedger, static function (string $delta) use (&$v27Visible): bool {
$v27Visible .= $delta;
return true;
});
$v27Pushed = true;
foreach (str_split($v27Wire, $byteSize) as $chunk) {
if (!$v27Stream->push($chunk)) { $v27Pushed = false; break; }
}
factStateExpect($v27Pushed && $v27Stream->finish() && !$v27Stream->rejected(),
'v27 complete portable units survive UTF-8/metadata/CRLF boundaries at byte size ' . $byteSize);
factStateExpect($v27Visible === $v27Wire,
'v27 role normalization preserves every accepted original byte at byte size ' . $byteSize);
}
// v29: a centralized date has only a closed, same-paragraph negative-list role.
$datedNegativeList = "已记录阴性(2026-10-23):\n"
. "1.“无恶寒发热,”(s2)\n2.“无高热烦渴、汗出淋漓或意识异常”(s2)\n"
. "3.“无明显五心烦热、潮热盗汗,”(s2)\n4.“无畏寒肢冷”(s2)\n"
. "5.“无咳嗽咯痰、胸痛心悸、浮肿或腹泻”(s2)\n6.“无明显急性感染诱因”(s2)\n"
. "7.“无喘促及异常气味”(s3)\n8.“无已知冠心病、心衰或慢性肾病”(s4)\n"
. "9.“无已知药物过敏史”(s5)\n10.“未使用胰岛素或中药”(s6)";
$bloodCombination = '血分方面,面色少华、脉细可供血虚鉴别,但缺乏头晕、唇甲色淡等记录,现有证据不足以确认血虚;'
. '固定刺痛为未知,舌无紫暗瘀斑,故血瘀缺少阳性支持。';
factStateStreamCheck($pilotLedger, $datedNegativeList, true,
'a portable complete centralized-date list validates each original quote and source date');
factStateStreamCheck($pilotLedger, $datedNegativeList . "\n另据2026-10-22运动记录:“无胸痛或晕厥。”(原资料未提供source_id)", true,
'the list date stops before a separately dated historical observation');
foreach ([
str_replace('2026-10-23', '2026-10-22', $datedNegativeList),
str_replace('(s3)', '(s999)', $datedNegativeList),
str_replace('7.“无喘促及异常气味”(s3)', '7.“无喘促”(s3)', $datedNegativeList),
str_replace('7.“无喘促及异常气味”(s3)', '7.“无喘促及异常气味”', $datedNegativeList),
str_replace('已记录阴性(2026-10-23):', '已记录阴性(2026-10-23,患者无固定刺痛):', $datedNegativeList),
"已记录阴性(2026-10-23):\n\n1.“无恶寒发热,”(s2)",
] as $invalidList) {
factStateStreamCheck($pilotLedger, $invalidList, false,
'a date header cannot bypass source IDs, dates, complete quotes, closed grammar or paragraph boundaries');
}
factStateStreamCheck($pilotLedger, $datedNegativeList . "\n患者无固定刺痛。", false,
'a dated list cannot authorize a following current unknown-to-negative', 'UNKNOWN_TO_NEGATIVE', '无固定刺痛');
factStateStreamCheck($pilotLedger, $datedNegativeList . "\n心悸未记录。", false,
'a dated list cannot erase a known source negative', 'NEGATIVE_TO_UNKNOWN', '未记录');
factStateStreamCheck($pilotLedger, $datedNegativeList . "\n口干未记录。", false,
'a dated list cannot erase a known source positive', 'POSITIVE_TO_UNKNOWN', '未记录');
factStateStreamCheck($pilotLedger, $datedNegativeList . "\n固定刺痛未记录(同日,s2)。", false,
'a centralized heading never seeds same-day metadata outside its list', 'SOURCE_ANNOTATION_INVALID', '未记录');
$oldHeaderSource = array_values(array_filter($dated['sources'], static fn (array $row): bool => str_starts_with($row['path'], 'daily_tracking.exercise.')))[0];
$oldDateList = "已记录阴性(2026-10-22):\n1.“无胸痛或晕厥”(" . $oldHeaderSource['id'] . ')';
factStateStreamCheck($dated, $oldDateList, true,
'a valid dated historical list remains an explicitly dated historical quotation');
factStateStreamCheck($dated, str_replace('2026-10-22', '2026-10-23', $oldDateList), false,
'a current header date cannot promote an older source', 'SOURCE_ANNOTATION_INVALID', '无胸痛');
factStateStreamCheck($dated, $oldDateList . "\n本次无耳鸣。", false,
'a historical list date cannot leak to a later explicitly current assertion', 'UNKNOWN_TO_NEGATIVE', '无耳鸣');
factStateStreamCheck($pilotLedger, $bloodCombination, true,
'the complete portable blood paragraph combines two exact negative features from one current tongue source');
factStateStreamCheck($pilotLedger, '舌无紫暗瘀斑。', true,
'a negative tongue combination preserves 紫暗 and 瘀斑 without inventing broad 舌紫');
foreach ([
TcmClinicalFactStateGuard::build(['tongue' => '无明显紫暗、瘀斑。']),
TcmClinicalFactStateGuard::build(['tongue' => '无紫暗。', 'tongue_coating' => '无瘀斑。']),
TcmClinicalFactStateGuard::build(['tongue' => '舌紫暗,无瘀斑。']),
] as $unsupportedCombination) {
factStateStreamCheck($unsupportedCombination, '舌无紫暗瘀斑。', false,
'the combination requires both complete unqualified negatives from the same current tongue source',
'TONGUE_FEATURE_NOT_IN_CURRENT_SOURCE', '舌无紫暗瘀斑');
}
factStateStreamCheck($pilotLedger, '舌无紫暗瘀斑,患者有固定刺痛。', false,
'a supported tongue combination cannot authorize a new positive tail', 'UNKNOWN_TO_POSITIVE', '有固定刺痛');
factStateStreamCheck($pilotLedger, '舌无紫暗瘀斑,舌质红绛。', false,
'a supported tongue combination cannot hide a new tongue feature', 'TONGUE_FEATURE_NOT_IN_CURRENT_SOURCE', '舌质红绛');
factStateStreamCheck($pilotLedger, '舌无紫暗瘀斑,脉滑。', false,
'a supported tongue combination cannot hide a new pulse feature', 'PULSE_FEATURE_NOT_IN_CURRENT_SOURCE', '脉滑');
factStateStreamCheck($pilotLedger, '无急性感染诱因。', false,
'the actual Qwen deletion of 明显 remains rejected', 'NEGATION_SCOPE_WIDENED', '无急性感染诱因');
$v29Wire = str_replace("\n", "\r\n", $datedNegativeList) . "\r\n\r\n" . $bloodCombination;
$v29Visible = '';
$v29Stream = new TcmClinicalReportStream($pilotLedger, static function (string $delta) use (&$v29Visible): bool {
$v29Visible .= $delta;
return true;
});
$v29Pushed = true;
foreach (str_split($v29Wire, 7) as $chunk) {
if (!$v29Stream->push($chunk)) { $v29Pushed = false; break; }
}
factStateExpect($v29Pushed && $v29Stream->finish() && !$v29Stream->rejected(),
'both portable v29 roles survive split UTF-8, source metadata and CRLF boundaries');
factStateExpect($v29Visible === $v29Wire,
'v29 validation-only normalization preserves all original accepted bytes');
// v30: the closed “未知项:” label is report structure, including before item 1.
$unknownItemSingle = '未知项:1.“未记录腰酸耳鸣或固定刺痛”(s2,2026-10-23);'
. '2.腹诊、体位性血压、氧饱和度、近期体重变化及完整饮食饮水量未采集或未记录;相关空栏保持未知。';
$unknownItemConcurrent = '未知项:1.“未记录腰酸耳鸣或固定刺痛”(s2,2026-10-23);'
. '2.婚姻、地区、腰围、血氧及体重变化等为空栏,保持未知;'
. '3.无舌图、图片备注、文件报告及视频附件,不能据此判断相应体征或检查结果。';
foreach ([$unknownItemSingle, $unknownItemConcurrent] as $paragraph) {
factStateStreamCheck($pilotLedger, $paragraph, true,
'the portable complete v29 unknown-items paragraph cannot turn item 1 into an eGFR data gap');
}
factStateStreamCheck($numberedLedger, '未知项:1.未记录耳鸣。', true,
'a closed unknown-items heading binds only its following item structure');
foreach (['未知项:1.eGFR 96mL/min/1.73m²未记录。', '未知项:2.未记录eGFR 96mL/min/1.73m²。',
'未知项:1.73m²未记录。'] as $erasedLab) {
factStateStreamCheck($numberedLedger, $erasedLab, false,
'Latin lab items and true decimal topics retain known-to-unknown checks', 'POSITIVE_TO_UNKNOWN', '未记录');
}
factStateStreamCheck($pilotLedger, '未知项:1.eGFR 96mL/min/1.73m²未记录(2026-10-23,s23)。', false,
'the actual dated s23 lab cannot be erased by an unknown-items heading', 'POSITIVE_TO_UNKNOWN', '未记录');
factStateStreamCheck($numberedLedger, '未知项:1.口干未记录。', false,
'an unknown-items heading cannot erase a recorded clinical positive', 'POSITIVE_TO_UNKNOWN', '未记录');
factStateStreamCheck($numberedLedger, '未知项:1.胸痛未记录。', false,
'an unknown-items heading cannot erase a recorded clinical negative', 'NEGATIVE_TO_UNKNOWN', '未记录');
foreach (['未知项:1.患者有耳鸣。', '未知项:1..患者有耳鸣。', '未知项1.患者有耳鸣。'] as $actualClinicalItem) {
factStateStreamCheck($numberedLedger, $actualClinicalItem, false,
'valid or malformed numeric/clinical list syntax never exempts an actual observation',
'UNKNOWN_TO_POSITIVE', '有耳鸣');
}
factStateStreamCheck($numberedLedger, '未知项:1.无耳鸣。', false,
'a closed unknown-items heading cannot authorize an actual negative', 'UNKNOWN_TO_NEGATIVE', '无耳鸣');
$v30Wire = $unknownItemSingle . "\r\n\r\n" . $unknownItemConcurrent;
$v30Visible = '';
$v30Stream = new TcmClinicalReportStream($pilotLedger, static function (string $delta) use (&$v30Visible): bool {
$v30Visible .= $delta;
return true;
});
$v30Pushed = true;
foreach (str_split($v30Wire, 7) as $chunk) {
if (!$v30Stream->push($chunk)) { $v30Pushed = false; break; }
}
factStateExpect($v30Pushed && $v30Stream->finish() && !$v30Stream->rejected(),
'both complete v30 portable units stream through split UTF-8 and CRLF boundaries');
factStateExpect($v30Visible === $v30Wire,
'v30 heading normalization preserves every accepted original input byte');
// v31: three proven discourse roles, with observed assertions kept outside them.
$evidenceNounParagraph = '气方面以气虚为主:乏力、动则气短、自汗、语声低微与无力脉相互印证,并见活动后加重,提示推动、固摄功能不足。'
. '血方面仅有面色少华、脉细可作血虚鉴别线索,但未记录头晕、唇甲色泽等,现有证据不足以确认血虚;亦无本次阳性资料支持血瘀。';
$upperPulseParagraph = "望诊:神疲,面色少华。身高170cm,体重65kg,BMI 22.5kg/m²。体温36.5℃,血压124/76mmHg。 \n"
. "闻诊:语声低微。 \n问诊:乏力、动则气短、自汗、口干3个月;轻微活动后气短,白天自汗且活动后加重,口干欲饮、少量频饮。"
. "食欲一般,大便稍干、每日1次;小便量较以往略多,夜尿1次;睡眠一般。2026-10-22步行20分钟后乏力、汗出较明显。 \n"
. "舌象:本次面诊记录:舌质淡红,舌面少津,苔少;无黄腻苔、紫暗或瘀斑。(s9) \n"
. "脉象:本次医师切诊:脉细弱,按之无力;不浮、不滑、不紧。(s10) \n切诊:脉诊结果见上列脉象;其他切诊体征未记录。";
$reviewQuestionParagraph = '适用前须核实:复核自汗与气短是否持续呈活动后加重,有无新发感染、胸闷胸痛、心悸、晕厥或气促加剧;'
. '补采完整饮水量、尿量、体重变化、腹诊及腰膝耳鸣等资料。若出现急性发热、明显实热、痰湿壅盛或其他实邪表现,应重新辨证,不能机械沿用本方向。';
foreach ([$evidenceNounParagraph, $upperPulseParagraph, $reviewQuestionParagraph] as $paragraph) {
factStateStreamCheck($pilotLedger, $paragraph, true,
'a complete portable v30 rejected unit retains its evidence, reference or review-question role');
}
factStateStreamCheck($pilotLedger, '亦无本次阳性资料支持血瘀。', true,
'a closed evidence noun is not an observed patient negative');
foreach (['亦无本次阳性资料支持血瘀,患者无固定刺痛。', '患者无固定刺痛,亦无本次阳性资料支持血瘀。'] as $mixedEvidence) {
factStateStreamCheck($pilotLedger, $mixedEvidence, false,
'an evidence noun cannot hide an actual negative before or after it', 'UNKNOWN_TO_NEGATIVE', '无固定刺痛');
}
factStateStreamCheck($pilotLedger, '亦无本次阳性资料支持血瘀且患者有心悸。', false,
'an evidence noun has no wildcard slot for an actual positive tail', 'NEGATIVE_TO_POSITIVE', '有心悸');
foreach (['切诊:脉诊结果见上列脉象。', '脉象结果见上述独立脉象行。', '脉诊结果见下列脉象。'] as $locationReference) {
factStateStreamCheck($pilotLedger, $locationReference, true,
'upper and lower location references contain no observed pulse value');
}
factStateStreamCheck($pilotLedger, '切诊:脉诊结果见上列脉象,脉结。', false,
'a result reference does not exempt an actual pulse 结', 'PULSE_FEATURE_NOT_IN_CURRENT_SOURCE', '脉结');
factStateStreamCheck($pilotLedger, '脉诊结果见上列脉象且脉滑。', false,
'an unbounded assertion tail does not fit the pulse-reference role', 'PULSE_FEATURE_NOT_IN_CURRENT_SOURCE', '脉滑');
factStateStreamCheck($pilotLedger, '患者无固定刺痛,脉诊结果见上列脉象。', false,
'an upper reference does not hide an earlier actual patient negative', 'UNKNOWN_TO_NEGATIVE', '无固定刺痛');
factStateStreamCheck($pilotLedger, '脉诊结果见上列脉象,口干未记录。', false,
'a result reference cannot erase a recorded positive', 'POSITIVE_TO_UNKNOWN', '未记录');
$reviewQuestion = '适用前须核实:复核自汗与气短是否持续呈活动后加重,有无新发感染、胸闷胸痛、心悸、晕厥或气促加剧';
factStateStreamCheck($pilotLedger, '适用前须核实:有无心悸或固定刺痛。', true,
'an anchored yes/no inquiry does not assert either source polarity');
foreach ([$reviewQuestion . ',患者无固定刺痛。', '患者无固定刺痛,' . $reviewQuestion . '。',
$reviewQuestion . '且患者无固定刺痛。', '适用前须核实:复核无固定刺痛。'] as $mixedQuestion) {
factStateStreamCheck($pilotLedger, $mixedQuestion, false,
'a review request cannot hide an actual negative prefix, suffix or non-question statement',
'UNKNOWN_TO_NEGATIVE', '无固定刺痛');
}
foreach ([$reviewQuestion . ',患者有心悸。', $reviewQuestion . '且患者有心悸。',
'适用前须核实:患者有心悸,复核自汗是否持续。'] as $mixedQuestion) {
factStateStreamCheck($pilotLedger, $mixedQuestion, false,
'a request frame cannot lend its generic collection exemption to an actual positive',
'NEGATIVE_TO_POSITIVE', '有心悸');
}
factStateStreamCheck($pilotLedger, $reviewQuestion . ',心悸未记录。', false,
'a review question cannot erase a recorded negative tail', 'NEGATIVE_TO_UNKNOWN', '未记录');
factStateStreamCheck($pilotLedger, '无固定刺痛。', false,
'the actual Qwen unknown-to-negative error remains rejected', 'UNKNOWN_TO_NEGATIVE', '无固定刺痛');
factStateStreamCheck($pilotLedger, '无五心烦热。', false,
'the actual Qwen degree-qualifier deletion remains rejected', 'NEGATION_SCOPE_WIDENED', '无五心烦热');
$v31Wire = $evidenceNounParagraph . "\r\n\r\n" . str_replace("\n", "\r\n", $upperPulseParagraph)
. "\r\n\r\n" . $reviewQuestionParagraph;
$v31Visible = '';
$v31Stream = new TcmClinicalReportStream($pilotLedger, static function (string $delta) use (&$v31Visible): bool {
$v31Visible .= $delta;
return true;
});
$v31Pushed = true;
foreach (str_split($v31Wire, 7) as $chunk) {
if (!$v31Stream->push($chunk)) { $v31Pushed = false; break; }
}
factStateExpect($v31Pushed && $v31Stream->finish() && !$v31Stream->rejected(),
'all three portable v31 roles survive split UTF-8, source annotation and CRLF boundaries');
factStateExpect($v31Visible === $v31Wire,
'v31 discourse-role handling preserves every accepted original byte');
// v32: grammatical data objects, bounded quantity predicates and review actions.
// These complete units are synthetic literals, independent of retained artifacts.
$missingRecordParagraph = '血方面仅有面色少华、脉细可供鉴别,尚不足以确认血虚;缺乏固定刺痛资料,亦不能确认血瘀。待补依据:见第一节未知第1项(s2)。';
$quantityParagraph = '气分以气虚为主要判断:乏力、活动后气短和汗出加重,与语声低微、脉弱无力相互印证,并见卫外不固、腠理失摄倾向。'
. '津液方面为津液偏少:口干欲饮而每次不多、大便稍干、舌面少津苔少;自汗及小便较前略多可能增加津液耗失,但不能仅据尿量变化确立肾阴虚或消渴下消。';
$safetyReviewParagraph = '禁忌与安全复核:虽第一节阴性第9项(s5)记录本次询问情况,新用药仍须逐味复核过敏、既往不良反应、肝肾功能、合并药物及制剂含糖辅料。'
. '2026-10-20 ALT 22U/L、肌酐78μmol/L、eGFR 96mL/min/1.73m²、尿ACR 12mg/g可作当前安全基线,不代表后续无需监测。'
. '若复核发现邪实未解、痰湿壅盛、中满纳差等不适用条件,不宜机械采用本方向。';
foreach ([$missingRecordParagraph, $quantityParagraph, $safetyReviewParagraph] as $paragraph) {
factStateStreamCheck($pilotLedger, $paragraph, true,
'a complete v32 portable unit keeps its missing-record, quantity or review-action role');
}
$gapLedger = TcmClinicalFactStateGuard::build([
'diagnosis_date' => '2026-10-23', 'present_illness' => '口干;无心悸;未记录耳鸣、固定刺痛或胰岛素。',
'tongue' => '舌质淡红,舌面少津,苔少。', 'pulse' => '脉细弱。',
]);
foreach (['缺乏固定刺痛资料。', '缺少耳鸣记录。', '无耳鸣资料。', '未见耳鸣记录。',
'缺少固定刺痛及耳鸣的专门文字描述。', '缺少舌裂纹资料。'] as $gapText) {
factStateStreamCheck($gapLedger, $gapText, true,
'a closed missing-data object retains unknown rather than patient-positive or patient-negative state');
}
foreach (['缺乏口干资料。', '缺少口干记录。', '没有口干专门描述。', '缺少口干及耳鸣记录。'] as $erasedPositive) {
factStateStreamCheck($gapLedger, $erasedPositive, false,
'missing-record metadata cannot erase a known same-time positive', 'POSITIVE_TO_UNKNOWN', '口干');
}
foreach (['缺少心悸记录。', '无心悸资料。'] as $erasedNegative) {
factStateStreamCheck($gapLedger, $erasedNegative, false,
'missing-record metadata cannot erase a known same-time negative', 'NEGATIVE_TO_UNKNOWN', '心悸');
}
factStateStreamCheck($gapLedger, '舌象:缺少资料。', false,
'a field label supplies the missing record subject', 'RECORDED_TO_UNKNOWN', '缺少资料');
factStateStreamCheck($gapLedger, "脉象:\n\n缺乏资料。", false,
'a carried recorded field cannot become an empty data gap across paragraphs', 'RECORDED_TO_UNKNOWN', '缺乏资料');
factStateStreamCheck($examUnknown, '舌象:缺少资料。', true,
'the same labeled data gap is faithful when the examination really is unknown');
factStateStreamCheck($examUnknown, '舌象:缺少资料,脉象未记录。', true,
'a comma-delimited field gap cannot turn the next unknown examination into an observed value');
factStateStreamCheck($examUnknown, '舌象:缺少资料,脉滑。', false,
'a comma-delimited field gap cannot authorize an actual pulse value', 'PULSE_FEATURE_NOT_IN_CURRENT_SOURCE', '脉滑');
factStateStreamCheck($gapLedger, '舌象:缺少资料,脉细弱。', false,
'a later grounded pulse cannot hide erasure of the recorded tongue field', 'RECORDED_TO_UNKNOWN', '缺少资料');
factStateStreamCheck($gapLedger, '缺少口干记录(2026-10-23,s1)。', false,
'valid source metadata cannot authorize erasing its actual positive', 'POSITIVE_TO_UNKNOWN', '口干');
factStateStreamCheck($gapLedger, '缺少耳鸣记录(2026-10-22,s1)。', false,
'a data-gap role cannot make an inconsistent source date valid', 'SOURCE_ANNOTATION_INVALID', '缺少耳鸣');
factStateStreamCheck($gapLedger, '缺乏胰岛素。', false,
'a deficiency without a record head noun remains an actual clinical proposition', 'UNKNOWN_TO_POSITIVE', '缺乏胰岛素');
$sourceGapLedger = TcmClinicalFactStateGuard::build(['present_illness' => '缺少耳鸣记录;缺乏胰岛素。']);
factStateStreamCheck($sourceGapLedger, '无耳鸣。', false,
'source-side data-gap grammar also preserves unknown', 'UNKNOWN_TO_NEGATIVE', '无耳鸣');
factStateStreamCheck($sourceGapLedger, '胰岛素未记录。', false,
'source-side deficiency is not indexed as missing-record metadata', 'POSITIVE_TO_UNKNOWN', '未记录');
foreach (['缺少耳鸣记录,患者有耳鸣。', '缺少耳鸣资料且患者有耳鸣。'] as $mixedGap) {
factStateStreamCheck($gapLedger, $mixedGap, false,
'a record object cannot hide an adjacent actual positive', 'UNKNOWN_TO_POSITIVE', '有耳鸣');
}
factStateStreamCheck($gapLedger, '患者无固定刺痛,缺少耳鸣记录。', false,
'a record object cannot hide an earlier actual negative', 'UNKNOWN_TO_NEGATIVE', '无固定刺痛');
factStateStreamCheck($gapLedger, '缺少患者无耳鸣记录。', false,
'an observed negative cannot be embedded inside a neutral data object', 'UNKNOWN_TO_NEGATIVE', '无耳鸣');
factStateStreamCheck($gapLedger, '缺少耳鸣记录,患者有心悸。', false,
'a missing-data clause cannot reverse a different recorded negative', 'NEGATIVE_TO_POSITIVE', '有心悸');
factStateStreamCheck($pilotLedger, '口干欲饮而每次不多、大便稍干、舌面少津苔少。', true,
'a closed quantity predicate cannot carry its negation into later positive list items');
factStateStreamCheck($pilotLedger, '饮水量不多,舌面少津,苔少;脉细弱。', true,
'an explicit measure subject has the same quantity boundary as an iteration subject');
factStateStreamCheck($pilotLedger, '每次不多、大便稍干、舌质红绛。', false,
'quantity scoping does not exempt a later invented tongue feature', 'TONGUE_FEATURE_NOT_IN_CURRENT_SOURCE', '舌质红绛');
factStateStreamCheck($pilotLedger, '每次不多、脉滑。', false,
'quantity scoping does not exempt a later invented pulse feature', 'PULSE_FEATURE_NOT_IN_CURRENT_SOURCE', '脉滑');
factStateStreamCheck($pilotLedger, '每餐不少,患者有心悸。', false,
'a bounded quantity does not exempt a later explicit clinical positive', 'NEGATIVE_TO_POSITIVE', '有心悸');
factStateStreamCheck($pilotLedger, '每次不多,患者无固定刺痛。', false,
'a bounded quantity does not exempt a later explicit unknown-to-negative error', 'UNKNOWN_TO_NEGATIVE', '无固定刺痛');
factStateStreamCheck($pilotLedger, '每次不多、无五心烦热。', false,
'a bounded quantity does not permit deleting a source degree qualifier', 'NEGATION_SCOPE_WIDENED', '无五心烦热');
factStateStreamCheck($pilotLedger, '每次不多,口干未记录。', false,
'a bounded quantity does not authorize a later known-to-unknown error', 'POSITIVE_TO_UNKNOWN', '未记录');
factStateStreamCheck($gapLedger, '耳鸣每次不多。', false,
'a quantity predicate still leaves its own clinical subject visible', 'UNKNOWN_TO_POSITIVE', '耳鸣');
factStateStreamCheck($pilotLedger, '不多、大便稍干、舌面少津苔少。', false,
'an unbound negative has no invented quantity subject', 'POSITIVE_TO_NEGATIVE', '大便');
factStateStreamCheck($pilotLedger, '每次不多舌质红绛。', false,
'an unclosed quantity complement cannot absorb a new examination value', 'TONGUE_FEATURE_NOT_IN_CURRENT_SOURCE', '红绛');
$quantityUnknownLedger = TcmClinicalFactStateGuard::build(['present_illness' => '饮水量未记录。']);
factStateStreamCheck($quantityUnknownLedger, '饮水量不多。', false,
'a measured quantity cannot be invented from an unknown record', 'UNKNOWN_TO_POSITIVE', '饮水量');
$reviewAction = '新用药仍须逐味复核过敏、既往不良反应、肝肾功能';
factStateStreamCheck($pilotLedger, $reviewAction . '。', true,
'a modal collection action governs a neutral noun list without asserting its contents');
foreach (['患者无固定刺痛,' . $reviewAction . '。', $reviewAction . ',患者无固定刺痛。',
'新用药仍须逐味复核无固定刺痛。'] as $mixedReviewAction) {
factStateStreamCheck($pilotLedger, $mixedReviewAction, false,
'a review action cannot hide an actual negative prefix, suffix or object statement',
'UNKNOWN_TO_NEGATIVE', '无固定刺痛');
}
foreach (['患者有心悸,' . $reviewAction . '。', $reviewAction . ',患者有心悸。',
$reviewAction . '且患者有心悸。', $reviewAction . '患者有心悸。',
'新用药须复核患者有心悸。', '须复核患者有心悸。'] as $mixedReviewAction) {
factStateStreamCheck($pilotLedger, $mixedReviewAction, false,
'a review action cannot hide an actual positive prefix, suffix or conjunction',
'NEGATIVE_TO_POSITIVE', '有心悸');
}
factStateStreamCheck($pilotLedger, $reviewAction . ',心悸未记录。', false,
'a review action cannot erase a known negative', 'NEGATIVE_TO_UNKNOWN', '未记录');
factStateStreamCheck($pilotLedger, $reviewAction . '且舌质红绛。', false,
'a request object list stops before an assertion conjunction', 'TONGUE_FEATURE_NOT_IN_CURRENT_SOURCE', '红绛');
factStateStreamCheck($gapLedger, $reviewAction . '且患者有耳鸣。', false,
'a historical noun inside a request cannot redate a new current patient assertion', 'UNKNOWN_TO_POSITIVE', '有耳鸣');
factStateStreamCheck($pilotLedger, '患者无不良反应。', false,
'an adverse-reaction noun does not exempt an actual unsupported patient absence', 'NEGATIVE_NOT_IN_SOURCE', '无不良反应');
factStateStreamCheck($pilotLedger, '患者无药物过敏史。', false,
'the original known-allergy qualifier still cannot be deleted', 'NEGATION_SCOPE_WIDENED', '无药物过敏史');
$v32Wire = $missingRecordParagraph . "\r\n\r\n" . $quantityParagraph . "\r\n\r\n" . $safetyReviewParagraph;
$v32Visible = '';
$v32Stream = new TcmClinicalReportStream($pilotLedger, static function (string $delta) use (&$v32Visible): bool {
$v32Visible .= $delta;
return true;
});
$v32Pushed = true;
foreach (str_split($v32Wire, 7) as $chunk) {
if (!$v32Stream->push($chunk)) { $v32Pushed = false; break; }
}
factStateExpect($v32Pushed && $v32Stream->finish() && !$v32Stream->rejected(),
'all complete v32 roles survive split UTF-8, metadata and CRLF boundaries');
factStateExpect($v32Visible === $v32Wire,
'v32 scope analysis preserves every accepted original input byte');
// v33: a postfix gap modifies its closed record object, never a patient finding.
// This portable literal includes the full rejected paragraph, without artifacts.
$postfixRecordParagraph = '气方面,以气虚为主要病机:乏力、动则气短、自汗、语声低微及脉按之无力构成连续证据链,并提示气的推动、固摄功能不足。'
. '血方面,面色少华、脉细可作为血分不足的鉴别线索,但缺少头晕、唇甲色淡等本例阳性记录,故血虚尚不能确证;'
. '固定刺痛资料缺失,待补依据:见第一节未知第1项(s2)。';
factStateStreamCheck($pilotLedger, $postfixRecordParagraph, true,
'the complete v32 OpenAI paragraph keeps its postfix missing-record role');
foreach (['固定刺痛资料缺失。', '耳鸣记录缺失。', '耳鸣信息缺失。', '耳鸣描述缺失。',
'耳鸣的专门文字描述缺失。', '耳鸣资料仍缺失。', '本次耳鸣记录尚缺失。',
'耳鸣、固定刺痛资料缺失。', '耳鸣及固定刺痛资料缺失。', '舌裂纹资料缺失。',
'耳鸣资料缺失,口干。', '耳鸣资料缺失,舌质淡红;脉细弱。'] as $postfixGap) {
factStateStreamCheck($gapLedger, $postfixGap, true,
'a closed postfix record gap retains unknown and preserves adjoining grounded findings');
}
foreach (['口干资料缺失。', '口干记录缺失。', '口干信息缺失。', '口干描述缺失。',
'口干及耳鸣记录缺失。', '口干的专门文字描述缺失。'] as $postfixErasure) {
factStateStreamCheck($gapLedger, $postfixErasure, false,
'a postfix record gap cannot erase a known positive', 'POSITIVE_TO_UNKNOWN', '口干');
}
foreach (['心悸记录缺失。', '心悸资料缺失。', '耳鸣及心悸信息缺失。'] as $postfixErasure) {
factStateStreamCheck($gapLedger, $postfixErasure, false,
'a postfix record gap cannot erase a known negative', 'NEGATIVE_TO_UNKNOWN', '心悸');
}
factStateStreamCheck($gapLedger, '舌象:资料缺失。', false,
'a labeled postfix record gap still conflicts with a recorded examination', 'RECORDED_TO_UNKNOWN', '资料缺失');
factStateStreamCheck($gapLedger, "脉象:\n\n资料缺失。", false,
'a carried recorded examination cannot become a postfix data gap', 'RECORDED_TO_UNKNOWN', '资料缺失');
factStateStreamCheck($examUnknown, '舌象:资料缺失。', true,
'a labeled postfix record gap is faithful for an unknown examination');
factStateStreamCheck($examUnknown, '舌象:资料缺失,脉象未记录。', true,
'a postfix record gap cannot supply a value for the next unknown examination');
factStateStreamCheck($examUnknown, '舌象:资料缺失,脉滑。', false,
'a postfix record gap cannot authorize a new examination value', 'PULSE_FEATURE_NOT_IN_CURRENT_SOURCE', '脉滑');
foreach (['耳鸣资料缺失,患者有耳鸣。', '患者有耳鸣,耳鸣资料缺失。',
'耳鸣资料缺失且患者有耳鸣。', '耳鸣资料缺失而患者有耳鸣。',
'患者有耳鸣资料缺失。', '耳鸣资料缺失、固定刺痛。', '耳鸣资料缺失及固定刺痛。',
'耳鸣资料缺失患者有耳鸣。'] as $mixedPostfixGap) {
factStateStreamCheck($gapLedger, $mixedPostfixGap, false,
'a postfix record object cannot absorb an actual positive clause or list tail', 'UNKNOWN_TO_POSITIVE', '耳鸣');
}
foreach (['耳鸣资料缺失,患者无固定刺痛。', '患者无固定刺痛,耳鸣资料缺失。',
'耳鸣资料缺失且患者无固定刺痛。', '无固定刺痛资料缺失。'] as $mixedPostfixGap) {
factStateStreamCheck($gapLedger, $mixedPostfixGap, false,
'a postfix record object cannot absorb an actual clinical negative', 'UNKNOWN_TO_NEGATIVE', '无固定刺痛');
}
factStateStreamCheck($gapLedger, '耳鸣资料缺失,患者有心悸。', false,
'a postfix data gap cannot reverse a different recorded negative', 'NEGATIVE_TO_POSITIVE', '有心悸');
factStateStreamCheck($pilotLedger, '固定刺痛资料缺失,口干未记录。', false,
'a postfix gap cannot erase an adjoining known positive', 'POSITIVE_TO_UNKNOWN', '未记录');
factStateStreamCheck($pilotLedger, '固定刺痛资料缺失,心悸未记录。', false,
'a postfix gap cannot erase an adjoining known negative', 'NEGATIVE_TO_UNKNOWN', '未记录');
factStateStreamCheck($pilotLedger, '固定刺痛资料缺失,无五心烦热。', false,
'a postfix gap cannot authorize deleting a degree qualifier', 'NEGATION_SCOPE_WIDENED', '无五心烦热');
factStateStreamCheck($pilotLedger, '固定刺痛资料缺失,舌质红绛。', false,
'a postfix gap cannot authorize an unsupported tongue feature', 'TONGUE_FEATURE_NOT_IN_CURRENT_SOURCE', '红绛');
factStateStreamCheck($pilotLedger, '固定刺痛资料缺失且脉滑。', false,
'a postfix gap cannot absorb a joined unsupported pulse feature', 'PULSE_FEATURE_NOT_IN_CURRENT_SOURCE', '脉滑');
factStateStreamCheck($gapLedger, '胰岛素缺失。', false,
'a deficiency without a record head noun stays a clinical proposition', 'UNKNOWN_TO_POSITIVE', '胰岛素');
factStateStreamCheck($gapLedger, '固定刺痛资料缺失(2026-10-22,s1)。', false,
'a postfix data gap cannot validate inconsistent source metadata', 'SOURCE_ANNOTATION_INVALID', '资料缺失');
$postfixSourceLedger = TcmClinicalFactStateGuard::build(['present_illness' => '耳鸣资料缺失;胰岛素缺失。']);
factStateStreamCheck($postfixSourceLedger, '无耳鸣。', false,
'source-side postfix record grammar also retains unknown', 'UNKNOWN_TO_NEGATIVE', '无耳鸣');
factStateStreamCheck($postfixSourceLedger, '患者有耳鸣。', false,
'source-side postfix record grammar cannot become a positive', 'UNKNOWN_TO_POSITIVE', '有耳鸣');
factStateStreamCheck($postfixSourceLedger, '胰岛素未记录。', false,
'source-side clinical deficiency is not a postfix record gap', 'POSITIVE_TO_UNKNOWN', '未记录');
$v33Wire = str_replace('。', "。\r\n", $postfixRecordParagraph) . "\r\n";
$v33Visible = '';
$v33Stream = new TcmClinicalReportStream($pilotLedger, static function (string $delta) use (&$v33Visible): bool {
$v33Visible .= $delta;
return true;
});
$v33Pushed = true;
foreach (str_split($v33Wire, 7) as $chunk) {
if (!$v33Stream->push($chunk)) { $v33Pushed = false; break; }
}
factStateExpect($v33Pushed && $v33Stream->finish() && !$v33Stream->rejected(),
'the portable v33 paragraph survives split UTF-8, source annotations and CRLF boundaries');
factStateExpect($v33Visible === $v33Wire,
'v33 role analysis preserves every accepted original byte');
// The exclusion predicate is inside a modal limit, not a patient denial.
$exclusionLimitParagraph = '表里:症状持续3个月、渐进出现,以乏力、动则气短、自汗、口干及二便改变为主,现有资料更支持里证倾向。'
. '限制依据:见第一节阴性第①、⑥项(s2),只能降低本次急性表证可能,不能仅凭否认彻底排除所有表证。';
factStateStreamCheck($pilotLedger, $exclusionLimitParagraph, true,
'the complete frozen B OpenAI paragraph limits exclusion rather than denying a patient finding');
$exclusionLedger = TcmClinicalFactStateGuard::build([
'present_illness' => '口干;无心悸;未记录表证、耳鸣或固定刺痛。',
'tongue' => '舌质淡红。', 'pulse' => '脉细弱。',
]);
$exclusionLimit = '不能仅凭否认彻底排除所有表证';
foreach ([$exclusionLimit . '。', '本次依据有限,' . $exclusionLimit . '。',
'不能排除耳鸣。', '不能彻底排除表证。', '不能排除固定刺痛或耳鸣。',
'不得单凭现有资料完全排除耳鸣。', '不应仅依据上述证据排除表证。',
'不得单凭本次资料完全排除耳鸣。', '不能仅凭当前阴性记录排除表证。',
'不能仅据阴性记录直接排除所有表证。', '不能只凭否认排除全部表证。',
'不据此宣称表证已被排除。', '但不据此宣称表证已被排除。', '不代表表证已被排除。',
'不能据此宣称表证已被排除。', '不说明耳鸣已经被完全排除。',
$exclusionLimit . ',口干,脉细弱。'] as $modalLimit) {
factStateStreamCheck($exclusionLedger, $modalLimit, true,
'a closed modal exclusion limit contains a neutral source noun and neutral target object');
}
foreach ([$exclusionLimit . ',患者有耳鸣。', '患者有耳鸣,' . $exclusionLimit . '。',
$exclusionLimit . '且患者有耳鸣。', $exclusionLimit . '患者有耳鸣。',
'不能排除患者有耳鸣。'] as $mixedLimit) {
factStateStreamCheck($exclusionLedger, $mixedLimit, false,
'a modal exclusion limit cannot absorb an actual patient positive', 'UNKNOWN_TO_POSITIVE', '有耳鸣');
}
foreach ([$exclusionLimit . ',患者无固定刺痛。', '患者无固定刺痛,' . $exclusionLimit . '。',
$exclusionLimit . '且患者无固定刺痛。', $exclusionLimit . '患者无固定刺痛。',
'不能排除患者无固定刺痛。', '不能仅凭患者无固定刺痛彻底排除表证。',
'不能仅凭否认固定刺痛彻底排除表证。'] as $mixedLimit) {
factStateStreamCheck($exclusionLedger, $mixedLimit, false,
'a modal limit cannot turn an embedded or adjacent clinical denial into a neutral noun', 'UNKNOWN_TO_NEGATIVE', '固定刺痛');
}
foreach ([$exclusionLimit . ',患者有心悸。', $exclusionLimit . '且患者有心悸。'] as $mixedLimit) {
factStateStreamCheck($exclusionLedger, $mixedLimit, false,
'a modal exclusion limit cannot reverse a known negative', 'NEGATIVE_TO_POSITIVE', '有心悸');
}
foreach ([$exclusionLimit . ',口干未记录。', $exclusionLimit . '且口干未记录。'] as $mixedLimit) {
factStateStreamCheck($exclusionLedger, $mixedLimit, false,
'a modal exclusion limit cannot erase a known positive', 'POSITIVE_TO_UNKNOWN', '未记录');
}
factStateStreamCheck($exclusionLedger, $exclusionLimit . ',心悸未记录。', false,
'a modal exclusion limit cannot erase a known negative', 'NEGATIVE_TO_UNKNOWN', '未记录');
foreach (['本次彻底排除表证。', '本次已排除表证。', '表证已被排除。', '据此宣称表证已被排除。'] as $actualExclusion) {
factStateStreamCheck($exclusionLedger, $actualExclusion, false,
'an actual exclusion without modal scope receives no exemption', 'UNKNOWN_TO_POSITIVE', '表证');
}
$exclusionScopeMethod = new ReflectionMethod(TcmClinicalFactStateGuard::class, 'exclusionLimitProse');
$exclusionScopeMethod->setAccessible(true);
foreach (['排除表证', '本次彻底排除表证', '仅凭阴性记录彻底排除表证', '患者无固定刺痛',
'表证已被排除', '据此宣称表证已被排除', '不能仅凭否认固定刺痛彻底排除表证', '舌质淡红而非明确红绛'] as $actualPredicate) {
factStateExpect($exclusionScopeMethod->invoke(null, $actualPredicate) === $actualPredicate,
'exclusion normalization preserves every byte of an actual or unbounded predicate');
}
factStateStreamCheck($exclusionLedger, '本次否认耳鸣。', false,
'an actual patient denial without modal scope remains a clinical negative', 'UNKNOWN_TO_NEGATIVE', '否认耳鸣');
factStateStreamCheck($pilotLedger, $exclusionLimit . ',无五心烦热。', false,
'a modal limit cannot authorize degree-qualifier loss', 'NEGATION_SCOPE_WIDENED', '无五心烦热');
factStateStreamCheck($pilotLedger, $exclusionLimit . '且脉滑。', false,
'a joined actual pulse value cannot borrow modal exclusion scope', 'PULSE_FEATURE_NOT_IN_CURRENT_SOURCE', '脉滑');
factStateStreamCheck($pilotLedger, $exclusionLimit . ',舌质红绛。', false,
'an actual tongue value cannot borrow modal exclusion scope', 'TONGUE_FEATURE_NOT_IN_CURRENT_SOURCE', '红绛');
factStateStreamCheck($pilotLedger, $exclusionLimit . ',舌质淡红而非明确红绛。', false,
'the frozen B tongue-color negation remains unsupported after modal scoping', 'TONGUE_FEATURE_NOT_IN_CURRENT_SOURCE', '非明确红绛');
factStateStreamCheck($pilotLedger, $exclusionLimit . '且舌质淡红而非明确红绛。', false,
'a joined tongue-color negation remains unsupported after modal scoping', 'TONGUE_FEATURE_NOT_IN_CURRENT_SOURCE', '非明确红绛');
foreach (['不能排除脉滑。', $exclusionLimit . '脉滑。'] as $phenotypeLimit) {
factStateStreamCheck($pilotLedger, $phenotypeLimit, false,
'modal normalization leaves pulse phenotypes under the existing examination policy', 'PULSE_FEATURE_NOT_IN_CURRENT_SOURCE', '脉滑');
}
foreach (['不能排除舌质红绛。', $exclusionLimit . '舌质红绛。'] as $phenotypeLimit) {
factStateStreamCheck($pilotLedger, $phenotypeLimit, false,
'modal normalization leaves tongue phenotypes under the existing examination policy', 'TONGUE_FEATURE_NOT_IN_CURRENT_SOURCE', '红绛');
}
factStateStreamCheck($pilotLedger, $exclusionLimit . '(s999)。', false,
'modal scope cannot validate an invalid source annotation', 'SOURCE_ANNOTATION_INVALID', '排除');
foreach (['不据此宣称相关证候已被排除', '不代表相关证候已被排除'] as $passiveLimit) {
factStateStreamCheck($pilotLedger, $passiveLimit . '。', true,
'a denial of an exclusion conclusion stays inside its modal scope');
factStateStreamCheck($pilotLedger, $passiveLimit . '且患者无固定刺痛。', false,
'a passive exclusion limit cannot absorb a joined patient absence', 'UNKNOWN_TO_NEGATIVE', '无固定刺痛');
factStateStreamCheck($pilotLedger, $passiveLimit . ',患者有心悸。', false,
'a passive exclusion limit cannot reverse a known patient negative', 'NEGATIVE_TO_POSITIVE', '有心悸');
factStateStreamCheck($pilotLedger, $passiveLimit . '且口干未记录。', false,
'a passive exclusion limit cannot erase a known finding', 'POSITIVE_TO_UNKNOWN', '未记录');
factStateStreamCheck($pilotLedger, $passiveLimit . '且舌质淡红而非明确红绛。', false,
'a passive exclusion limit cannot reinterpret a tongue phenotype', 'TONGUE_FEATURE_NOT_IN_CURRENT_SOURCE', '非明确红绛');
}
$attachmentRoleParagraph = '反证与限制:限制依据见第一节已记录阴性第2、3、4项(s2);这些资料降低显著实热、虚热及阳虚寒象对本方向的支持,但不据此宣称相关证候已被排除。'
. '限制依据另见第一节已记录阴性第5项(s2)和第7项(s3);仅说明当前缺乏相应心肺急症伴随表现,不能替代心肺检查。'
. '舌脉虽有本次医师记录,但无附件可供二次图像核验,仍应由复核医师结合现场所见确认。';
$denialReferenceParagraph = '反证与局限:限制依据见第一节已记录阴性第2、3、4项(s2),这仅提示典型热盛、虚热或阳虚寒象的支持不足,不代表相关证候已被排除;'
. '见第5、7项(s2、s3),仅降低明显心肺伴随症状对当前判断的干扰,不能代替心肺评估。既往疾病否认见第8项(s4),亦不能替代本次检查。';
foreach ([$attachmentRoleParagraph, $denialReferenceParagraph] as $metadataParagraph) {
factStateStreamCheck($pilotLedger, $metadataParagraph, true,
'a complete retained A paragraph preserves bounded availability or source-verified history reference roles');
}
$attachmentRole = '无附件可供二次图像核验';
foreach ([$attachmentRole . '。', '未提供舌照可供二次复核。', '无图像可供图片核验。',
'舌脉有本次医师记录,但' . $attachmentRole . ',仍应复核。'] as $availability) {
factStateStreamCheck($pilotLedger, $availability, true,
'a closed attachment availability object is metadata rather than a clinical absence');
}
foreach ([$attachmentRole . ',患者无固定刺痛。', '患者无固定刺痛,' . $attachmentRole . '。',
$attachmentRole . '且患者无固定刺痛。', $attachmentRole . '患者无固定刺痛。',
$attachmentRole . '、固定刺痛。'] as $mixedAvailability) {
factStateStreamCheck($pilotLedger, $mixedAvailability, false,
'attachment availability cannot absorb actual clinical absences or list items', 'UNKNOWN_TO_NEGATIVE', '固定刺痛');
}
factStateStreamCheck($pilotLedger, $attachmentRole . '且患者有心悸。', false,
'attachment availability cannot reverse a known negative', 'NEGATIVE_TO_POSITIVE', '有心悸');
factStateStreamCheck($pilotLedger, $attachmentRole . '且口干未记录。', false,
'attachment availability cannot erase a known positive', 'POSITIVE_TO_UNKNOWN', '未记录');
factStateStreamCheck($pilotLedger, $attachmentRole . '且脉滑。', false,
'attachment availability cannot authorize a new pulse phenotype', 'PULSE_FEATURE_NOT_IN_CURRENT_SOURCE', '脉滑');
factStateStreamCheck($pilotLedger, '本次' . $attachmentRole . '及皮疹。', false,
'a clinical list cannot be reclassified as one attachment object', 'NEGATIVE_NOT_IN_SOURCE', '皮疹');
factStateStreamCheck($pilotLedger, $attachmentRole . '(s999)。', false,
'attachment metadata cannot validate a nonexistent citation', 'SOURCE_ANNOTATION_INVALID', '核验');
$referenceLedger = TcmClinicalFactStateGuard::build([
'id' => 1, 'diagnosis_date' => '2026-10-23',
'present_illness' => '口干;无心悸;未记录固定刺痛或耳鸣。',
'past_history' => '无已知冠心病。', 'allergy_history' => '未记录', 'family_history' => '否认',
]);
$referenceIds = [];
foreach ($referenceLedger['sources'] as $source) $referenceIds[$source['field']] = $source['id'];
$denialReference = '既往疾病否认见第8项(' . $referenceIds['past_history'] . ')';
foreach ([$denialReference . '。', $denialReference . ',亦不能替代本次检查。',
'既往史否认见第8项(2026-10-23,' . $referenceIds['past_history'] . ')。',
'家族史否认见第⑧项(' . $referenceIds['family_history'] . ')。'] as $verifiedReference) {
factStateStreamCheck($referenceLedger, $verifiedReference, true,
'a neutral history locator cites a current negative source in the same history field');
}
foreach (['既往疾病否认见第8项。', '既往疾病否认见第8项(s999)。',
'既往疾病否认见第8项(' . $referenceIds['present_illness'] . ')。',
'过敏史否认见第8项(' . $referenceIds['allergy_history'] . ')。',
'既往疾病否认见第8项(2026-10-22,' . $referenceIds['past_history'] . ')。'] as $invalidReference) {
factStateStreamCheck($referenceLedger, $invalidReference, false,
'a history locator needs a real same-time negative source in its own history field', 'SOURCE_ANNOTATION_INVALID', '否认见');
}
factStateStreamCheck($referenceLedger, $denialReference . '且患者无固定刺痛。', false,
'a verified history locator cannot absorb a patient absence', 'UNKNOWN_TO_NEGATIVE', '无固定刺痛');
factStateStreamCheck($referenceLedger, $denialReference . ',患者有心悸。', false,
'a verified history locator cannot reverse another negative', 'NEGATIVE_TO_POSITIVE', '有心悸');
factStateStreamCheck($referenceLedger, $denialReference . '且口干未记录。', false,
'a verified history locator cannot erase a known finding', 'POSITIVE_TO_UNKNOWN', '未记录');
factStateStreamCheck($referenceLedger, '固定刺痛否认见第8项(' . $referenceIds['past_history'] . ')。', false,
'a named clinical symptom is not a neutral history-field locator', 'UNKNOWN_TO_POSITIVE', '固定刺痛');
factStateStreamCheck($referenceLedger, '既往疾病否认见第8项(' . $referenceIds['past_history'] . ',当前患者无固定刺痛)。', false,
'a mixed clinical citation cannot borrow history-reference scope', 'UNKNOWN_TO_NEGATIVE', '无固定刺痛');
factStateStreamCheck($pilotLedger, '既往疾病否认见第8项(s4),舌质淡红而非明确红绛。', false,
'a valid history reference cannot authorize an unsupported tongue comparison', 'TONGUE_FEATURE_NOT_IN_CURRENT_SOURCE', '非明确红绛');
$historicalReferenceLedger = TcmClinicalFactStateGuard::build([
'id' => 1, 'diagnosis_date' => '2026-10-23', 'past_history' => '未记录',
], ['required_content' => [['key' => 'past_history', 'value' => [[
'id' => 2, 'diagnosis_date' => '2026-10-22', 'past_history' => '否认',
]]]]]);
$historicalReferenceSource = array_values(array_filter($historicalReferenceLedger['sources'],
static fn (array $source): bool => $source['scope'] === 'historical'))[0];
factStateStreamCheck($historicalReferenceLedger, '既往疾病否认见第8项(' . $historicalReferenceSource['id'] . ')。', false,
'a historical denial cannot supply a current history reference', 'SOURCE_ANNOTATION_INVALID', '否认见');
foreach (['糖尿病4年。', '既往否认冠心病。', '无附件。', '否认记录有误。'] as $unsupportedHistory) {
$unsupportedReferenceLedger = TcmClinicalFactStateGuard::build([
'diagnosis_date' => '2026-10-23', 'past_history' => $unsupportedHistory,
]);
factStateStreamCheck($unsupportedReferenceLedger, '既往疾病否认见第8项(s1)。', false,
'a positive, historical or metadata-only history source cannot authorize a current denial locator',
'SOURCE_ANNOTATION_INVALID', '否认见');
}
$v33ScopeWire = implode("\r\n\r\n", [$postfixRecordParagraph, $exclusionLimitParagraph,
$attachmentRoleParagraph, $denialReferenceParagraph]) . "\r\n\r\n";
$v33ScopeVisible = '';
$v33ScopeStream = new TcmClinicalReportStream($pilotLedger, static function (string $delta) use (&$v33ScopeVisible): bool {
$v33ScopeVisible .= $delta;
return true;
});
$v33ScopePushed = true;
foreach (str_split($v33ScopeWire, 7) as $chunk) {
if (!$v33ScopeStream->push($chunk)) { $v33ScopePushed = false; break; }
}
factStateExpect($v33ScopePushed && $v33ScopeStream->finish() && !$v33ScopeStream->rejected(),
'all v33 scoped roles survive split UTF-8, source annotations and CRLF paragraph boundaries together');
factStateExpect($v33ScopeVisible === $v33ScopeWire,
'all accepted v33 scoped roles preserve every original streamed byte');
// Follow-up: verification is a bounded review action, not an observed noun list.
$verifySafetyParagraph = '禁忌与安全:虽本次询问无已知药物过敏史,首次用药仍须核对人参、麦冬、五味子及制剂辅料的既往反应。'
. '对药材质量、胃肠耐受、肝肾功能变化及潜在相互作用需复核;妊娠事项不适用于本例男性。'
. '2026-10-20 ALT 22U/L、肌酐78μmol/L、eGFR 96mL/min/1.73m²、尿ACR 12mg/g,可作当前安全基线,但不能保证后续用药绝对安全。';
factStateStreamCheck($pilotLedger, $verifySafetyParagraph, true,
'the complete retained v33 safety paragraph preserves its verification-action role');
$verifyAction = '首次用药仍须核对药材及制剂辅料的既往反应';
foreach ([$verifyAction . '。', '用药前需逐项核对既往反应及合并药物。',
'复诊仍应核对过敏、肝肾功能。', '仍须核对既往不良反应。',
'虽本次询问无已知药物过敏史,' . $verifyAction . '。'] as $neutralVerification) {
factStateStreamCheck($pilotLedger, $neutralVerification, true,
'a modal verification action governs only its neutral object list');
}
foreach (['患者无固定刺痛,' . $verifyAction . '。', $verifyAction . ',患者无固定刺痛。',
$verifyAction . '且患者无固定刺痛。', $verifyAction . '患者无固定刺痛。',
'首次用药须核对无固定刺痛。', '须核对患者无固定刺痛。'] as $mixedVerification) {
factStateStreamCheck($pilotLedger, $mixedVerification, false,
'verification cannot absorb an actual negative prefix, suffix or object assertion', 'UNKNOWN_TO_NEGATIVE', '无固定刺痛');
}
foreach (['患者有心悸,' . $verifyAction . '。', $verifyAction . ',患者有心悸。',
$verifyAction . '且患者有心悸。', $verifyAction . '患者有心悸。',
'首次用药须核对患者有心悸。', '须核对患者有心悸。', '需核对患者有心悸。',
'首次用药仍须核对有心悸。'] as $mixedVerification) {
factStateStreamCheck($pilotLedger, $mixedVerification, false,
'verification cannot absorb an actual positive and cannot borrow a generic modal exemption', 'NEGATIVE_TO_POSITIVE', '有心悸');
}
factStateStreamCheck($pilotLedger, $verifyAction . '且口干未记录。', false,
'a verification action cannot erase a known positive', 'POSITIVE_TO_UNKNOWN', '未记录');
factStateStreamCheck($pilotLedger, $verifyAction . ',心悸未记录。', false,
'a verification action cannot erase a known negative', 'NEGATIVE_TO_UNKNOWN', '未记录');
factStateStreamCheck($gapLedger, $verifyAction . '且患者有耳鸣。', false,
'a historical noun in a verification object cannot redate a new current assertion', 'UNKNOWN_TO_POSITIVE', '有耳鸣');
factStateStreamCheck($pilotLedger, $verifyAction . '且舌质红绛。', false,
'a verification action cannot authorize a new tongue phenotype', 'TONGUE_FEATURE_NOT_IN_CURRENT_SOURCE', '红绛');
factStateStreamCheck($pilotLedger, $verifyAction . '且脉滑。', false,
'a verification action cannot authorize a new pulse phenotype', 'PULSE_FEATURE_NOT_IN_CURRENT_SOURCE', '脉滑');
factStateStreamCheck($pilotLedger, $verifyAction . ',舌质淡红而非明确红绛。', false,
'a verification action cannot reinterpret the unsupported tongue comparison', 'TONGUE_FEATURE_NOT_IN_CURRENT_SOURCE', '非明确红绛');
factStateStreamCheck($pilotLedger, '虽本次询问无药物过敏史,' . $verifyAction . '。', false,
'a verification action cannot authorize deleting the known-allergy qualifier', 'NEGATION_SCOPE_WIDENED', '无药物过敏史');
factStateStreamCheck($allergyUnknown, '虽本次询问无已知药物过敏史,' . $verifyAction . '。', false,
'a verification action cannot make an unknown allergy history an observed negative', 'UNKNOWN_TO_NEGATIVE', '无已知药物过敏史');
factStateStreamCheck($pilotLedger, $verifyAction . '(s999)。', false,
'verification cannot validate a nonexistent source citation', 'SOURCE_ANNOTATION_INVALID', '核对');
$verifyWire = $verifySafetyParagraph . "\r\n\r\n";
$verifyVisible = '';
$verifyStream = new TcmClinicalReportStream($pilotLedger, static function (string $delta) use (&$verifyVisible): bool {
$verifyVisible .= $delta;
return true;
});
$verifyPushed = true;
foreach (str_split($verifyWire, 7) as $chunk) {
if (!$verifyStream->push($chunk)) { $verifyPushed = false; break; }
}
factStateExpect($verifyPushed && $verifyStream->finish() && !$verifyStream->rejected(),
'the complete safety verification paragraph survives split UTF-8 and CRLF boundaries');
factStateExpect($verifyVisible === $verifyWire,
'safety verification preserves every accepted original byte');
$retainedParagraphs = [
'tcm_complete_v33_original_single_20261024_1_openai.rejected.json' => [true, ''],
'tcm_complete_v32_workflow_a_c4_20261024_2_openai.rejected.json' => [true, ''],
'tcm_complete_v32_workflow_a_c4_20261024_3_openai.rejected.json' => [true, ''],
'tcm_complete_v32_workflow_b_c4_20261024_3_openai.rejected.json' => [true, ''],
'tcm_complete_v32_workflow_b_c4_20261024_2_openai.rejected.json' => [false, 'TONGUE_FEATURE_NOT_IN_CURRENT_SOURCE'],
'tcm_complete_v32_workflow_a_single_20261024_1_openai.rejected.json' => [true, ''],
'tcm_complete_v32_workflow_a_single_20261024_0_qwen.rejected.json' => [false, 'NEGATION_SCOPE_WIDENED'],
'tcm_complete_v31_isolated_single_20261023_1_openai.rejected.json' => [true, ''],
'tcm_complete_v31_isolated_c4_20261023_2_openai.rejected.json' => [true, ''],
'tcm_complete_v31_isolated_c4_20261023_3_openai.rejected.json' => [false, 'TONGUE_FEATURE_NOT_IN_CURRENT_SOURCE'],
'tcm_complete_v31_isolated_single_20261023_0_qwen.rejected.json' => [false, 'NEGATION_SCOPE_WIDENED'],
'tcm_complete_v31_isolated_c4_20261023_0_qwen.rejected.json' => [false, 'NEGATION_SCOPE_WIDENED'],
'tcm_complete_v31_isolated_c4_20261023_1_qwen.rejected.json' => [false, 'NEGATION_SCOPE_WIDENED'],
'tcm_complete_v29_isolated_deadline_after_20261023_openai_next_full90_openai.rejected.json' => [true, ''],
'tcm_complete_v30_isolated_single_20261023_1_openai.rejected.json' => [true, ''],
'tcm_complete_v30_isolated_c4_20261023_2_openai.rejected.json' => [true, ''],
'tcm_complete_v30_isolated_c4_20261023_3_openai.rejected.json' => [true, ''],
'tcm_complete_v30_isolated_single_20261023_0_qwen.rejected.json' => [false, 'UNKNOWN_TO_NEGATIVE'],
'tcm_complete_v30_isolated_c4_20261023_0_qwen.rejected.json' => [false, 'NEGATION_SCOPE_WIDENED'],
'tcm_complete_v30_isolated_c4_20261023_1_qwen.rejected.json' => [false, 'NEGATION_SCOPE_WIDENED'],
'tcm_complete_v29_isolated_single_20261023_1_openai.rejected.json' => [true, ''],
'tcm_complete_v29_isolated_c4_20261023_3_openai.rejected.json' => [true, ''],
'tcm_complete_v29_isolated_single_20261023_0_qwen.rejected.json' => [false, 'NEGATION_SCOPE_WIDENED'],
'tcm_complete_v29_isolated_c4_20261023_0_qwen.rejected.json' => [false, 'NEGATION_SCOPE_WIDENED'],
'tcm_complete_v29_isolated_c4_20261023_1_qwen.rejected.json' => [false, 'NEGATION_SCOPE_WIDENED'],
'tcm_complete_v28_isolated_single_20261023_1_openai.rejected.json' => [true, ''],
'tcm_complete_v28_isolated_c4_20261023_2_openai.rejected.json' => [true, ''],
'tcm_complete_v28_isolated_c4_20261023_3_openai.rejected.json' => [true, ''],
'tcm_complete_v28_isolated_single_20261023_0_qwen.rejected.json' => [false, 'NEGATION_SCOPE_WIDENED'],
'tcm_complete_v28_isolated_c4_20261023_0_qwen.rejected.json' => [false, 'NEGATION_SCOPE_WIDENED'],
'tcm_complete_v28_isolated_c4_20261023_1_qwen.rejected.json' => [false, 'NEGATION_SCOPE_WIDENED'],
'tcm_complete_v26_single_fixed_1_openai.rejected.json' => [true, ''],
'tcm_complete_v26_c4_fixed_2_openai.rejected.json' => [true, ''],
'tcm_complete_v26_c4_fixed_3_openai.rejected.json' => [true, ''],
'tcm_complete_v26_single_fixed_0_qwen.rejected.json' => [false, 'NEGATION_SCOPE_WIDENED'],
'tcm_complete_v26_c4_fixed_0_qwen.rejected.json' => [false, 'NEGATION_SCOPE_WIDENED'],
'tcm_complete_v26_c4_fixed_1_qwen.rejected.json' => [false, 'NEGATION_SCOPE_WIDENED'],
'tcm_complete_v25_single_fixed_1_openai.rejected.json' => [true, ''],
'tcm_complete_v25_c4_fixed_2_openai.rejected.json' => [true, ''],
'tcm_complete_v25_c4_fixed_3_openai.rejected.json' => [true, ''],
'tcm_complete_v25_single_fixed_0_qwen.rejected.json' => [false, 'NEGATION_SCOPE_WIDENED'],
'tcm_complete_v25_c4_fixed_0_qwen.rejected.json' => [false, 'NEGATION_SCOPE_WIDENED'],
'tcm_complete_v25_c4_fixed_1_qwen.rejected.json' => [false, 'NEGATION_SCOPE_WIDENED'],
'tcm_complete_v24_single_fixed_0_qwen.rejected.json' => [true, ''],
'tcm_complete_v24_single_fixed_1_openai.rejected.json' => [true, ''],
'tcm_complete_v24_c4_fixed_0_qwen.rejected.json' => [true, ''],
'tcm_complete_v24_c4_fixed_1_qwen.rejected.json' => [true, ''],
'tcm_complete_v24_c4_fixed_2_openai.rejected.json' => [true, ''],
'tcm_complete_v24_c4_fixed_3_openai.rejected.json' => [true, ''],
'tcm_complete_v23_c4_fixed_3_openai.rejected.json' => [true, ''],
'tcm_complete_v23_c4_fixed_2_openai.rejected.json' => [true, ''],
'tcm_complete_v23_single_fixed_0_qwen.rejected.json' => [false, 'TONGUE_FEATURE_NOT_IN_CURRENT_SOURCE'],
'tcm_complete_v23_c4_fixed_1_qwen.rejected.json' => [false, 'NEGATION_SCOPE_WIDENED'],
'tcm_complete_v23_single_fixed_1_openai.rejected.json' => [false, 'TONGUE_FEATURE_NOT_IN_CURRENT_SOURCE'],
'tcm_complete_v22_qwen_single_fixed_0_qwen.rejected.json' => [true, ''],
'tcm_complete_v22_openai_single_fixed_0_openai.rejected.json' => [true, ''],
'tcm_complete_v21_mixed2_fixed_1_openai.rejected.json' => [false, 'NEGATIVE_TO_UNKNOWN'],
'tcm_complete_v20_mixed2_fixed_1_openai.rejected.json' => [true, ''],
'tcm_complete_v19_c4_fixed_2_openai.rejected.json' => [true, ''],
'tcm_complete_v18_c4_fixed_2_openai.rejected.json' => [true, ''],
'tcm_complete_v18_c4_fixed_3_openai.rejected.json' => [true, ''],
'tcm_complete_v14_staged_pilot3_1_openai.rejected.json' => [true, ''],
'tcm_complete_v14_staged_pilot2_1_openai.rejected.json' => [true, ''],
'tcm_complete_v14_staged_pilot4_1_openai.rejected.json' => [true, ''],
'tcm_complete_v14_single_fixed_1_openai.rejected.json' => [true, ''],
'tcm_complete_v15_single_fixed_1_openai.rejected.json' => [true, ''],
'tcm_complete_v17_single_fixed_1_openai.rejected.json' => [true, ''],
'tcm_complete_v17_c4_fixed_2_openai.rejected.json' => [false, 'TONGUE_FEATURE_NOT_IN_CURRENT_SOURCE'],
'tcm_complete_v17_c4_fixed_3_openai.rejected.json' => [false, 'TONGUE_FEATURE_NOT_IN_CURRENT_SOURCE'],
'tcm_complete_v17_c4_fixed_0_qwen.rejected.json' => [false, 'NEGATION_SCOPE_WIDENED'],
'tcm_complete_v17_c4_fixed_1_qwen.rejected.json' => [false, 'NEGATION_SCOPE_WIDENED'],
'tcm_complete_v16_single_fixed_1_openai.rejected.json' => [true, ''],
'tcm_complete_v16_c4_fixed_2_openai.rejected.json' => [true, ''],
'tcm_complete_v16_c4_fixed_3_openai.rejected.json' => [true, ''],
'tcm_complete_v14_staged_pilot3_0_qwen.rejected.json' => [false, 'NEGATIVE_NOT_IN_SOURCE'],
'tcm_complete_v14_staged_pilot2_0_qwen.rejected.json' => [false, 'NEGATION_SCOPE_WIDENED'],
];
if (!$replayLocalArtifacts) $retainedParagraphs = [];
foreach ($retainedParagraphs as $file => [$valid, $code]) {
$path = $artifactRoot . $file;
if (!is_file($path)) continue;
$saved = json_decode((string) file_get_contents($path), true);
factStateExpect(is_array($saved['rejections'] ?? null) && $saved['rejections'] !== [], 'pilot artifact contains the complete rejected paragraph');
foreach ($saved['rejections'] ?? [] as $rejection) {
factStateCheck($pilotLedger, (string) $rejection['unit'], $valid, 'full retained pilot paragraph: ' . $file, $code);
}
}
factStateCheck([], '无固定刺痛。', false, 'missing ledger cannot produce a successful validation', 'FACT_STATE_LEDGER_UNAVAILABLE');
factStateCheck($ledger, "\xC3\x28", false, 'malformed UTF-8 is not silently accepted', 'FACT_STATE_INVALID_UTF8');
factStateExpect(str_contains(TcmClinicalFactStateGuard::prompt($fixtureLedger), '固定刺痛')
&& mb_strlen(TcmClinicalFactStateGuard::prompt($fixtureLedger), 'UTF-8') < 700,
'compact prompt points to explicit unknown topics without duplicating the case');
if ($failures !== []) {
fwrite(STDERR, 'Clinical fact state: ' . count($failures) . ' failures / ' . $assertions . " assertions\n- " . implode("\n- ", $failures) . "\n");
exit(1);
}
echo 'Clinical fact state: ' . $assertions . " assertions passed.\n";