1177 lines
89 KiB
PHP
1177 lines
89 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\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";
|