1587 lines
142 KiB
PHP
1587 lines
142 KiB
PHP
<?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";
|