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

1177 lines
89 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
declare(strict_types=1);
require dirname(__DIR__) . '/vendor/autoload.php';
require __DIR__ . '/fixtures/CompleteTcmCase.php';
use app\adminapi\service\AiRequiredSource;
use app\adminapi\service\TcmClinicalEvidenceContract as Contract;
use app\adminapi\service\TcmClinicalEvidenceIndex as Index;
use app\adminapi\service\TcmClinicalFactStateGuard as Guard;
$count = 0;
function evidenceContractCheck(array $index, string $text, bool $expected, string $code = '', bool $complete = false): void
{
++$GLOBALS['count'];
$before = $text;
$result = Contract::validate($index, $text, $complete);
if ($result['valid'] !== $expected || ($code !== '' && !in_array($code, array_column($result['issues'], 'code'), true))) {
throw new RuntimeException('Evidence contract result mismatch: ' . json_encode([$text, $expected, $code, $result], JSON_UNESCAPED_UNICODE));
}
++$GLOBALS['count'];
if ($text !== $before) throw new RuntimeException('Validation must preserve all original model bytes.');
foreach ($result['issues'] as $issue) {
++$GLOBALS['count'];
if (array_keys($issue) !== ['code', 'source_id'] || !preg_match('/^[A-Z][A-Z0-9_]+$/D', $issue['code'])
|| !preg_match('/^(?:s[1-9][0-9]*|report|)$/D', $issue['source_id'])) {
throw new RuntimeException('Only safe issue codes and source IDs may be returned.');
}
}
}
function evidenceSourceId(array $index, string $field, string $path = ''): string
{
foreach ($index['sources'] as $id => $source) if ($source['field'] === $field
&& ($path === '' || str_starts_with($source['path'], $path))) return $id;
throw new RuntimeException('Missing test source.');
}
/** Pure Contract/API controls; no ledger builder or stream class is invoked. */
function evidenceContractMetaChecks(array $index, string $first, string $denialId, string $wrongId): void
{
$head = $first . "二、八纲\n";
$capability = '但对脏腑定位并无额外证明作用';
$classified = '尚无足以确立痰、湿、瘀、食积等实邪的阳性证据';
foreach ([$capability . '。', '对于证型与病机没有独立佐证作用。', '对辨证结论尚无直接支持作用。',
$classified . '。', '没有足以确认痰或湿等病邪的独立证据。',
'无足以支持气虚证与阴虚证等证候的直接依据。',
'无足以证明阳虚、阴虚或阳亢等证候的充分阳性证据。',
'无足以排除阴虚的独立证据。',
'不能据此写成“无实邪”。', '不得写为「无胸痛」。', '不应当直接表述为"无口干"。',
'不能写为“无副作用”。',
'现有证据不能直接归类为“无实邪”。', '尚不能据此判定为“无证候”。',
$classified . ',不能据此写成“无实邪”。',
'不能据此写成“无实邪”,2026-10-23“无胸痛”(' . $denialId . ')。',
'不能据此写成2026-10-23“无胸痛”(' . $denialId . ')。'] as $phrase) {
evidenceContractCheck($index, $head . $phrase, true);
}
foreach (['患者无副作用。', '药物没有相互作用。', '对脏腑定位并无副作用。',
'对阴虚没有相互作用。', '对口干或舌红无额外证明作用。',
'对患者有胸痛诊断无直接证明作用。', $capability . '性皮疹。',
$capability . ',患者无口干。', $capability . '且患者无头晕。',
'尚无足以确立口干、舌红等症状的阳性证据。',
'尚无足以确立口干、舌红等实邪的阳性证据。',
'无足以确立患者无口干证的独立证据。',
'无足以确立患者有胸痛诊断的阳性证据。',
'无足以确立痰、湿、瘀、食积的阳性证据。', '无痰的独立证据。',
'无足以出现阴虚的独立证据。', $classified . '性皮疹。',
$classified . '证明患者无头晕。', $classified . ',患者无口干。',
'无足以确立' . implode('、', array_fill(0, 9, '痰')) . '等实邪的阳性证据。'] as $assertion) {
evidenceContractCheck($index, $head . $assertion, false, 'UNQUOTED_PATIENT_DENIAL');
}
foreach (['可据此写成“无实邪”。', '写成“无口干”。', '并非不能据此写成“无实邪”。',
'不是不能写成“无实邪”。', '不能说不能写成“无口干”。', '并非“这一结论”不能写成“无实邪”。',
'不能据此写成“无实邪”是事实。', '不能据此写成“无实邪”“无口干”。',
'不能据此写成“无实邪”(s999)。', '不能据此写成“无实邪”(s0)。',
'不能据此写成“无实邪”(非来源)。',
'不能据此写成“无' . str_repeat('甲', 80) . '”。'] as $notIllustrative) {
evidenceContractCheck($index, $head . $notIllustrative, false, 'DENIAL_SOURCE_QUOTE_INVALID');
}
evidenceContractCheck($index, $head . '不能据此写成“无实邪”,患者无口干。', false, 'UNQUOTED_PATIENT_DENIAL');
evidenceContractCheck($index, $head . '不得据此写成“无副作用”,药物无相互作用。', false, 'UNQUOTED_PATIENT_DENIAL');
evidenceContractCheck($index, $head . '不能据此写成“无实邪”,患者否认头晕。', false, 'UNQUOTED_PATIENT_DENIAL');
evidenceContractCheck($index, $head . '不能据此写成“无胸痛”(' . $wrongId . ')。', false, 'DENIAL_SOURCE_QUOTE_INVALID');
evidenceContractCheck($index, $head . '不能据此写成2026-10-22“无胸痛”(' . $denialId . ')。', false, 'EVIDENCE_SOURCE_DATE_MISMATCH');
evidenceContractCheck($index, $head . '不能据此写成本次2026-10-22“无晕厥”(s90)。', false, 'EXAM_SOURCE_NOT_CURRENT');
evidenceContractCheck($index, '一、四诊' . "\n问诊:不能据此写成“无口干”。", false, 'EXAM_FACT_PROTOCOL_INVALID');
foreach ([$capability, $classified] as $prefix) {
evidenceContractCheck($index, $head . $prefix . ';“无胸痛”(s999)。', false, 'DENIAL_SOURCE_QUOTE_INVALID');
evidenceContractCheck($index, $head . $prefix . ';2026-10-22“无胸痛”(' . $denialId . ')。', false, 'EVIDENCE_SOURCE_DATE_MISMATCH');
evidenceContractCheck($index, $head . $prefix . ';本次2026-10-22“无晕厥”(s90)。', false, 'EXAM_SOURCE_NOT_CURRENT');
}
}
function evidenceContractR13Units(): array
{
return [
'寒热:口干、少津、苔少提示津液不足,但不足以单独成立热证;寒证亦无独立阳性寒象。寒热两端现有资料不足以确定,不能因一端证据不足反定另一端。' . "\n\n",
'虚实:神疲、语声低微、动则气短、自汗,兼脉细弱、按之无力(s1、s3、s10),共同支持虚证。实证缺乏独立阳性依据;舌脉相关限制见s9、s10,不能据此反推纯虚无实。' . "\n\n",
];
}
/** Typed epistemic roles do not authorize an unquoted patient/observed absence. */
function evidenceContractInferenceChecks(array $index, string $first, string $denialId, string $wrongId): void
{
$head = $first . "二、八纲\n";
foreach (['寒证亦无独立阳性寒象。', '热证尚无明确独立阳性热象证据。',
'虚证没有充分独立阳性虚象依据。', '表证亦无阳性的独立表象。',
'阴证无独立的阳性阴象。', '寒热:寒证亦无独立阳性寒象。',
'不能据此反推纯虚无实。', '不可仅据此推出单纯热证无寒证。',
'现有资料不应由此判定为无实证。', '不得直接推断为阴证而没有阳证。',
'不能反推出纯虚无实的结论。', '虚实:不能据此反推纯虚无实。'] as $phrase) {
evidenceContractCheck($index, $head . $phrase, true);
}
foreach (['患者无独立阳性寒象。', '患者寒证亦无独立阳性寒象。',
'患者:寒证亦无独立阳性寒象。', '本次查见:寒证亦无独立阳性寒象。',
'寒证患者亦无独立阳性寒象。', '寒证亦无寒象。', '寒证亦无独立寒象。',
'寒证亦无独立阳性畏寒肢冷。', '寒证亦无独立阳性口干。',
'寒证亦无独立阳性舌红。', '寒证亦无独立阳性热象。',
'寒证亦无独立阳性寒象性皮疹。',
'不能据此反推无口干。', '不能据此反推无舌红。', '不能据此反推无寒冷。',
'不能据此反推无畏寒肢冷。', '不能据此反推患者纯虚无实。',
'不能据此反推本次无寒象。', '不能据此反推查见无实证。',
'患者不能据此反推纯虚无实。', '患者:不能据此反推纯虚无实。',
'并非不能据此反推纯虚无实。', '不是不能反推纯虚无实。',
'不能不反推纯虚无实。', '可以据此反推纯虚无实。',
'不能据此反推纯虚无实性皮疹。'] as $assertion) {
evidenceContractCheck($index, $head . $assertion, false, 'UNQUOTED_PATIENT_DENIAL');
}
evidenceContractCheck($index, $first . "三、气血津液与脏腑\n寒证亦无独立阳性寒象。", false, 'UNQUOTED_PATIENT_DENIAL');
foreach (['寒证亦无独立阳性寒象', '不能据此反推纯虚无实'] as $epistemic) {
foreach ([',患者无口干。', '且否认头晕。', ';本次未见舌红。', ',无副作用。', ',没有相互作用。'] as $tail) {
evidenceContractCheck($index, $head . $epistemic . $tail, false, 'UNQUOTED_PATIENT_DENIAL');
}
evidenceContractCheck($index, $head . $epistemic . ';“无胸痛”(s999)。', false, 'DENIAL_SOURCE_QUOTE_INVALID');
evidenceContractCheck($index, $head . $epistemic . ';“无胸痛”(' . $wrongId . ')。', false, 'DENIAL_SOURCE_QUOTE_INVALID');
evidenceContractCheck($index, $head . $epistemic . ';2026-10-22“无胸痛”(' . $denialId . ')。', false, 'EVIDENCE_SOURCE_DATE_MISMATCH');
evidenceContractCheck($index, $head . $epistemic . ';本次2026-10-22“无晕厥”(s90)。', false, 'EXAM_SOURCE_NOT_CURRENT');
evidenceContractCheck($index, $head . $epistemic . ';2026-10-23“无胸痛”(' . $denialId . ')。', true);
evidenceContractCheck($index, "一、四诊\n问诊:" . $epistemic . '。', false, 'EXAM_FACT_PROTOCOL_INVALID');
}
$qualified = $index;
$qualified['sources']['s88'] = ['quote' => '无明显畏寒、肢冷。', 'scope' => 'current', 'date' => '2026-10-23',
'record_state' => 'negative', 'spans' => [['role' => 'interview', 'recorded' => true, 'quote' => '无明显畏寒、肢冷。']]];
evidenceContractCheck($qualified, $head . '不能据此反推纯虚无实;2026-10-23“无明显畏寒、肢冷。”(s88)。', true);
foreach (['无畏寒、肢冷。', '无明显畏寒'] as $widenedQuote) {
evidenceContractCheck($qualified, $head . '不能据此反推纯虚无实;2026-10-23“' . $widenedQuote . '”(s88)。', false, 'DENIAL_SOURCE_QUOTE_INVALID');
}
foreach (['categoricalEvidenceAbsence' => '寒证亦无独立阳性寒象',
'negatedCategoricalInference' => '不能据此反推纯虚无实'] as $method => $phrase) {
foreach ([',患者有胸痛。', ',舌质红绛。', ',脉滑。', ',胸痛未记录。'] as $tail) {
$masked = (new ReflectionMethod(Contract::class, $method))->invoke(null, $phrase . $tail);
++$GLOBALS['count'];
if (!str_ends_with($masked, $tail)) throw new RuntimeException('Epistemic noun scope consumed an adjacent factual predicate.');
}
}
foreach (evidenceContractR13Units() as $unit) evidenceContractCheck($index, $head . $unit, true);
}
function evidenceContractR15Units(): array
{
return [
'tcm_complete_v43r14_openai_deadline_20261024_openai_next_full90_openai.rejected.json' =>
'虚实:乏力、动则气短、自汗、神疲、语声低微及脉细弱无力(s1、s3、s10)共同支持虚证。实证所需壅滞、积聚等阳性依据不足,不能仅据虚证直接判定全属虚而无实。' . "\n\n",
'tcm_complete_v43r14_supplement_ooc2_20261024_1_openai.rejected.json' =>
'阴阳:本次可判断病性偏虚,但虚证不能单独推出阴证;亦无支持阳证的独立阳性组合,故阴阳属性现有资料不足以确定。' . "\n\n",
];
}
/** Explicit abstract premises/complements cannot lend their role to patient assertions. */
function evidenceContractR15InferenceChecks(array $index, string $first, string $denialId, string $wrongId): void
{
$head = $first . "二、八纲\n";
$inference = '不能仅据虚证直接判定全属虚而无实';
$support = '亦无支持阳证的独立阳性组合';
foreach ([$inference . '。', $support . '。',
'不可只凭热证推定全为热而没有寒。',
'现有证据不应当单凭里证认定均属里而无表。',
'不得依据阴证直接判断为纯阴无阳。',
'不宜根据实证推出全部属于实而无虚。',
'不能由此判定全属虚证而无实证的结论。',
'不能直接判定全属虚而无实。', '不能仅据阴证判定没有阳证。',
'不能只据寒证推断为单纯寒证无热证。',
'没有佐证里证的充分独立阳性组合。',
'尚无证明寒证的阳性的独立依据。',
'仍无支持阴证型的可靠独立的阳性组合。'] as $phrase) {
evidenceContractCheck($index, $head . $phrase, true);
}
foreach (['患者全属虚而无实。', '本次查见全属虚而无实。', '已经判定全属虚而无实。',
'可以仅据虚证直接判定全属虚而无实。', '并非' . $inference . '。', '不是' . $inference . '。',
'不能不根据虚证直接判定全属虚而无实。',
'不应当不依据虚证判定全属虚而无实。',
'不能仅据虚证直接不判定全属虚而无实。',
'不能仅据患者虚证直接判定全属虚而无实。',
'不能仅据本次虚证直接判定全属虚而无实。',
'不能仅据查见虚证直接判定全属虚而无实。',
'不能仅据虚证直接判定患者全属虚而无实。',
'不能仅据口干直接判定全属虚而无实。',
'不能仅据舌红证直接判定全属虚而无实。',
'不能仅据虚证直接判定全属虚而无口干。',
'不能仅据虚证直接判定全属虚而无舌红。',
'不能仅据虚证直接判定全属虚而无畏寒肢冷。',
'不能仅据虚证未记录直接判定全属虚而无实。',
$inference . '性皮疹。', $inference . '(s2)。',
'患者:' . $inference . '。', '2026-10-23' . $inference . '。',
'没有支持口干的独立阳性组合。', '无支持舌红的独立阳性组合。',
'无支持患者有口干阳证的独立阳性组合。',
'无支持本次阳证的独立阳性组合。',
'无支持阳证未记录的独立阳性组合。',
'无支持阳证的组合。', '无支持阳证的独立组合。', '无支持阳证的阳性组合。',
'无支持阳证的独立阳性口干。', $support . '性皮疹。',
'并非' . $support . '。', '无并非支持阳证的独立阳性组合。'] as $assertion) {
evidenceContractCheck($index, $head . $assertion, false, 'UNQUOTED_PATIENT_DENIAL');
}
evidenceContractCheck($index, $first . "三、气血津液与脏腑\n" . $support . '。', false, 'UNQUOTED_PATIENT_DENIAL');
foreach ([$inference, $support] as $phrase) {
foreach ([',患者无口干。', '且否认头晕。', '而本次未见舌红。', ';没有相互作用。'] as $tail) {
evidenceContractCheck($index, $head . $phrase . $tail, false, 'UNQUOTED_PATIENT_DENIAL');
}
evidenceContractCheck($index, $head . '患者无口干,' . $phrase . '。', false, 'UNQUOTED_PATIENT_DENIAL');
evidenceContractCheck($index, $head . $phrase . ';“无胸痛”(s999)。', false, 'DENIAL_SOURCE_QUOTE_INVALID');
evidenceContractCheck($index, $head . $phrase . ';“无胸痛”(' . $wrongId . ')。', false, 'DENIAL_SOURCE_QUOTE_INVALID');
evidenceContractCheck($index, $head . $phrase . ';2026-10-22“无胸痛”(' . $denialId . ')。', false, 'EVIDENCE_SOURCE_DATE_MISMATCH');
evidenceContractCheck($index, $head . $phrase . ';本次2026-10-22“无晕厥”(s90)。', false, 'EXAM_SOURCE_NOT_CURRENT');
evidenceContractCheck($index, $head . $phrase . ';2026-10-23“无胸痛”(' . $denialId . ')。', true);
evidenceContractCheck($index, "一、四诊\n问诊:" . $phrase . '。', false, 'EXAM_FACT_PROTOCOL_INVALID');
}
$qualified = $index;
$qualified['sources']['s88'] = ['quote' => '无明显畏寒、肢冷。', 'scope' => 'current', 'date' => '2026-10-23',
'record_state' => 'negative', 'spans' => [['role' => 'interview', 'recorded' => true, 'quote' => '无明显畏寒、肢冷。']]];
foreach ([$inference, $support] as $phrase) {
evidenceContractCheck($qualified, $head . $phrase . ';2026-10-23“无明显畏寒、肢冷。”(s88)。', true);
evidenceContractCheck($qualified, $head . $phrase . ';2026-10-23“无畏寒、肢冷。”(s88)。', false, 'DENIAL_SOURCE_QUOTE_INVALID');
}
foreach (['negatedCategoricalInference' => $inference, 'categoricalEvidenceAbsence' => $support] as $method => $phrase) {
foreach (['患者有胸痛。', '患者无口干。', '舌质红绛。', '脉滑。', '胸痛未记录。'] as $assertion) {
foreach ([$assertion . ',' . $phrase, $phrase . ',' . $assertion] as $text) {
$masked = (new ReflectionMethod(Contract::class, $method))->invoke(null, $text);
++$GLOBALS['count'];
if (!str_contains($masked, $assertion)) throw new RuntimeException('Abstract inference/support role consumed an adjoining factual predicate.');
}
}
}
foreach (evidenceContractR15Units() as $file => $unit) {
$path = dirname(__DIR__, 2) . '/app/artifacts/ai_concurrency/' . $file;
if (is_file($path)) {
$saved = json_decode((string) file_get_contents($path), true);
++$GLOBALS['count'];
if (($saved['rejections'][0]['unit'] ?? '') !== $unit) throw new RuntimeException('Portable r15 unit differs from retained original model bytes.');
}
evidenceContractCheck($index, $head . $unit, true);
}
}
function evidenceContractR14Condition(): string
{
return '适用条件:复核后仍以气虚、津亏和自汗为主要矛盾,且没有新出现的实邪、急性脱水或需优先处置的异常时,方可由医师决定是否采用。若证候转变,应重新辨证。';
}
/** Future criteria are not present findings, and cannot absorb neighbouring claims. */
function evidenceContractFutureConditionChecks(array $index, string $first, string $denialId, string $wrongId): void
{
$head = $first . "六、中药方药及复核监测\n";
$condition = evidenceContractR14Condition();
foreach ([$condition, '适用前提:复诊后没有新增异常时,方可考虑。',
'采用条件:重新评估后仍符合原证候,且无新出现的不适时方可决定是否采用。',
'用药条件:核实后没有需要优先处理的异常时,方可考虑。',
'适用条件:复核后,若没有新增口干时,方可考虑。'] as $future) {
evidenceContractCheck($index, $head . $future, true);
}
foreach (['没有新出现的实邪、急性脱水或需优先处置的异常。',
'适用条件:没有新出现的异常时,方可考虑。',
'适用条件:复核后没有新出现的异常,方可考虑。',
'适用条件:复核后没有新出现的异常时,可以考虑。',
'适用条件:已复核后没有新出现的异常时,方可考虑。',
'适用条件:患者复核后没有新出现的异常时,方可考虑。',
'适用条件:复核后患者没有新出现的异常时,方可考虑。',
'适用条件:复核后发现没有新出现的异常时,方可考虑。',
'适用条件:复核后仍符合原证候,且目前没有新出现的异常时,方可考虑。',
'适用条件:复核后仍符合原证候,且没有已经确认的异常时,方可考虑。',
'适用条件:复核后仍符合原证候,且没有此前出现的异常时,方可考虑。',
'适用条件:复核后没有新出现的异常且患者无口干时,方可考虑。',
'适用条件:复核后没有新出现的异常,患者无口干时,方可考虑。',
'适用条件:复核后没有新出现的异常并否认头晕时,方可考虑。',
'适用条件:复核后没有胸痛未记录时,方可考虑。',
'适用条件:复核后没有新出现的异常(s999)时,方可考虑。',
'适用条件:复核后没有' . str_repeat('甲', 65) . '时,方可考虑。',
'适用条件:复核后' . str_repeat('甲', 65) . '且没有新增异常时,方可考虑。'] as $notFutureOnly) {
evidenceContractCheck($index, $head . $notFutureOnly, false, 'UNQUOTED_PATIENT_DENIAL');
}
foreach (['患者无口干。' . $condition, '患者无口干;' . $condition,
$condition . '患者无口干。', $condition . '且患者无头晕。',
$condition . '患者未见舌红。'] as $adjoiningDenial) {
evidenceContractCheck($index, $head . $adjoiningDenial, false, 'UNQUOTED_PATIENT_DENIAL');
}
evidenceContractCheck($index, $head . $condition . '“无胸痛”(s999)。', false, 'DENIAL_SOURCE_QUOTE_INVALID');
evidenceContractCheck($index, $head . $condition . '“无胸痛”(' . $wrongId . ')。', false, 'DENIAL_SOURCE_QUOTE_INVALID');
evidenceContractCheck($index, $head . $condition . '2026-10-22“无胸痛”(' . $denialId . ')。', false, 'EVIDENCE_SOURCE_DATE_MISMATCH');
evidenceContractCheck($index, $head . $condition . '本次2026-10-22“无晕厥”(s90)。', false, 'EXAM_SOURCE_NOT_CURRENT');
evidenceContractCheck($index, $head . $condition . '2026-10-23“无胸痛”(' . $denialId . ')。', true);
$qualified = $index;
$qualified['sources']['s88'] = ['quote' => '无明显畏寒、肢冷。', 'scope' => 'current', 'date' => '2026-10-23',
'record_state' => 'negative', 'spans' => [['role' => 'interview', 'recorded' => true, 'quote' => '无明显畏寒、肢冷。']]];
evidenceContractCheck($qualified, $head . $condition . '2026-10-23“无畏寒、肢冷。”(s88)。', false, 'DENIAL_SOURCE_QUOTE_INVALID');
evidenceContractCheck($index, "一、四诊\n问诊:" . $condition, false, 'EXAM_FACT_PROTOCOL_INVALID');
foreach (['患者有胸痛。', '舌质红绛。', '脉滑。', '胸痛未记录。'] as $assertion) {
foreach ([$assertion . $condition, $condition . $assertion] as $text) {
$masked = (new ReflectionMethod(Contract::class, 'prospectiveApplicabilityAbsence'))->invoke(null, $text);
++$GLOBALS['count'];
if (!str_contains($masked, $assertion)) throw new RuntimeException('A future criterion consumed an adjoining factual predicate.');
}
}
$path = dirname(__DIR__, 2) . '/app/artifacts/ai_concurrency/tcm_complete_v43r13_single_openai_20261024_0_openai.rejected.json';
if (is_file($path)) {
$saved = json_decode((string) file_get_contents($path), true);
$unit = $saved['rejections'][1]['unit'] ?? '';
++$GLOBALS['count'];
if (!is_string($unit) || !str_contains($unit, $condition)) throw new RuntimeException('The retained r13 future-condition unit differs from the expected raw bytes.');
evidenceContractCheck($index, $first . $unit, true);
}
}
// This narrow mode can run while other workers own the source builder/stream.
if (in_array('--pure-meta', $argv ?? [], true)) {
if (class_exists(Index::class, false)) throw new RuntimeException('Pure mode cannot use the mutable production Index.');
final class EvidenceContractPureIndex
{
public const VERSION = 'isolated-contract-test-index';
public const ROLES = ['望诊' => 'inspection', '闻诊' => 'listening', '问诊' => 'interview',
'切诊' => 'palpation', '舌象' => 'tongue', '脉象' => 'pulse'];
}
class_alias(EvidenceContractPureIndex::class, Index::class);
$pureIndex = ['version' => Index::VERSION, 'valid' => true, 'sources' => []];
$pureSpecs = [
's1' => ['望诊', '神疲。'], 's2' => ['问诊', '口干。无胸痛。未记录头晕。'],
's3' => ['闻诊', '语声低微。'], 's4' => ['舌象', '舌质淡红,舌面少津。'],
's5' => ['脉象', '脉细弱,按之无力。'],
];
$pureLines = [];
foreach ($pureSpecs as $id => [$role, $quote]) {
$pureIndex['sources'][$id] = ['quote' => $quote, 'scope' => 'current', 'date' => '2026-10-23',
'record_state' => 'positive', 'spans' => [['role' => Index::ROLES[$role], 'recorded' => true, 'quote' => $quote]]];
$pureLines[$role] = $role . ':2026-10-23“' . $quote . '”(' . $id . ')。';
}
$pureIndex['sources']['s90'] = ['quote' => '活动后汗出。无晕厥。', 'scope' => 'historical',
'date' => '2026-10-22', 'record_state' => 'positive', 'spans' => []];
$pureFirst = "一、四诊\n" . implode("\n", [$pureLines['望诊'], $pureLines['闻诊'], $pureLines['问诊'],
'切诊:其他触诊未记录,脉象见独立脉象项。', $pureLines['舌象'], $pureLines['脉象']]) . "\n";
evidenceContractCheck($pureIndex, $pureFirst, true, '', true);
evidenceContractMetaChecks($pureIndex, $pureFirst, 's2', 's1');
evidenceContractInferenceChecks($pureIndex, $pureFirst, 's2', 's1');
evidenceContractR15InferenceChecks($pureIndex, $pureFirst, 's2', 's1');
evidenceContractFutureConditionChecks($pureIndex, $pureFirst, 's2', 's1');
$r13Path = dirname(__DIR__, 2) . '/app/artifacts/ai_concurrency/tcm_complete_v43r12_openai_deadline_20261024_openai_next_full90_openai.rejected.json';
if (is_file($r13Path)) {
$r13Saved = json_decode((string) file_get_contents($r13Path), true);
++$count;
if (array_column($r13Saved['rejections'] ?? [], 'unit') !== evidenceContractR13Units()) {
throw new RuntimeException('Portable r13 units differ from retained raw failure bytes.');
}
}
foreach (['tcm_complete_v43r10_single_openai_20261024_0_openai.rejected.json',
'tcm_complete_v43r10_supplement_ooc2_20261024_1_openai.rejected.json'] as $file) {
$path = dirname(__DIR__, 2) . '/app/artifacts/ai_concurrency/' . $file;
if (!is_file($path)) continue;
$saved = json_decode((string) file_get_contents($path), true);
$unit = $saved['rejections'][0]['unit'] ?? null;
if (!is_string($unit) || $unit === '') throw new RuntimeException('Saved r10 pure unit is unavailable.');
evidenceContractCheck($pureIndex, $pureFirst . "二、八纲\n" . $unit, true);
}
++$count;
if (class_exists(Guard::class, false) || class_exists(\app\adminapi\service\TcmClinicalReportStream::class, false)
|| (new ReflectionClass(Index::class))->getName() !== EvidenceContractPureIndex::class) {
throw new RuntimeException('Pure Contract mode must not load mutable source/stream dependencies.');
}
echo "Clinical evidence contract pure metalinguistic controls: {$count} assertions passed.\n";
exit(0);
}
$row = ['id' => 71, 'diagnosis_date' => '2026-10-23', 'chief_complaint' => '口干3个月。',
'present_illness' => '活动后汗出。无胸痛。未记录头晕。',
'symptoms' => '望诊神疲、面色少华;闻诊语声低微,无异常气味。问诊及切诊见本次记录。',
'tongue' => '舌质淡红,舌面少津;无黄腻苔。', 'pulse' => '脉细弱,按之无力。'];
$ledger = Guard::build($row);
$ledger['sources']['s90'] = ['id' => 's90', 'path' => 'chat.im.0.text', 'field' => 'text',
'scope' => 'historical', 'date' => '2026-10-22', 'quote' => '活动后汗出。无晕厥。'];
$ledger['sources']['s91'] = ['id' => 's91', 'path' => 'prescriptions.0.prescription_name', 'field' => 'prescription_name',
'scope' => 'historical', 'date' => '2020-05-02', 'quote' => '历史方药早已停用。'];
$index = Index::build($ledger);
$history = evidenceSourceId($index, 'present_illness');
$mixed = evidenceSourceId($index, 'symptoms');
$tongue = evidenceSourceId($index, 'tongue');
$pulse = evidenceSourceId($index, 'pulse');
$first = "一、四诊\n\n望诊:2026-10-23“神疲、面色少华”({$mixed})。\n\n"
. "闻诊:2026-10-23“语声低微,无异常气味”({$mixed})。\n\n"
. "问诊:2026-10-23“活动后汗出。无胸痛。未记录头晕。”({$history})。\n\n"
. "切诊:其他触诊未记录,脉象见独立脉象项。\n\n"
. "舌象:2026-10-23“舌质淡红,舌面少津;无黄腻苔。”({$tongue})。\n\n"
. "脉象:2026-10-23“脉细弱,按之无力。”({$pulse})。\n\n"
. "追踪:2026-10-22“活动后汗出。无晕厥。”(s90)。\n\n";
evidenceContractCheck($index, $first, true, '', true);
evidenceContractCheck($index, '一、四诊', true);
evidenceContractCheck($index, "一、四诊\n\n望诊:2026-10-23“神疲、面色少华”({$mixed})。", true);
evidenceContractCheck($index, "一、四诊\n\n望诊:2026-10-23“神疲、面色少华”({$mixed})。", false, 'EXAM_ROLE_MISSING', true);
evidenceContractCheck($index, "望诊:2026-10-23“活动后汗出”({$history})。", false, 'EXAM_ROLE_MISMATCH');
evidenceContractCheck($index, "切诊:2026-10-23“活动后汗出”({$history})。", false, 'EXAM_ROLE_MISMATCH');
evidenceContractCheck($index, "望诊:2026-10-23“语声低微”({$mixed})。", false, 'EXAM_SOURCE_QUOTE_INVALID');
evidenceContractCheck($index, "闻诊:2026-10-23“神疲”({$mixed})。", false, 'EXAM_SOURCE_QUOTE_INVALID');
evidenceContractCheck($index, "切诊:2026-10-23“及切诊见本次记录”({$mixed})。", false, 'EXAM_ROLE_MISMATCH');
evidenceContractCheck($index, "脉象:2026-10-23“舌质淡红”({$tongue})。", false, 'EXAM_ROLE_MISMATCH');
evidenceContractCheck($index, "问诊:2026-10-21“活动后汗出”({$history})。", false, 'EVIDENCE_SOURCE_DATE_MISMATCH');
evidenceContractCheck($index, "问诊:2026-10-23、2026-99-99“活动后汗出”({$history})。", false, 'EVIDENCE_SOURCE_DATE_MISMATCH');
evidenceContractCheck($index, "问诊:“活动后汗出”({$history})。", false, 'EVIDENCE_SOURCE_DATE_MISMATCH');
evidenceContractCheck($index, "问诊:2026年10月23日“活动后汗出”({$history})。", true);
evidenceContractCheck($index, "问诊补充:2026-10-23“活动后汗出”({$history})。", true);
evidenceContractCheck($index, "切诊补充:2026-10-23“活动后汗出”({$history})。", false, 'EXAM_ROLE_MISMATCH');
evidenceContractCheck($index, "望诊补充:2026-10-23“活动后汗出”({$history})。", false, 'EXAM_ROLE_MISMATCH');
evidenceContractCheck($index, "问诊补充:2026-10-22“活动后汗出”({$history})。", false, 'EVIDENCE_SOURCE_DATE_MISMATCH');
evidenceContractCheck($index, "问诊补充:2026-10-23“活动后汗出”({$history}),患者无头晕。", false, 'EXAM_FACT_PROTOCOL_INVALID');
evidenceContractCheck($index, "问诊:日期未记录“活动后汗出”({$history})。", false, 'EVIDENCE_SOURCE_DATE_MISMATCH');
$undatedRow = $row;
unset($undatedRow['diagnosis_date']);
$undatedIndex = Index::build(Guard::build($undatedRow));
$undatedHistory = evidenceSourceId($undatedIndex, 'present_illness');
evidenceContractCheck($undatedIndex, "问诊:日期未记录“活动后汗出”({$undatedHistory})。", true);
evidenceContractCheck($undatedIndex, "问诊:2026-10-23“活动后汗出”({$undatedHistory})。", false, 'EVIDENCE_SOURCE_DATE_MISMATCH');
evidenceContractCheck($undatedIndex, '问诊:未记录。', false, 'EXAM_KNOWN_ROLE_TO_UNKNOWN');
evidenceContractCheck($undatedIndex, "问诊:2026-10-23日期未记录“活动后汗出”({$undatedHistory})。", false, 'EVIDENCE_SOURCE_DATE_MISMATCH');
evidenceContractCheck($index, "问诊:2026-10-23“活动后汗出”({$history}),“无胸痛”({$history})。", true);
evidenceContractCheck($index, "问诊:2026-10-23“活动后大汗出”({$history})。", false, 'EXAM_SOURCE_QUOTE_INVALID');
evidenceContractCheck($index, "问诊:2026-10-23“胸痛”({$history})。", false, 'EXAM_SOURCE_QUOTE_INVALID');
evidenceContractCheck($index, "问诊:2026-10-23“记录头晕”({$history})。", false, 'EXAM_SOURCE_QUOTE_INVALID');
evidenceContractCheck($index, "问诊:2026-10-23“活动后汗出”(s999)。", false, 'SOURCE_ANNOTATION_INVALID');
evidenceContractCheck($index, '问诊:活动后汗出,气虚。', false, 'EXAM_FACT_PROTOCOL_INVALID');
evidenceContractCheck($index, "问诊:2026-10-23“活动后汗出”({$history}),可见气虚。", false, 'EXAM_FACT_PROTOCOL_INVALID');
evidenceContractCheck($index, "问诊:2026-10-23“活动后汗出”。", false, 'SOURCE_ANNOTATION_INVALID');
foreach (['望诊', '闻诊', '问诊', '舌象', '脉象'] as $role) evidenceContractCheck($index, $role . ':未记录。', false, 'EXAM_KNOWN_ROLE_TO_UNKNOWN');
evidenceContractCheck($index, '切诊:未记录。', true);
$withPalpation = $row;
$withPalpation['symptoms'] .= '切诊腹软,无压痛。';
$palpationIndex = Index::build(Guard::build($withPalpation));
evidenceContractCheck($palpationIndex, '切诊:其他触诊未记录,脉象见独立脉象项。', false, 'EXAM_KNOWN_ROLE_TO_UNKNOWN');
evidenceContractCheck($index, '问诊:2026-10-22“活动后汗出”(s90)。', false, 'EXAM_SOURCE_NOT_CURRENT');
evidenceContractCheck($index, '追踪:2026-10-22“活动后汗出”(s90)。', true);
foreach (['历史访谈', '历史问诊', '追踪资料'] as $historyLabel) {
evidenceContractCheck($index, $historyLabel . ':2026-10-22“活动后汗出”(s90)。', true);
evidenceContractCheck($index, $historyLabel . ':2026-10-23“活动后汗出”(s90)。', false, 'EVIDENCE_SOURCE_DATE_MISMATCH');
evidenceContractCheck($index, $historyLabel . ':2026-10-22“患者无头晕”(s90)。', false, 'EXAM_SOURCE_QUOTE_INVALID');
evidenceContractCheck($index, $historyLabel . ':2026-10-22“活动后汗出”(s90),本次望诊见自汗。', false, 'LONGITUDINAL_FACT_PROTOCOL_INVALID');
}
evidenceContractCheck($index, '追踪:2026-10-23“活动后汗出”(s90)。', false, 'EVIDENCE_SOURCE_DATE_MISMATCH');
evidenceContractCheck($index, "历史:2026-10-23“舌质淡红”({$tongue})。", false, 'LONGITUDINAL_SOURCE_NOT_HISTORICAL');
evidenceContractCheck($index, '追踪:2026-10-22“活动后汗出”(s90),本次望诊见自汗。', false, 'LONGITUDINAL_FACT_PROTOCOL_INVALID');
evidenceContractCheck($index, $first . "二、八纲\n证据不足,血红蛋白与出血史未记录。", true, '', true);
foreach (['无血色素、出血史及其他血虚表现', '本次无晕厥史', '2026-10-23无血红蛋白异常', '未见明确红绛', '没有头晕', '否认多尿', '不伴心悸'] as $denial) {
evidenceContractCheck($index, "二、八纲\n" . $denial . '。', false, 'UNQUOTED_PATIENT_DENIAL');
}
evidenceContractCheck($index, $first . "二、八纲\n2026-10-23“无胸痛”({$history})仅为记录,其他资料未记录。", true);
evidenceContractCheck($index, "二、八纲\n2026-10-22“无胸痛”({$history})。", false, 'EVIDENCE_SOURCE_DATE_MISMATCH');
evidenceContractCheck($index, "二、八纲\n“无头晕”({$history})。", false, 'DENIAL_SOURCE_QUOTE_INVALID');
evidenceContractCheck($index, '二、八纲' . "\n“无晕厥”(s999)。", false, 'DENIAL_SOURCE_QUOTE_INVALID');
evidenceContractCheck($index, $first . '二、八纲' . "\n核实汗出有无外邪未解,复核有无头晕。", true);
evidenceContractCheck($index, '二、八纲' . "\n核实有无头晕,患者无血红蛋白异常。", false, 'UNQUOTED_PATIENT_DENIAL');
evidenceContractCheck($index, $first . '二、八纲' . "\n无法判断,无论哪种情况均需复核;按之无力,无须重复同一资料。", true);
evidenceContractCheck($index, '二、八纲' . "\n不等于绝对无风险,患者无头晕。", false, 'UNQUOTED_PATIENT_DENIAL');
evidenceContractCheck($index, $first . '二、八纲' . "\n不等于绝对无风险。", true);
foreach (['阳证亦无完整独立证据。', '肾的定位无充分依据,需继续采集。', '无明确证据支持该推断。'] as $epistemic) {
evidenceContractCheck($index, $first . "二、八纲\n" . $epistemic, true);
}
foreach (['无完整独立证据,患者无头晕。', '无完整独立证据证明患者无头晕。',
'患者无胸痛,无完整独立证据。', '无心悸证据。', '无完整独立证据性皮疹。'] as $assertion) {
evidenceContractCheck($index, $first . "二、八纲\n" . $assertion, false, 'UNQUOTED_PATIENT_DENIAL');
}
$unknownLedger = Guard::build(['present_illness' => '无']);
$unknownIndex = Index::build($unknownLedger);
$unknownId = evidenceSourceId($unknownIndex, 'present_illness');
evidenceContractCheck($unknownIndex, "问诊:日期未记录“无”({$unknownId})。", false, 'EXAM_ROLE_MISMATCH');
evidenceContractCheck($unknownIndex, "二、八纲\n“无”({$unknownId})。", false, 'DENIAL_SOURCE_QUOTE_INVALID');
evidenceContractCheck($unknownIndex, "历史:日期未记录“无”({$unknownId})。", false, 'EXAM_SOURCE_QUOTE_INVALID');
$denialList = Index::build(Guard::build(['present_illness' => '否认:胸痛、心悸。']));
$denialListId = evidenceSourceId($denialList, 'present_illness');
evidenceContractCheck($denialList, "问诊:日期未记录“胸痛、心悸”({$denialListId})。", false, 'EXAM_SOURCE_QUOTE_INVALID');
evidenceContractCheck($denialList, "问诊:日期未记录“否认:胸痛、心悸。”({$denialListId})。", true);
evidenceContractCheck([], '', false, 'EVIDENCE_INDEX_UNAVAILABLE');
evidenceContractCheck($index, "\xC3\x28", false, 'EVIDENCE_INVALID_UTF8');
// Exact saved v42 content failures, retained as data instead of an expected syndrome.
$fixture = Index::build(Guard::build(CompleteTcmCase::current(), AiRequiredSource::submission(CompleteTcmCase::rows())));
$fixtureLedger = Guard::build(CompleteTcmCase::current(), AiRequiredSource::submission(CompleteTcmCase::rows()));
$neutralPrerequisite = '适用前须核实:复核自汗是否由低血糖、感染或其他急性原因引起。';
foreach ([$neutralPrerequisite => '',
$neutralPrerequisite . '患者无固定刺痛。' => 'UNKNOWN_TO_NEGATIVE',
$neutralPrerequisite . '且患者有心悸。' => 'NEGATIVE_TO_POSITIVE',
$neutralPrerequisite . '心悸未记录。' => 'NEGATIVE_TO_UNKNOWN',
'适用条件:患者无固定刺痛。' => 'UNKNOWN_TO_NEGATIVE',
'无急性感染诱因。' => 'NEGATION_SCOPE_WIDENED'] as $condition => $expectedCode) {
$check = Guard::validate($fixtureLedger, $condition);
++$count;
if ($check['valid'] !== ($expectedCode === '')
|| ($expectedCode !== '' && !in_array($expectedCode, array_column($check['issues'], 'code'), true))) {
throw new RuntimeException('Neutral future question changed a neighbouring patient assertion boundary.');
}
}
evidenceContractCheck($fixture, '一、四诊' . "\n望诊:2026-10-23见“神疲、面色少华”;症状逐渐出现。", false, 'EXAM_FACT_PROTOCOL_INVALID');
evidenceContractCheck($fixture, '一、四诊' . "\n切诊:本次症状随活动加重,结合自汗、气短提示卫外及推动功能偏弱;腹诊、皮肤温度、四肢温度等切诊细节未记录,不能补作正常。", false, 'EXAM_FACT_PROTOCOL_INVALID');
evidenceContractCheck($fixture, '三、气血津液与脏腑' . "\n血:面色少华、脉细可作血少鉴别线索,但无血色素、出血史及其他血虚表现,现有资料不足以确定血虚。", false, 'UNQUOTED_PATIENT_DENIAL');
evidenceContractCheck($fixture, '六、中药方药及复核监测' . "\n反证与局限:舌质仅为淡红,未见明确红绛;虚热症状证据不足,故“阴虚”程度需复核。", false, 'UNQUOTED_PATIENT_DENIAL');
// Exact rejected v43 model unit: the common historical interview label is
// metadata only. Its original date/quote/source still need verification.
$fixtureRows = CompleteTcmCase::rows();
$fixtureRows['chat_records'] = ['tencent_im' => $fixtureRows['im_messages'], 'wechat_work' => []];
$fixtureWithChat = Index::build(Guard::build(CompleteTcmCase::current(), AiRequiredSource::submission($fixtureRows)));
$fixtureChat = evidenceSourceId($fixtureWithChat, 'text', 'chat.');
evidenceContractCheck($fixtureWithChat, '历史访谈:2026-10-23“合成访谈:活动后出汗和气短明显,喝水每次不多,没有最近高热、恶寒、腹泻。”(' . $fixtureChat . ')。', true);
// Structural provenance does not certify the medical explanatory bridge.
evidenceContractCheck($index, $first . "四、证名\n推断:用已引来源支持任意不相干的证名。", true, '', true);
// Simulated paragraph release checks at actual byte boundaries; the validator does no forwarding.
foreach ([1, 2, 7, 31] as $size) {
$pending = ''; $accepted = '';
foreach (str_split($first, $size) as $chunk) {
$pending .= $chunk;
while (preg_match('/(?:\r?\n){2}/', $pending, $boundary, PREG_OFFSET_CAPTURE)) {
$end = $boundary[0][1] + strlen($boundary[0][0]);
$unit = substr($pending, 0, $end);
$pending = substr($pending, $end);
evidenceContractCheck($index, $accepted . $unit, true);
$accepted .= $unit;
}
}
++$count;
if ($accepted . $pending !== $first) throw new RuntimeException('Incremental original bytes changed.');
evidenceContractCheck($index, $accepted . $pending, true, '', true);
evidenceContractCheck($index, $accepted . "二、八纲\n患者无头晕。\n\n", false, 'UNQUOTED_PATIENT_DENIAL');
}
// A comma can continue a denial list. It is not a safe quote boundary.
$commaIndex = Index::build(Guard::build(['diagnosis_date' => '2026-10-23',
'present_illness' => '无明显胸痛,心悸;口干,少量频饮。']));
$commaSource = evidenceSourceId($commaIndex, 'present_illness');
foreach (['心悸', '明显胸痛', '无明显胸痛', '口干', '少量频饮'] as $cut) {
evidenceContractCheck($commaIndex, "问诊:2026-10-23“{$cut}”({$commaSource})。", false, 'EXAM_SOURCE_QUOTE_INVALID');
}
foreach (['无明显胸痛,心悸', '口干,少量频饮'] as $whole) {
evidenceContractCheck($commaIndex, "问诊:2026-10-23“{$whole}”({$commaSource})。", true);
}
evidenceContractCheck($index, "一、四诊\n\n望诊:2026-10-23“神疲、面色少华”({$mixed})。\n\n二、八纲\n\n",
false, 'EXAM_ROLE_MISSING');
// Independently reproduced bypass: entering a later heading must fail before
// either that heading or its unsupported patient observation reaches forward.
foreach (['二、八纲', '三、气血津液与脏腑', '六、中药方药及复核监测'] as $laterHeading) {
$wire = $laterHeading . "\n\n本次新见结节肿块。\n\n";
evidenceContractCheck($index, $wire, false, 'EXAM_ROLE_MISSING');
foreach ([1, 2, 7, 31] as $size) {
$visible = '';
$stream = new \app\adminapi\service\TcmClinicalReportStream($ledger,
static function (string $delta) use (&$visible): bool { $visible .= $delta; return true; },
null, '', null, $index);
foreach (str_split($wire, $size) as $chunk) if (!$stream->push($chunk)) break;
++$count;
if (!$stream->rejected() || $visible !== '' || $stream->finish()
|| !in_array('EXAM_ROLE_MISSING', array_column($stream->issues(), 'code'), true)) {
throw new RuntimeException('Later-section entry bypassed the actual pre-forward role gate.');
}
}
}
// Even all six quoted role fragments do not establish an omitted first heading.
$headinglessFirst = substr($first, strlen("一、四诊\n\n"));
evidenceContractCheck($index, $headinglessFirst . "二、八纲\n\n", false, 'EXAM_ROLE_MISSING');
evidenceContractCheck($index, $headinglessFirst, false, 'EXAM_ROLE_MISSING', true);
// Exercise the actual shared stream gate, not a simulated forwarding loop.
foreach (['无完整独立证据且患者有胸痛。' => 'NEGATIVE_TO_POSITIVE',
'无完整独立证据,患者无头晕。' => 'UNKNOWN_TO_NEGATIVE',
'无完整独立证据,胸痛未记录。' => 'NEGATIVE_TO_UNKNOWN'] as $adjacent => $expectedCode) {
foreach ([1, 31] as $size) {
$visible = '';
$stream = new \app\adminapi\service\TcmClinicalReportStream($ledger,
static function (string $delta) use (&$visible): bool { $visible .= $delta; return true; },
null, '', null, $index);
$prefix = $first . "二、八纲\n\n";
foreach (str_split($prefix, $size) as $chunk) {
if (!$stream->push($chunk)) throw new RuntimeException('Valid source frame rejected before adjacent assertion test.');
}
foreach (str_split($adjacent . "\n\n", $size) as $chunk) if (!$stream->push($chunk)) break;
++$count;
if (!$stream->rejected() || $visible !== $prefix
|| !in_array($expectedCode, array_column($stream->issues(), 'code'), true)) {
throw new RuntimeException('Evidence-absence noun masked an adjacent actual patient assertion.');
}
}
}
foreach ([1, 2, 7, 31] as $size) {
$visible = '';
$stream = new \app\adminapi\service\TcmClinicalReportStream($ledger,
static function (string $delta) use (&$visible): bool { $visible .= $delta; return true; },
null, '', null, $index);
foreach (str_split($first, $size) as $chunk) {
++$count;
if (!$stream->push($chunk)) throw new RuntimeException('Valid protocol rejected before original bytes forward.');
}
++$count;
if ($visible !== $first) throw new RuntimeException('Shared stream changed accepted role protocol bytes.');
$badRole = "切诊:2026-10-23“活动后汗出”({$history})。\n\n";
foreach (str_split($badRole, $size) as $chunk) if (!$stream->push($chunk)) break;
++$count;
if (!$stream->rejected() || $visible !== $first
|| !in_array('EXAM_ROLE_MISMATCH', array_column($stream->issues(), 'code'), true)) {
throw new RuntimeException('A source-backed history cannot leak through as palpation.');
}
}
// Heading annotations do not change the acquisition role or patient facts.
foreach (['已记录', '补充', '(已记录)', '(补充)', '已记录补充', '(已记录补充)'] as $metadata) {
evidenceContractCheck($index, "问诊{$metadata}:2026-10-23“活动后汗出”({$history})。", true);
evidenceContractCheck($index, "望诊{$metadata}:2026-10-23“活动后汗出”({$history})。", false, 'EXAM_ROLE_MISMATCH');
}
foreach (['阴性', '(阴性)', '(阴性)', '已记录阴性', '阴性补充', '补充(阴性)', '(已记录阴性)', '已记录(阴性)补充'] as $metadata) {
evidenceContractCheck($index, "问诊{$metadata}:2026-10-23“无胸痛”({$history})。", true);
evidenceContractCheck($index, "望诊{$metadata}:2026-10-23“无胸痛”({$history})。", false, 'EXAM_ROLE_MISMATCH');
evidenceContractCheck($index, "问诊{$metadata}:2026-10-22“无胸痛”({$history})。", false, 'EVIDENCE_SOURCE_DATE_MISMATCH');
evidenceContractCheck($index, "问诊{$metadata}:2026-10-23“无胸痛”(s999)。", false, 'SOURCE_ANNOTATION_INVALID');
evidenceContractCheck($index, "问诊{$metadata}:2026-10-23“无胸痛”({$history}),“活动后汗出”({$history})。", false, 'EXAM_NEGATIVE_HEADING_UNSUPPORTED');
evidenceContractCheck($index, "问诊{$metadata}:2026-10-23“无胸痛”({$history}),患者无头晕。", false, 'EXAM_FACT_PROTOCOL_INVALID');
}
foreach (['阳性', '(阳性)', '正常', '(正常)', '已记录正常', '正常补充', '(阴性正常)', '阴性阳性',
'阴性阴性', '((阴性))', '(阴性', '阴性)', '(阴性,未记录)', '其他', '无异常'] as $metadata) {
evidenceContractCheck($index, "问诊{$metadata}:2026-10-23“无胸痛”({$history})。", false, 'EXAM_HEADING_METADATA_UNSUPPORTED');
}
evidenceContractCheck($index, '切诊阴性:未记录。', false, 'EXAM_NEGATIVE_HEADING_UNSUPPORTED');
evidenceContractCheck($index, '切诊(阴性):其他触诊未记录,脉象见独立脉象项。', false, 'EXAM_NEGATIVE_HEADING_UNSUPPORTED');
evidenceContractCheck($unknownIndex, "问诊阴性:日期未记录“无”({$unknownId})。", false, 'EXAM_NEGATIVE_HEADING_UNSUPPORTED');
evidenceContractCheck($index, "脉象阴性:2026-10-23“脉细弱,按之无力。”({$pulse})。", false, 'EXAM_NEGATIVE_HEADING_UNSUPPORTED');
evidenceContractCheck($index, "问诊阴性:2026-10-22“活动后汗出。无晕厥。”(s90)。", false, 'EXAM_SOURCE_NOT_CURRENT');
evidenceContractCheck($index, "问诊阴性:2026-10-23“无明显胸痛”({$history})。", false, 'EXAM_SOURCE_QUOTE_INVALID');
evidenceContractCheck($index, "问诊(阴性)\n2026-10-23“无胸痛”({$history})。\n“活动后汗出”({$history})。", false, 'EXAM_NEGATIVE_HEADING_UNSUPPORTED');
$generalLedger = Guard::build(['diagnosis_date' => '2026-10-23', 'present_illness' => '食欲一般;无胸痛。', 'tongue' => '舌象正常。']);
$generalIndex = Index::build($generalLedger);
$generalSource = evidenceSourceId($generalIndex, 'present_illness');
$normalTongue = evidenceSourceId($generalIndex, 'tongue');
evidenceContractCheck($generalIndex, "问诊阴性:2026-10-23“食欲一般”({$generalSource})。", false, 'EXAM_NEGATIVE_HEADING_UNSUPPORTED');
evidenceContractCheck($generalIndex, "问诊阴性:2026-10-23“食欲一般;无胸痛。”({$generalSource})。", true);
evidenceContractCheck($generalIndex, "问诊阳性:2026-10-23“食欲一般”({$generalSource})。", false, 'EXAM_HEADING_METADATA_UNSUPPORTED');
evidenceContractCheck($generalIndex, "舌象正常:2026-10-23“舌象正常。”({$normalTongue})。", false, 'EXAM_HEADING_METADATA_UNSUPPORTED');
evidenceContractCheck($generalIndex, "舌象:2026-10-23“舌象正常。”({$normalTongue})。", true);
// Use the production pure snapshot projection and sanitization, preserving
// the exact sN mapping of the saved v43r4 rejected model units.
$savedSnapshot = (new ReflectionMethod(\app\adminapi\logic\tcm\PatientAiReportLogic::class, 'buildSourceSnapshotFromRows'))
->invoke(null, CompleteTcmCase::rows());
$savedSubmission = (new ReflectionMethod(\app\adminapi\logic\tcm\PatientAiReportLogic::class, 'sanitizeSnapshotForUpstream'))
->invoke(null, AiRequiredSource::submission($savedSnapshot));
$savedLedger = Guard::build(CompleteTcmCase::current(), $savedSubmission);
$savedIndex = Index::build($savedLedger);
$savedUnits = [
'问诊阴性:2026-10-23“无恶寒发热,无高热烦渴、汗出淋漓或意识异常。”(s2)。“无明显五心烦热、潮热盗汗,无畏寒肢冷。”(s2)。“无咳嗽咯痰、胸痛心悸、浮肿或腹泻。”(s2)。“合成病史:2型糖尿病4年;无已知冠心病、心衰或慢性肾病。”(s4)。' . "\n\n",
'问诊阴性:2026-10-23“无恶寒发热,无高热烦渴、汗出淋漓或意识异常。”(s2)。“无明显五心烦热、潮热盗汗,无畏寒肢冷。”(s2)。“无咳嗽咯痰、胸痛心悸、浮肿或腹泻。”(s2)。“合成病史:2型糖尿病4年;无已知冠心病、心衰或慢性肾病。”(s4)。“本次询问无已知药物过敏史”(s5)。' . "\n\n",
];
$savedRejectionsPath = dirname(__DIR__, 2) . '/app/artifacts/ai_concurrency/tcm_complete_v43r4_single_openai_20261024_0_openai.rejected.json';
if (is_file($savedRejectionsPath)) {
$savedRejections = json_decode((string) file_get_contents($savedRejectionsPath), true);
++$count;
if (array_column($savedRejections['rejections'] ?? [], 'unit') !== $savedUnits) throw new RuntimeException('Portable v43r4 unit bytes differ from the retained original artifact.');
}
$savedPrefix = "一、四诊\n\n望诊:2026-10-23“神疲、面色少华”(s3)。\n\n闻诊:2026-10-23“语声低微,无喘促及异常气味”(s3)。\n\n"
. '问诊:2026-10-23“乏力、动则气短、自汗、口干3个月。”(s1)。“合成接诊记录:神疲乏力,轻微活动后气短,白天自汗、活动后加重,口干欲饮、少量频饮。”(s2)。“食欲一般,大便稍干、每日1次,小便量较以往略多、夜尿1次。”(s2)。“此次症状逐渐出现,无明显急性感染诱因;睡眠一般,未记录腰酸耳鸣或固定刺痛。”(s2)。' . "\n\n";
foreach ($savedUnits as $savedUnit) {
evidenceContractCheck($savedIndex, $savedPrefix . $savedUnit, true);
$savedOriginal = $savedUnit;
foreach ([1, 2, 7, 31] as $size) {
$visible = '';
$stream = new \app\adminapi\service\TcmClinicalReportStream($savedLedger,
static function (string $delta) use (&$visible): bool { $visible .= $delta; return true; },
null, '', null, $savedIndex);
foreach (str_split($savedPrefix . $savedUnit, $size) as $chunk) {
if (!$stream->push($chunk)) throw new RuntimeException('Exact v43r4 negative subgroup failed the actual pre-forward gate.');
}
++$count;
if (!$stream->finish() || $stream->rejected() || $visible !== $savedPrefix . $savedOriginal
|| $stream->acceptedContent() !== $visible || $savedUnit !== $savedOriginal) {
throw new RuntimeException('Heading compatibility changed original source/report bytes.');
}
}
}
$badHeadingUnits = [
'望诊阴性:2026-10-23“无胸痛”(' . $history . ')。' . "\n\n" => 'EXAM_ROLE_MISMATCH',
'问诊阴性:2026-10-22“无胸痛”(' . $history . ')。' . "\n\n" => 'EVIDENCE_SOURCE_DATE_MISMATCH',
'问诊阴性:2026-10-23“活动后汗出”(' . $history . ')。' . "\n\n" => 'EXAM_NEGATIVE_HEADING_UNSUPPORTED',
'脉象阴性:2026-10-23“脉细弱,按之无力。”(' . $pulse . ')。' . "\n\n" => 'EXAM_NEGATIVE_HEADING_UNSUPPORTED',
'问诊阴性:2026-10-23“无胸痛”(' . $history . '),患者无头晕。' . "\n\n" => '',
'问诊正常:2026-10-23“无胸痛”(' . $history . ')。' . "\n\n" => 'EXAM_HEADING_METADATA_UNSUPPORTED',
];
foreach ($badHeadingUnits as $unit => $expectedCode) foreach ([1, 2, 7, 31] as $size) {
$visible = '';
$stream = new \app\adminapi\service\TcmClinicalReportStream($ledger,
static function (string $delta) use (&$visible): bool { $visible .= $delta; return true; },
null, '', null, $index);
foreach (str_split($first, $size) as $chunk) if (!$stream->push($chunk)) throw new RuntimeException('Valid prefix failed in heading adversarial test.');
foreach (str_split($unit, $size) as $chunk) if (!$stream->push($chunk)) break;
++$count;
if (!$stream->rejected() || $stream->finish() || $visible !== $first
|| ($expectedCode !== '' && !in_array($expectedCode, array_column($stream->issues(), 'code'), true))) {
throw new RuntimeException('Heading metadata waived or forwarded an invalid role/date/polarity/adjacent assertion.');
}
}
// Bounded diagnostic evidence absence is distinct from a patient-feature absence.
$evidenceAbsencePhrases = ['现有资料没有表证的独立阳性依据。', '没有气虚证的充分证据。',
'没有寒热属性判断的明确依据。', '没有脏腑定位的直接依据。', '当前没有辨证结论的独立依据。',
'没有证型的依据。', '没有表证的独立阳性依据支持这一推断。'];
foreach ($evidenceAbsencePhrases as $phrase) evidenceContractCheck($index, $first . "二、八纲\n" . $phrase, true);
foreach (['没有口干的证据。', '没有舌红的独立阳性依据。', '患者没有胸痛。', '没有心悸证据。',
'没有患者无口干的表证的独立阳性依据。', '没有患者有胸痛诊断的充分依据。',
'没有存在胸痛证的独立依据。', '没有口干属于热证的明确证据。', '没有是否表证的依据。',
'没有表证的独立阳性依据性皮疹。', '没有表证的独立阳性依据无头晕。',
'没有表证的证据证明患者无头晕。', '没有表证的独立阳性依据,患者无头晕。',
'没有表证的独立阳性依据,否认头晕。', '没有表证的依据,未见红绛。'] as $actualAssertion) {
evidenceContractCheck($index, $first . "二、八纲\n" . $actualAssertion, false, 'UNQUOTED_PATIENT_DENIAL');
}
$evidenceAbsenceHead = '现有资料没有表证的独立阳性依据;';
evidenceContractCheck($index, $first . "二、八纲\n" . $evidenceAbsenceHead . '“无胸痛”(s999)。', false, 'DENIAL_SOURCE_QUOTE_INVALID');
evidenceContractCheck($index, $first . "二、八纲\n" . $evidenceAbsenceHead . '“无胸痛”(' . $mixed . ')。', false, 'DENIAL_SOURCE_QUOTE_INVALID');
evidenceContractCheck($index, $first . "二、八纲\n" . $evidenceAbsenceHead . '2026-10-22“无胸痛”(' . $history . ')。', false, 'EVIDENCE_SOURCE_DATE_MISMATCH');
evidenceContractCheck($index, $first . "二、八纲\n" . $evidenceAbsenceHead . '2026-10-23“无胸痛”(' . $history . ')。', true);
// Exact v43r7 units: the extra first-section prose remains rejected; the
// evidence-noun statement after the complete first section is accepted intact.
$r7Units = [
'问诊:2026-10-23“此次症状逐渐出现,无明显急性感染诱因;睡眠一般,未记录腰酸耳鸣或固定刺痛。”(s2)。婚姻、体重变化及详细饮水总量未记录。' . "\n\n",
'表里:现有资料没有表证的独立阳性依据;症状持续3个月并涉及汗、饮、便、尿(s1、s2),提示内在功能失调倾向,但定位为里证仍需结合病程及脏腑证据复核。' . "\n\n",
];
$r7RejectionsPath = dirname(__DIR__, 2) . '/app/artifacts/ai_concurrency/tcm_complete_v43r7_single_openai_20261024_0_openai.rejected.json';
if (is_file($r7RejectionsPath)) {
$r7Rejections = json_decode((string) file_get_contents($r7RejectionsPath), true);
++$count;
if (array_column($r7Rejections['rejections'] ?? [], 'unit') !== $r7Units) throw new RuntimeException('Portable v43r7 units differ from the retained original bytes.');
}
$savedTongueId = evidenceSourceId($savedIndex, 'tongue');
$savedPulseId = evidenceSourceId($savedIndex, 'pulse');
$r7First = $savedPrefix . "切诊:其他触诊未记录,脉象见独立脉象项。\n\n"
. '舌象:2026-10-23“' . $savedIndex['sources'][$savedTongueId]['quote'] . '”(' . $savedTongueId . ')。' . "\n\n"
. '脉象:2026-10-23“' . $savedIndex['sources'][$savedPulseId]['quote'] . '”(' . $savedPulseId . ')。' . "\n\n";
evidenceContractCheck($savedIndex, $savedPrefix . $r7Units[0], false, 'EXAM_FACT_PROTOCOL_INVALID');
evidenceContractCheck($savedIndex, $r7First . "二、八纲\n\n" . $r7Units[1], true);
evidenceContractCheck($savedIndex, $savedPrefix . $r7Units[1], false, 'EXAM_FACT_PROTOCOL_INVALID');
foreach ([1, 2, 7, 31] as $size) {
$visible = '';
$stream = new \app\adminapi\service\TcmClinicalReportStream($savedLedger,
static function (string $delta) use (&$visible): bool { $visible .= $delta; return true; },
null, '', null, $savedIndex);
foreach (str_split($savedPrefix, $size) as $chunk) if (!$stream->push($chunk)) throw new RuntimeException('Saved first-section prefix rejected.');
foreach (str_split($r7Units[0], $size) as $chunk) if (!$stream->push($chunk)) break;
++$count;
if (!$stream->rejected() || $stream->finish() || $visible !== $savedPrefix
|| !in_array('EXAM_FACT_PROTOCOL_INVALID', array_column($stream->issues(), 'code'), true)) {
throw new RuntimeException('Evidence-noun correction weakened the first-section quote-only boundary.');
}
$visible = '';
$wire = $r7First . "二、八纲\n\n" . $r7Units[1];
$stream = new \app\adminapi\service\TcmClinicalReportStream($savedLedger,
static function (string $delta) use (&$visible): bool { $visible .= $delta; return true; },
null, '', null, $savedIndex);
foreach (str_split($wire, $size) as $chunk) if (!$stream->push($chunk)) throw new RuntimeException('Exact diagnostic evidence-absence unit rejected before forward.');
++$count;
if (!$stream->finish() || $stream->rejected() || $visible !== $wire || $stream->acceptedContent() !== $wire) {
throw new RuntimeException('Diagnostic evidence-absence compatibility changed original report bytes.');
}
}
// The source-state guard still sees all neighbouring patient predicates.
$absenceAdjacentUnits = [
$evidenceAbsenceHead . '患者有胸痛。' => 'NEGATIVE_TO_POSITIVE',
$evidenceAbsenceHead . '患者无口干。' => 'POSITIVE_TO_NEGATIVE',
$evidenceAbsenceHead . '患者无头晕。' => 'UNKNOWN_TO_NEGATIVE',
$evidenceAbsenceHead . '胸痛未记录。' => 'NEGATIVE_TO_UNKNOWN',
$evidenceAbsenceHead . '舌质红绛。' => 'TONGUE_FEATURE_NOT_IN_CURRENT_SOURCE',
$evidenceAbsenceHead . '脉滑。' => 'PULSE_FEATURE_NOT_IN_CURRENT_SOURCE',
];
foreach ($absenceAdjacentUnits as $unit => $expectedCode) foreach ([1, 2, 7, 31] as $size) {
$visible = '';
$prefix = $first . "二、八纲\n\n";
$stream = new \app\adminapi\service\TcmClinicalReportStream($ledger,
static function (string $delta) use (&$visible): bool { $visible .= $delta; return true; },
null, '', null, $index);
foreach (str_split($prefix, $size) as $chunk) if (!$stream->push($chunk)) throw new RuntimeException('Valid prefix failed the adjacent evidence-absence test.');
foreach (str_split($unit . "\n\n", $size) as $chunk) if (!$stream->push($chunk)) break;
++$count;
if (!$stream->rejected() || $stream->finish() || $visible !== $prefix
|| !in_array($expectedCode, array_column($stream->issues(), 'code'), true)) {
throw new RuntimeException('Diagnostic evidence absence waived a neighbouring source-state or examination assertion.');
}
}
// Diagnostic lists have a positive noun grammar, not a blacklist of the
// current fixture's symptoms. Modifiers may sit on either side of the list.
$diagnosticLists = ['阳虚、阴虚或阳亢', '气虚与血虚', '气阴两虚及阴阳两虚',
'表证、热证与阴虚火旺证', '脏腑定位以及辨证结论'];
foreach (['无', '没有'] as $operator) foreach (['', '足够', '完整独立'] as $beforeList)
foreach (['', '独立', '充分阳性'] as $afterList) foreach ($diagnosticLists as $list) {
evidenceContractCheck($index, $first . "二、八纲\n现有资料" . $operator . $beforeList . $list . '的' . $afterList . '证据。', true);
}
evidenceContractCheck($index, $first . "二、八纲\n没有足够气虚或血虚独立证据。", true);
evidenceContractCheck($index, $first . "二、八纲\n无独立阴虚依据。", true);
$listEvidenceHead = '且无足够阳虚、阴虚或阳亢的独立证据';
foreach (['无足够口干或舌红的独立证据。', '没有口干、阳虚的独立证据。',
'无足够阴虚、舌红或阳亢的证据。', '无阳虚、没有口干的证据。',
'无足够阳虚、患者无口干证的独立证据。', '没有阴虚、患者有胸痛诊断的证据。',
'无足够阴虚与存在胸痛证的独立证据。', '没有是否阴虚或阳虚的证据。',
$listEvidenceHead . '性皮疹。', $listEvidenceHead . '证明患者无头晕。',
$listEvidenceHead . ',患者无口干。', $listEvidenceHead . '且否认头晕。',
'无' . implode('、', array_fill(0, 9, '阳虚')) . '的独立证据。',
'没有' . str_repeat('甲', 17) . '证的独立依据。'] as $notEvidenceOnly) {
evidenceContractCheck($index, $first . "二、八纲\n" . $notEvidenceOnly, false, 'UNQUOTED_PATIENT_DENIAL');
}
evidenceContractCheck($index, $first . "二、八纲\n" . $listEvidenceHead . ';“无胸痛”(s999)。', false, 'DENIAL_SOURCE_QUOTE_INVALID');
evidenceContractCheck($index, $first . "二、八纲\n" . $listEvidenceHead . ';“无胸痛”(' . $mixed . ')。', false, 'DENIAL_SOURCE_QUOTE_INVALID');
evidenceContractCheck($index, $first . "二、八纲\n" . $listEvidenceHead . ';2026-10-22“无胸痛”(' . $history . ')。', false, 'EVIDENCE_SOURCE_DATE_MISMATCH');
evidenceContractCheck($index, $first . "二、八纲\n" . $listEvidenceHead . ';本次“无晕厥”(s90)。', false, 'EXAM_SOURCE_NOT_CURRENT');
$r9EpistemicUnit = '阴阳:本次虚象较明确,但虚证不能单独推出阴证。寒热均未定,且无足够阳虚、阴虚或阳亢的独立证据,故阴阳总纲现有资料不足以确定。' . "\n\n";
$r9RejectionPath = dirname(__DIR__, 2) . '/app/artifacts/ai_concurrency/tcm_complete_v43r9_single_openai_20261024_0_openai.rejected.json';
if (is_file($r9RejectionPath)) {
$r9Rejections = json_decode((string) file_get_contents($r9RejectionPath), true);
++$count;
if (($r9Rejections['rejections'][0]['unit'] ?? '') !== $r9EpistemicUnit) throw new RuntimeException('Portable r9 evidence-list unit differs from the original saved bytes.');
}
evidenceContractCheck($savedIndex, $r7First . "二、八纲\n\n" . $r9EpistemicUnit, true);
foreach ([1, 2, 7, 31] as $size) {
$visible = '';
$wire = $r7First . "二、八纲\n\n" . $r9EpistemicUnit;
$stream = new \app\adminapi\service\TcmClinicalReportStream($savedLedger,
static function (string $delta) use (&$visible): bool { $visible .= $delta; return true; },
null, '', null, $savedIndex);
foreach (str_split($wire, $size) as $chunk) if (!$stream->push($chunk)) throw new RuntimeException('Exact r9 evidence-list unit rejected before forward.');
++$count;
if (!$stream->finish() || $stream->rejected() || $visible !== $wire || $stream->acceptedContent() !== $wire) {
throw new RuntimeException('The bounded evidence-list grammar changed original report bytes.');
}
}
// Every adjacent patient assertion remains visible to the shared source-state
// guard. This is deliberately a full stream check, not just the noun grammar.
$listAdjacent = [
$listEvidenceHead . ',患者有胸痛。' => 'NEGATIVE_TO_POSITIVE',
$listEvidenceHead . ',患者无口干。' => 'POSITIVE_TO_NEGATIVE',
$listEvidenceHead . ',患者无头晕。' => 'UNKNOWN_TO_NEGATIVE',
$listEvidenceHead . ',胸痛未记录。' => 'NEGATIVE_TO_UNKNOWN',
$listEvidenceHead . ',舌质红绛。' => 'TONGUE_FEATURE_NOT_IN_CURRENT_SOURCE',
$listEvidenceHead . ',脉滑。' => 'PULSE_FEATURE_NOT_IN_CURRENT_SOURCE',
];
foreach ($listAdjacent as $unit => $expectedCode) foreach ([1, 2, 7, 31] as $size) {
$visible = '';
$prefix = $first . "二、八纲\n\n";
$stream = new \app\adminapi\service\TcmClinicalReportStream($ledger,
static function (string $delta) use (&$visible): bool { $visible .= $delta; return true; },
null, '', null, $index);
foreach (str_split($prefix, $size) as $chunk) if (!$stream->push($chunk)) throw new RuntimeException('Valid prefix failed in evidence-list adjacency test.');
foreach (str_split($unit . "\n\n", $size) as $chunk) if (!$stream->push($chunk)) break;
++$count;
if (!$stream->rejected() || $stream->finish() || $visible !== $prefix
|| !in_array($expectedCode, array_column($stream->issues(), 'code'), true)) {
throw new RuntimeException('The evidence-list noun phrase waived an adjoining patient assertion.');
}
}
evidenceContractMetaChecks($index, $first, $history, $mixed);
// The saved r10 units are data, not a preferred clinical conclusion. The
// production fixture projection above supplies the original source index.
$r10Units = [
'tcm_complete_v43r10_single_openai_20261024_0_openai.rejected.json' => [0,
'本例支持点:气短、乏力、自汗与脉弱构成气虚主轴(s1、s10);口干欲饮、少量频饮及舌少津构成津伤兼证(s2、s9)。该方向可同时覆盖益气、生津、敛汗,但对脏腑定位并无额外证明作用。' . "\n\n"],
'tcm_complete_v43r10_supplement_ooc2_20261024_1_openai.rejected.json' => [0,
'虚实:神疲、动则气短、自汗、语声低微、面色少华及脉细弱无力(s1、s2、s3、s10)共同支持虚证。尚无足以确立痰、湿、瘀、食积等实邪的阳性证据,不能据此写成“无实邪”。' . "\n\n"],
];
foreach ($r10Units as $file => [$unitIndex, $unit]) {
$path = dirname(__DIR__, 2) . '/app/artifacts/ai_concurrency/' . $file;
if (is_file($path)) {
$saved = json_decode((string) file_get_contents($path), true);
++$count;
if (($saved['rejections'][$unitIndex]['unit'] ?? '') !== $unit) throw new RuntimeException('Portable r10 unit differs from retained original bytes.');
}
evidenceContractCheck($savedIndex, $r7First . "二、八纲\n\n" . $unit, true);
foreach ([1, 2, 7, 31] as $size) {
$visible = '';
$wire = $r7First . "二、八纲\n\n" . $unit;
$stream = new \app\adminapi\service\TcmClinicalReportStream($savedLedger,
static function (string $delta) use (&$visible): bool { $visible .= $delta; return true; },
null, '', null, $savedIndex);
foreach (str_split($wire, $size) as $chunk) if (!$stream->push($chunk)) throw new RuntimeException('Exact r10 evidential/meta unit rejected before forward.');
++$count;
if (!$stream->finish() || $stream->rejected() || $visible !== $wire || $stream->acceptedContent() !== $wire) {
throw new RuntimeException('The evidential/metalinguistic grammar changed original r10 report bytes.');
}
}
}
$metaAdjacentHeads = ['对脏腑定位并无额外证明作用',
'尚无足以确立痰、湿、瘀、食积等实邪的阳性证据', '不能据此写成“无实邪”'];
foreach ($metaAdjacentHeads as $metaHead) foreach ([
'患者有胸痛。' => 'NEGATIVE_TO_POSITIVE', '患者无口干。' => 'POSITIVE_TO_NEGATIVE',
'患者无头晕。' => 'UNKNOWN_TO_NEGATIVE', '胸痛未记录。' => 'NEGATIVE_TO_UNKNOWN',
'舌质红绛。' => 'TONGUE_FEATURE_NOT_IN_CURRENT_SOURCE', '脉滑。' => 'PULSE_FEATURE_NOT_IN_CURRENT_SOURCE',
] as $tail => $expectedCode) foreach ([1, 2, 7, 31] as $size) {
$visible = '';
$prefix = $first . "二、八纲\n\n";
$stream = new \app\adminapi\service\TcmClinicalReportStream($ledger,
static function (string $delta) use (&$visible): bool { $visible .= $delta; return true; },
null, '', null, $index);
foreach (str_split($prefix, $size) as $chunk) if (!$stream->push($chunk)) throw new RuntimeException('Valid prefix failed in r10 adjacency control.');
foreach (str_split($metaHead . ',' . $tail . "\n\n", $size) as $chunk) if (!$stream->push($chunk)) break;
++$count;
if (!$stream->rejected() || $stream->finish() || $visible !== $prefix
|| !in_array($expectedCode, array_column($stream->issues(), 'code'), true)) {
throw new RuntimeException('An evidential/metalinguistic phrase waived an adjacent patient assertion.');
}
}
evidenceContractInferenceChecks($index, $first, $history, $mixed);
$r13Path = dirname(__DIR__, 2) . '/app/artifacts/ai_concurrency/tcm_complete_v43r12_openai_deadline_20261024_openai_next_full90_openai.rejected.json';
if (is_file($r13Path)) {
$r13Saved = json_decode((string) file_get_contents($r13Path), true);
++$count;
if (array_column($r13Saved['rejections'] ?? [], 'unit') !== evidenceContractR13Units()) {
throw new RuntimeException('Portable r13 units differ from retained original failure bytes.');
}
}
foreach (evidenceContractR13Units() as $unit) {
evidenceContractCheck($savedIndex, $r7First . "二、八纲\n\n" . $unit, true);
foreach ([1, 2, 7, 31] as $size) {
$wire = $r7First . "二、八纲\n\n" . $unit;
$visible = '';
$stream = new \app\adminapi\service\TcmClinicalReportStream($savedLedger,
static function (string $delta) use (&$visible): bool { $visible .= $delta; return true; },
null, '', null, $savedIndex);
foreach (str_split($wire, $size) as $chunk) if (!$stream->push($chunk)) throw new RuntimeException('Exact r13 epistemic unit rejected before forward.');
++$count;
if (!$stream->finish() || $stream->rejected() || $visible !== $wire || $stream->acceptedContent() !== $wire) {
throw new RuntimeException('Abstract epistemic/inference handling changed original r13 report bytes.');
}
}
}
foreach (['寒证亦无独立阳性寒象', '不能据此反推纯虚无实'] as $epistemic) foreach ([
'患者有胸痛。' => 'NEGATIVE_TO_POSITIVE', '患者无口干。' => 'POSITIVE_TO_NEGATIVE',
'患者无头晕。' => 'UNKNOWN_TO_NEGATIVE', '胸痛未记录。' => 'NEGATIVE_TO_UNKNOWN',
'舌质红绛。' => 'TONGUE_FEATURE_NOT_IN_CURRENT_SOURCE', '脉滑。' => 'PULSE_FEATURE_NOT_IN_CURRENT_SOURCE',
] as $tail => $expectedCode) foreach ([1, 2, 7, 31] as $size) {
$visible = '';
$prefix = $first . "二、八纲\n\n";
$stream = new \app\adminapi\service\TcmClinicalReportStream($ledger,
static function (string $delta) use (&$visible): bool { $visible .= $delta; return true; },
null, '', null, $index);
foreach (str_split($prefix, $size) as $chunk) if (!$stream->push($chunk)) throw new RuntimeException('Valid prefix failed in r13 assertion-scope control.');
foreach (str_split($epistemic . ',' . $tail . "\n\n", $size) as $chunk) if (!$stream->push($chunk)) break;
++$count;
if (!$stream->rejected() || $stream->finish() || $visible !== $prefix
|| !in_array($expectedCode, array_column($stream->issues(), 'code'), true)) {
throw new RuntimeException('An abstract epistemic/inference role waived a neighbouring patient assertion.');
}
}
evidenceContractFutureConditionChecks($index, $first, $history, $mixed);
$r14Path = dirname(__DIR__, 2) . '/app/artifacts/ai_concurrency/tcm_complete_v43r13_single_openai_20261024_0_openai.rejected.json';
if (is_file($r14Path)) {
$r14Saved = json_decode((string) file_get_contents($r14Path), true);
$r14Unit = $r14Saved['rejections'][1]['unit'] ?? '';
++$count;
if (!is_string($r14Unit) || !str_contains($r14Unit, evidenceContractR14Condition())) throw new RuntimeException('Saved r14 applicability unit differs from expected original bytes.');
evidenceContractCheck($savedIndex, $r7First . $r14Unit, true);
foreach ([1, 2, 7, 31] as $size) {
$wire = $r7First . $r14Unit;
$visible = '';
$stream = new \app\adminapi\service\TcmClinicalReportStream($savedLedger,
static function (string $delta) use (&$visible): bool { $visible .= $delta; return true; },
null, '', null, $savedIndex);
foreach (str_split($wire, $size) as $chunk) if (!$stream->push($chunk)) throw new RuntimeException('Original future-applicability unit rejected before forwarding.');
++$count;
if (!$stream->finish() || $stream->rejected() || $visible !== $wire || $stream->acceptedContent() !== $wire) {
throw new RuntimeException('Future-applicability compatibility changed original report bytes.');
}
}
}
foreach ([
'患者有胸痛。' => 'NEGATIVE_TO_POSITIVE', '患者无口干。' => 'POSITIVE_TO_NEGATIVE',
'患者无头晕。' => 'UNKNOWN_TO_NEGATIVE', '胸痛未记录。' => 'NEGATIVE_TO_UNKNOWN',
'舌质红绛。' => 'TONGUE_FEATURE_NOT_IN_CURRENT_SOURCE', '脉滑。' => 'PULSE_FEATURE_NOT_IN_CURRENT_SOURCE',
] as $assertion => $expectedCode) foreach ([true, false] as $before) foreach ([1, 2, 7, 31] as $size) {
$visible = '';
$prefix = $first . "六、中药方药及复核监测\n\n";
$unit = ($before ? $assertion . evidenceContractR14Condition() : evidenceContractR14Condition() . $assertion) . "\n\n";
$stream = new \app\adminapi\service\TcmClinicalReportStream($ledger,
static function (string $delta) use (&$visible): bool { $visible .= $delta; return true; },
null, '', null, $index);
foreach (str_split($prefix, $size) as $chunk) if (!$stream->push($chunk)) throw new RuntimeException('Valid prefix failed in future-applicability adjacency control.');
foreach (str_split($unit, $size) as $chunk) if (!$stream->push($chunk)) break;
++$count;
if (!$stream->rejected() || $stream->finish() || $visible !== $prefix
|| !in_array($expectedCode, array_column($stream->issues(), 'code'), true)) {
throw new RuntimeException('A future applicability condition waived a preceding or following actual assertion.');
}
}
// Shared source/stream checks are intentionally separate from --pure-meta.
evidenceContractR15InferenceChecks($index, $first, $history, $mixed);
foreach (evidenceContractR15Units() as $unit) {
evidenceContractCheck($savedIndex, $r7First . "二、八纲\n\n" . $unit, true);
foreach ([1, 2, 7, 31] as $size) {
$wire = $r7First . "二、八纲\n\n" . $unit;
$visible = '';
$stream = new \app\adminapi\service\TcmClinicalReportStream($savedLedger,
static function (string $delta) use (&$visible): bool { $visible .= $delta; return true; },
null, '', null, $savedIndex);
foreach (str_split($wire, $size) as $chunk) if (!$stream->push($chunk)) throw new RuntimeException('Exact r15 category inference/support unit rejected before forwarding.');
++$count;
if (!$stream->finish() || $stream->rejected() || $visible !== $wire || $stream->acceptedContent() !== $wire) {
throw new RuntimeException('Category inference/support compatibility changed original model bytes.');
}
}
}
foreach (['不能仅据虚证直接判定全属虚而无实', '亦无支持阳证的独立阳性组合'] as $phrase) foreach ([
'患者有胸痛。' => 'NEGATIVE_TO_POSITIVE', '患者无口干。' => 'POSITIVE_TO_NEGATIVE',
'患者无头晕。' => 'UNKNOWN_TO_NEGATIVE', '胸痛未记录。' => 'NEGATIVE_TO_UNKNOWN',
'舌质红绛。' => 'TONGUE_FEATURE_NOT_IN_CURRENT_SOURCE', '脉滑。' => 'PULSE_FEATURE_NOT_IN_CURRENT_SOURCE',
] as $assertion => $expectedCode) foreach ([true, false] as $before) foreach ([1, 2, 7, 31] as $size) {
$visible = '';
$prefix = $first . "二、八纲\n\n";
$unit = ($before ? $assertion . $phrase : $phrase . ',' . $assertion) . "\n\n";
$stream = new \app\adminapi\service\TcmClinicalReportStream($ledger,
static function (string $delta) use (&$visible): bool { $visible .= $delta; return true; },
null, '', null, $index);
foreach (str_split($prefix, $size) as $chunk) if (!$stream->push($chunk)) throw new RuntimeException('Valid prefix failed in r15 factual-adjacency control.');
foreach (str_split($unit, $size) as $chunk) if (!$stream->push($chunk)) break;
++$count;
if (!$stream->rejected() || $stream->finish() || $visible !== $prefix
|| !in_array($expectedCode, array_column($stream->issues(), 'code'), true)) {
throw new RuntimeException('A bounded category inference/support role waived a neighbouring patient assertion.');
}
}
echo "Clinical evidence contract: {$count} assertions passed.\n";
/** Retained model units are regression data, not preferred clinical conclusions. */
function evidenceContractR17Units(): array
{
return [
['tcm_complete_v43r16_supplement_ooc2_20261024_1_openai.rejected.json', 0, "二、八纲\n",
'表里:现有资料未见足以独立确定表证或里证的阳性定位依据。病程3个月仅提示迁延,不能单独判里;表里均尚不能定性。' . "\n\n"],
['tcm_complete_v43r16_supplement_ooc2_20261024_1_openai.rejected.json', 1, "五、治法\n",
'生津针对口干少量频饮及舌面少津(s2、s9),居兼治位;不采用温阳、清热、滋阴降火或活血等尚无独立证据的限定治法。' . "\n\n"],
['tcm_complete_v43r16_openai_deadline_20261024_openai_next_full90_openai.rejected.json', 0, "二、八纲\n",
'寒热:口干、少津、苔少及大便稍干(s2、s9)可提示津少,尚不足单独成立热证;寒象亦无本次独立阳性证据。寒热属性现有资料不足以确定。' . "\n\n"],
];
}
/** Can run through a shutdown callback after existing --pure-meta exits, with no peers loaded. */
function evidenceContractR17MetaChecks(array $index, string $first, string $denialId, string $wrongId): void
{
$head = $first . "二、八纲\n";
$evidence = '现有资料未见足以独立确定表证或里证的阳性定位依据';
$withheld = '不采用温阳、清热、滋阴降火或活血等尚无独立证据的限定治法';
foreach ([$evidence . '。', $withheld . '。',
'现有证据未见足以直接确定寒证或热证的独立辨证依据。',
'未见足以明确支持阴虚证及气虚证的充分诊断证据。',
'没有足以独立确认表证的阳性定位证据。',
'无足以充分确定里证的独立分型依据。',
'不选用温补、清解等仍没有可靠依据的具体治疗方法。',
'故不予采用活血与温阳等尚无充分证据的特定治则。',
'暂不采用养津等无独立证据的治法。'] as $phrase) {
evidenceContractCheck($index, $head . $phrase, true);
}
foreach (['患者未见口干。', '本次未见舌红。', '查见未见口干、舌红。',
'未见足以独立确定口干或舌红的阳性定位依据。',
'未见足以独立确定患者口干证的阳性定位依据。',
'未见足以独立确定患者无口干证的阳性定位依据。',
'未见足以独立确定本次有胸痛诊断的阳性定位依据。',
'未见足以独立确定表证未记录的阳性定位依据。',
'未见足以独立确定口干、舌红的证据。',
'未见足以独立确定表证。口干诊断的阳性定位依据。',
'未见足以独立确定表证;口干诊断的阳性定位依据。',
'未见足以独立确定表证的阳性口干。',
$evidence . '性皮疹。', '否认足以独立确定表证的阳性定位依据。',
'有无足以独立确定表证的阳性定位依据。',
'不采用温阳、患者无口干等尚无独立证据的限定治法。',
'不采用温阳、无胸痛等尚无独立证据的限定治法。',
'不采用温阳等尚无独立证据的限定口干。',
'不采用温阳等尚无独立证据的限定治法是患者无口干依据。',
'并非不采用温阳等尚无独立证据的限定治法。',
'患者不采用温阳等尚无独立证据的限定治法。',
'不采用' . str_repeat('甲', 13) . '等尚无独立证据的限定治法。',
'不采用' . implode('、', array_fill(0, 9, '甲')) . '等尚无独立证据的限定治法。',
'不采用温阳等尚未见独立证据的限定治法。'] as $assertion) {
evidenceContractCheck($index, $head . $assertion, false, 'UNQUOTED_PATIENT_DENIAL');
}
foreach ([$evidence, $withheld] as $phrase) {
foreach ([',患者无口干。', '且否认头晕。', '而本次未见舌红。', ';没有相互作用。'] as $tail) {
evidenceContractCheck($index, $head . $phrase . $tail, false, 'UNQUOTED_PATIENT_DENIAL');
}
evidenceContractCheck($index, $head . '患者无口干;' . $phrase . '。', false, 'UNQUOTED_PATIENT_DENIAL');
evidenceContractCheck($index, $head . $phrase . ';“无胸痛”(s999)。', false, 'DENIAL_SOURCE_QUOTE_INVALID');
evidenceContractCheck($index, $head . $phrase . ';“无胸痛”(' . $wrongId . ')。', false, 'DENIAL_SOURCE_QUOTE_INVALID');
evidenceContractCheck($index, $head . $phrase . ';2026-10-22“无胸痛”(' . $denialId . ')。', false, 'EVIDENCE_SOURCE_DATE_MISMATCH');
evidenceContractCheck($index, $head . $phrase . ';本次2026-10-22“无晕厥”(s90)。', false, 'EXAM_SOURCE_NOT_CURRENT');
evidenceContractCheck($index, $head . $phrase . ';2026-10-23“无胸痛”(' . $denialId . ')。', true);
evidenceContractCheck($index, "一、四诊\n问诊:" . $phrase . '。', false, 'EXAM_FACT_PROTOCOL_INVALID');
}
$qualified = $index;
$qualified['sources']['s88'] = ['quote' => '无明显畏寒、肢冷。', 'scope' => 'current', 'date' => '2026-10-23',
'record_state' => 'negative', 'spans' => [['role' => 'interview', 'recorded' => true, 'quote' => '无明显畏寒、肢冷。']]];
$qualified['sources']['s89'] = ['quote' => '无头晕。', 'scope' => 'current', 'date' => '2026-10-23',
'record_state' => 'unknown', 'spans' => [['role' => 'interview', 'recorded' => false, 'quote' => '无头晕。']]];
foreach ([$evidence, $withheld] as $phrase) {
evidenceContractCheck($qualified, $head . $phrase . ';2026-10-23“无明显畏寒、肢冷。”(s88)。', true);
evidenceContractCheck($qualified, $head . $phrase . ';2026-10-23“无畏寒、肢冷。”(s88)。', false, 'DENIAL_SOURCE_QUOTE_INVALID');
evidenceContractCheck($qualified, $head . $phrase . ';2026-10-23“无头晕。”(s89)。', false, 'DENIAL_SOURCE_QUOTE_INVALID');
}
foreach (['diagnosticEvidenceAbsence' => $evidence, 'withheldMethodEvidenceAbsence' => $withheld] as $method => $phrase) {
foreach (['患者有胸痛。', '患者无口干。', '舌质红绛。', '脉滑。', '现用药未记录。'] as $assertion) {
foreach ([$assertion . ';' . $phrase, $phrase . ',' . $assertion] as $text) {
$masked = (new ReflectionMethod(Contract::class, $method))->invoke(null, $text);
++$GLOBALS['count'];
if (!str_contains($masked, $assertion)) throw new RuntimeException('Diagnostic meta role consumed an adjoining factual predicate.');
}
}
}
$methodInput = '不采用温阳、患者无口干等尚无独立证据的限定治法';
$methodMasked = (new ReflectionMethod(Contract::class, 'withheldMethodEvidenceAbsence'))->invoke(null, $methodInput);
++$GLOBALS['count'];
if ($methodMasked !== str_replace('尚无独立', '尚' . str_repeat(' ', strlen('无')) . '独立', $methodInput)) {
throw new RuntimeException('Withheld-method grammar must mask only its evidence operator, retaining actual denials in the list.');
}
foreach (evidenceContractR17Units() as [$file, $unitIndex, $section, $unit]) {
$path = dirname(__DIR__, 2) . '/app/artifacts/ai_concurrency/' . $file;
if (is_file($path)) {
$saved = json_decode((string) file_get_contents($path), true);
++$GLOBALS['count'];
if (($saved['rejections'][$unitIndex]['unit'] ?? '') !== $unit) throw new RuntimeException('Portable r17 unit differs from retained original model bytes.');
}
evidenceContractCheck($index, $first . $section . $unit, true);
}
}
// Appended-only r17 coverage: existing expectations and pure-mode entry remain unchanged.
evidenceContractR17MetaChecks($index, $first, $history, $mixed);
foreach (evidenceContractR17Units() as [$file, $unitIndex, $section, $unit]) {
evidenceContractCheck($savedIndex, $r7First . $section . $unit, true);
foreach ([1, 2, 7, 31] as $size) {
$wire = $r7First . $section . "\n" . $unit;
$visible = '';
$stream = new \app\adminapi\service\TcmClinicalReportStream($savedLedger,
static function (string $delta) use (&$visible): bool { $visible .= $delta; return true; },
null, '', null, $savedIndex);
foreach (str_split($wire, $size) as $chunk) if (!$stream->push($chunk)) throw new RuntimeException('Exact r17 diagnostic meta unit rejected before forwarding.');
++$count;
if (!$stream->finish() || $stream->rejected() || $visible !== $wire || $stream->acceptedContent() !== $wire) {
throw new RuntimeException('Diagnostic meta handling changed retained original model bytes.');
}
}
}
foreach (['现有资料未见足以独立确定表证或里证的阳性定位依据',
'不采用温阳、清热、滋阴降火或活血等尚无独立证据的限定治法'] as $phrase) foreach ([
'患者有胸痛。' => 'NEGATIVE_TO_POSITIVE', '患者无口干。' => 'POSITIVE_TO_NEGATIVE',
'患者无头晕。' => 'UNKNOWN_TO_NEGATIVE', '胸痛未记录。' => 'NEGATIVE_TO_UNKNOWN',
'舌质红绛。' => 'TONGUE_FEATURE_NOT_IN_CURRENT_SOURCE', '脉滑。' => 'PULSE_FEATURE_NOT_IN_CURRENT_SOURCE',
] as $assertion => $expectedCode) foreach ([true, false] as $before) foreach ([1, 2, 7, 31] as $size) {
$visible = '';
$prefix = $first . "二、八纲\n\n";
$unit = ($before ? $assertion . $phrase : $phrase . ',' . $assertion) . "\n\n";
$stream = new \app\adminapi\service\TcmClinicalReportStream($ledger,
static function (string $delta) use (&$visible): bool { $visible .= $delta; return true; },
null, '', null, $index);
foreach (str_split($prefix, $size) as $chunk) if (!$stream->push($chunk)) throw new RuntimeException('Valid prefix failed in r17 diagnostic meta adjacency control.');
foreach (str_split($unit, $size) as $chunk) if (!$stream->push($chunk)) break;
++$count;
if (!$stream->rejected() || $stream->finish() || $visible !== $prefix
|| !in_array($expectedCode, array_column($stream->issues(), 'code'), true)) {
throw new RuntimeException('A diagnostic meta role waived a neighbouring clinical assertion.');
}
}
echo "Clinical evidence contract including appended r17 checks: {$count} assertions passed.\n";