invoke(null, CompleteTcmCase::rows()); $submission = (new ReflectionMethod(PatientAiReportLogic::class, 'sanitizeSnapshotForUpstream')) ->invoke(null, AiRequiredSource::submission($snapshot)); $ledger = Guard::build(CompleteTcmCase::current(), $submission); $index = Index::build($ledger); $expect(hash('sha256', json_encode($ledger, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)) === '2ebbec0b558347bf4ee128dfb5a9e63fc4e355d58f7226486a2ae96098bd57f6', 'The complete immutable source projection retains all source IDs, quotes, dates, roles and states.'); // Establish the real six-role first section, using complete original role spans. $first = "一、四诊\n\n"; foreach (Index::ROLES as $label => $role) { $sourceSpan = null; foreach ($index['sources'] as $source) { if ($source['scope'] !== 'current') continue; foreach ($source['spans'] as $span) { if ($span['role'] === $role && $span['recorded']) { $sourceSpan = [$source, trim($span['quote'])]; break 2; } } } if ($role === 'palpation' && $sourceSpan === null) { $first .= "切诊:其他触诊未记录,脉象见独立脉象项。\n\n"; continue; } $expect($sourceSpan !== null, 'Every recorded exam role has actual source provenance.'); [$source, $quote] = $sourceSpan; $first .= $label . ':' . $source['date'] . '“' . $quote . '”(' . $source['id'] . ")。\n\n"; } $contract = static function (string $text, bool $valid, string $code = '', bool $complete = false) use ($index, $expect): array { $original = $text; $result = Contract::validate($index, $text, $complete); $expect($result['valid'] === $valid, json_encode([$text, $result], JSON_UNESCAPED_UNICODE)); if ($code !== '') $expect(in_array($code, array_column($result['issues'], 'code'), true), 'Expected ' . $code . ': ' . json_encode($result, JSON_UNESCAPED_UNICODE)); $expect($text === $original, 'Contract validation preserves every original model/output byte.'); return $result; }; $guard = static function (string $text, bool $valid, string $code = '') use ($ledger, $expect): array { $original = $text; $result = Guard::validate($ledger, $text); $expect($result['valid'] === $valid, json_encode([$text, $result], JSON_UNESCAPED_UNICODE)); if ($code !== '') $expect(in_array($code, array_column($result['issues'], 'code'), true), 'Expected unchanged Guard issue ' . $code . ': ' . json_encode($result, JSON_UNESCAPED_UNICODE)); $expect($text === $original, 'Guard validation preserves the original string.'); return $result; }; $contract($first, true, '', true); $head = $first . "二、八纲\n\n"; $unit = "虚实:神疲、面色少华、语声低微,活动后气短、自汗,脉细弱无力(s2、s3、s10),共同支持虚证。实证缺乏阳性依据,不能据此直接判定“无实”。\n\n"; $artifact = dirname(__DIR__, 2) . '/app/artifacts/ai_concurrency/tcm_complete_v43r20_cap8_single_openai_20261024_0_openai.rejected.json'; if (is_file($artifact)) { $saved = json_decode((string) file_get_contents($artifact), true, 512, JSON_THROW_ON_ERROR); $expect($saved['rejections'][0]['unit'] === $unit, 'The portable exact unit equals the retained original failure.'); } $expect(strlen($unit) === 201 && hash('sha256', $unit) === '6739ca28205232838eb426ceccf164c609d7fda9b2be5878255dfcad3c77421a', 'The original retained bytes are not rewritten or substituted.'); $contract($head . $unit, true, '', true); $guard($unit, true); foreach (['表', '里', '寒', '热', '虚', '实', '阴', '阳'] as $category) { foreach (['不能据此直接判定', '不得由此直接判断', '不应仅凭虚证直接认定'] as $operator) { foreach ([['“', '”'], ['「', '」'], ['"', '"']] as [$open, $close]) { $contract($head . $operator . $open . '无' . $category . $close . '。', true); } } } $base = '不能据此直接判定“无实”'; foreach (['可据此直接判定“无实”。', '可以据此直接判定“无实”。', '并非不能据此直接判定“无实”。', '不是不能据此直接判定“无实”。', '不能不据此直接判定“无实”。', '不能说不能据此直接判定“无实”。', '患者已判定“无实”。', '本次查见“无实”。', $base . '是事实。', $base . '“无固定刺痛”。', $base . '(s999)。', $base . '(s0)。', $base . '(非来源)。', $base . '(s2)。', $base . '(s2、s3)。', '不能据此直接判定“无固定刺痛”。', '不能据此直接判定“患者无口干”。', '不能据此直接判定“无实性皮疹”。', '不能据此直接判定“无' . str_repeat('实', 80) . '”。', ] as $text) $contract($head . $text, false, 'DENIAL_SOURCE_QUOTE_INVALID'); // The exception belongs to its immediately adjacent quote, not a previous clause/quote. foreach ([$base . ';“无固定刺痛”。', $base . ';“无固定刺痛”(s999)。', '不能据此直接判定“无实”;因此“无口干”。', '并非“这一结论”不能据此直接判定“无实”。', $base . ',不能据此直接判定“无固定刺痛”。'] as $text) { $contract($head . $text, false, 'DENIAL_SOURCE_QUOTE_INVALID'); } $contract($head . '不得写为“无胸痛”。', true); // Existing writing-role fallback is retained. $assertions = [ ['患者有心悸', 'NEGATIVE_TO_POSITIVE'], ['患者无固定刺痛', 'UNKNOWN_TO_NEGATIVE'], ['患者有固定刺痛', 'UNKNOWN_TO_POSITIVE'], ['心悸未记录', 'NEGATIVE_TO_UNKNOWN'], ['口干未记录', 'POSITIVE_TO_UNKNOWN'], ['舌质红绛', 'TONGUE_FEATURE_NOT_IN_CURRENT_SOURCE'], ['脉滑', 'PULSE_FEATURE_NOT_IN_CURRENT_SOURCE'], ['无五心烦热', 'NEGATION_SCOPE_WIDENED'], ]; foreach ([',', ',', '且', '但', '并', '并且', '同时', '还', '又', '另'] as $joiner) { foreach ($assertions as [$assertion, $code]) { foreach ([$base . $joiner . $assertion . '。', $assertion . ',' . $base . '。'] as $text) { $original = $text; $guardResult = Guard::validate($ledger, $text); $contractResult = Contract::validate($index, $head . $text); $expect(!($guardResult['valid'] && $contractResult['valid']), 'The combined source/state and quote protocol cannot accept an adjoining actual assertion.'); if (str_starts_with($text, $base) && !in_array($joiner, [',', ','], true)) { $expect(!$contractResult['valid'] && in_array('DENIAL_SOURCE_QUOTE_INVALID', array_column($contractResult['issues'], 'code'), true), 'A direct conjunction tail never gains the new inference-quote exemption.'); } else { $expect(!$guardResult['valid'] && in_array($code, array_column($guardResult['issues'], 'code'), true), 'The unchanged Guard retains every punctuation-separated actual assertion issue.'); } $expect($text === $original, 'Combined pure validation preserves the original model bytes.'); } } } $contract($head . $base . ',患者无固定刺痛。', false, 'UNQUOTED_PATIENT_DENIAL'); $contract($head . $base . '且药物无相互作用。', false, 'DENIAL_SOURCE_QUOTE_INVALID'); $contract($head . $base . '且心悸未记录。', false, 'DENIAL_SOURCE_QUOTE_INVALID'); $guard($base . ',心悸未记录。', false, 'NEGATIVE_TO_UNKNOWN'); $guard($base . ',口干未记录。', false, 'POSITIVE_TO_UNKNOWN'); $contract($head . '患者无固定刺痛,' . $base . '。', false, 'UNQUOTED_PATIENT_DENIAL'); $contract($head . $base . ';2026-10-23“无咳嗽咯痰、胸痛心悸、浮肿或腹泻。”(s2)。', true); $contract($head . $base . ';2026-10-23“无五心烦热、潮热盗汗,无畏寒肢冷。”(s2)。', false, 'DENIAL_SOURCE_QUOTE_INVALID'); $contract($head . $base . ';2026-10-22“无咳嗽咯痰、胸痛心悸、浮肿或腹泻。”(s2)。', false, 'EVIDENCE_SOURCE_DATE_MISMATCH'); $contract($head . $base . ';2026-10-23“无固定刺痛”(s2)。', false, 'DENIAL_SOURCE_QUOTE_INVALID'); $contract($head . $base . ';本次2026-10-23“' . $index['sources']['s33']['quote'] . '”(s33)。', false, 'EXAM_SOURCE_NOT_CURRENT'); // The later-section illustration role cannot supply a first-section clinical fact. foreach (array_keys(Index::ROLES) as $label) { $contract("一、四诊\n\n" . $label . ':' . $base . '。', false, 'SOURCE_ANNOTATION_INVALID'); } $contract("二、八纲\n\n" . $base . '。', false, 'EXAM_ROLE_MISSING'); $contract(str_replace('(s3)', '(s999)', $first), false, 'SOURCE_ANNOTATION_INVALID', true); $contract(str_replace('2026-10-23', '2026-10-22', $first), false, 'EVIDENCE_SOURCE_DATE_MISMATCH', true); $expect(hash('sha256', json_encode($ledger, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)) === '2ebbec0b558347bf4ee128dfb5a9e63fc4e355d58f7226486a2ae96098bd57f6', 'Validation leaves the complete original source ledger unchanged.'); $expect(!class_exists('app\\adminapi\\service\\TcmClinicalReportStream', false) && !class_exists('app\\adminapi\\logic\\tcm\\DiagnosisAiLogic', false), 'This pure focused test never loads peer stream/logic classes.'); echo "TCM negated inference quote role: {$count} assertions passed (pure production Contract/Guard; no stream/model/network).\n";