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

156 lines
10 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);
// Pure finite integration regression: no inference/bootstrap/config/network.
require dirname(__DIR__) . '/vendor/autoload.php';
require __DIR__ . '/fixtures/CompleteTcmCase.php';
use app\adminapi\logic\tcm\PatientAiReportLogic;
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;
use app\adminapi\service\TcmClinicalReportStream as Stream;
$checks = 0; $failures = []; $cases = 0;
$expect = static function (bool $ok, string $message) use (&$checks, &$failures): void {
++$checks;
if (!$ok) $failures[] = $message;
};
$snapshotPath = dirname(__DIR__, 2) . '/app/artifacts/ai_concurrency/v43r25c_before_TcmClinicalFactStateGuard.php.snapshot';
$beforeSource = file_get_contents($snapshotPath);
$expect(hash('sha256', $beforeSource) === 'b608f236c0b8877dbffb106ca8461596cf95ce911c060171cd20da31ca4a450c', 'Exact c Guard snapshot.');
if (hash('sha256', $beforeSource) !== 'b608f236c0b8877dbffb106ca8461596cf95ce911c060171cd20da31ca4a450c') {
throw new RuntimeException('Unverified before source');
}
$beforeSource = preg_replace('/final class TcmClinicalFactStateGuard\b/', 'final class R25dBeforeGuard', $beforeSource, 1, $replaced);
$expect($replaced === 1, 'Before class renamed only for isolated offline comparison.');
eval(substr($beforeSource, strlen('<?php')));
$beforeGuard = 'app\\adminapi\\service\\R25dBeforeGuard';
$raw = (new ReflectionMethod(PatientAiReportLogic::class, 'buildSourceSnapshotFromRows'))->invoke(null, CompleteTcmCase::rows());
$submission = (new ReflectionMethod(PatientAiReportLogic::class, 'sanitizeSnapshotForUpstream'))->invoke(null, AiRequiredSource::submission($raw));
$ledger = Guard::build(CompleteTcmCase::current(), $submission);
$ledgerBytes = serialize($ledger);
$expect($ledgerBytes === serialize($beforeGuard::build(CompleteTcmCase::current(), $submission)), 'Construction remains byte identical.');
$expect(hash('sha256', json_encode($ledger, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES))
=== '2ebbec0b558347bf4ee128dfb5a9e63fc4e355d58f7226486a2ae96098bd57f6', 'Immutable full ledger identity.');
$expect(hash_file('sha256', __DIR__ . '/fixtures/CompleteTcmCase.php')
=== 'b54c47ec11f6a99dc87bdda78629e18c8a6f37a64017e42a72db399c727cb1c6', 'Fixture identity.');
$makeHead = static function (array $index): string {
$head = "一、四诊\n\n";
foreach (Index::ROLES as $label => $role) {
$found = null;
foreach ($index['sources'] as $source) {
if ($source['scope'] !== 'current') continue;
foreach ($source['spans'] as $span) {
if ($span['role'] === $role && $span['recorded']) { $found = [$source, $span]; break 2; }
}
}
if ($found === null) {
$head .= $label . ($role === 'palpation' ? ":其他触诊未记录,脉象见独立脉象项。\n\n" : ":未记录\n\n");
} else {
[$source, $span] = $found;
$head .= $label . ':' . $source['date'] . '“' . trim($span['quote']) . '”(' . $source['id'] . ")。\n\n";
}
}
return $head . "二、八纲\n\n";
};
$check = static function (string $unit, bool $valid, ?array $input = null, bool $oldGap = false) use (
$ledger, $makeHead, $beforeGuard, $expect, &$cases): void {
++$cases;
$input ??= $ledger;
$index = Index::build($input);
$head = $makeHead($index);
$original = $unit;
$expect(Guard::validate($input, $head)['valid'] && Contract::validate($index, $head, true)['valid'], 'Source-quoted prefix valid.');
$guard = Guard::validate($input, $head . $unit);
$contract = Contract::validate($index, $head . $unit, true);
$expect($guard['valid'] === $valid, 'Composed Guard result ' . $unit . ': ' . json_encode($guard));
$expect(($guard['valid'] && $contract['valid']) === $valid, 'Composed acceptance matches ' . $unit);
if (!$valid) $expect(in_array('SOURCE_ANNOTATION_INVALID', array_column($guard['issues'], 'code'), true), 'Own local provenance rejection ' . $unit);
if ($oldGap) $expect($beforeGuard::validate($input, $head . $unit)['valid'], 'Exact pre-repair integration bypass reproduced.');
foreach ([1, 11, 43] as $size) {
$visible = '';
$stream = new Stream($input, static function (string $chunk) use (&$visible): bool { $visible .= $chunk; return true; },
null, $head, null, $index);
foreach (mb_str_split($unit . "\n\n", $size, 'UTF-8') as $chunk) if (!$stream->push($chunk)) break;
$expect($stream->finish() === $valid && $stream->rejected() === !$valid, 'Streaming acceptance at chunk ' . $size . ': ' . $unit);
$expect($visible === ($valid ? $unit . "\n\n" : ''), 'Exact original accepted bytes / zero rejected release at chunk ' . $size);
}
$expect($unit === $original, 'Validation never rewrites original unit.');
};
// Whole helper rejection must agree with the real Guard/Contract/stream path.
foreach (['本次', '此次', '当前', '目前'] as $time) {
$check('脉细弱(s10),' . $time . '无力(s3)。', false, null, $time === '本次');
$check('舌质淡红(s9),' . $time . '少津(s3)。', false, null, $time === '当前');
$check('脉细弱(s10),' . $time . '无力(s10)。', true);
$check('舌质淡红(s9),' . $time . '少津(s9)。', true);
}
foreach (['既往', '历史', '当年', '上次'] as $time) {
$check('脉细弱(s10),' . $time . '无力(s10)。', false, null, $time === '既往');
}
foreach (['脉细弱(s10), 当前按之无力(s3)。',
'脉细弱,本次无力(s3)。', '舌质淡红,当前少津(s3)。',
'脉细弱(s10),亦无力,本次无力(s3)。',
'脉细弱(s10),本次滑(s10)。', '脉细弱(s10),当前有力(s10)。',
'脉细弱(2026-10-23,s10),本次无力(2020-05-02,s10)。',
'舌质淡红、脉细弱(s9、s10),本次无力(s3)。',
'脉细弱(s10),当前无力(s10),目前无力(s3)。',
'脉细弱(s10),当前舌少津(s10)。'] as $unit) $check($unit, false);
foreach (['脉细弱,本次无力(s10)。', '舌质淡红,当前少津(s9)。',
'脉细弱(s10),亦无力,本次无力(s10)。',
'脉细弱(s10),本次无力(s1、s2、s10)。',
'脉细弱(s10),当前无力(s10),目前无力(s10)。',
'舌质淡红(s9),本次脉细弱无力(s1、s2、s10)。'] as $unit) $check($unit, true);
$historical = $ledger;
$historical['sources']['s40'] = array_replace($ledger['sources']['s10'], ['id' => 's40', 'scope' => 'historical',
'path' => 'history.0.pulse', 'date' => '2020-05-02', 'quote' => '2020-05-02医师切诊:脉细弱无力。']);
$check('既往脉细弱(2020-05-02,s40),当前无力(s40)。', false, $historical, true);
$check('既往脉细弱(2020-05-02,s40),上次无力(s40)。', true, $historical);
$check('既往脉细弱(2020-05-02,s40),上次无力(2026-10-23,s40)。', false, $historical);
$check('既往脉细弱(2020-05-02,s40),本次无力(s10)。', true, $historical);
$oldVisit = CompleteTcmCase::current(); $oldVisit['diagnosis_date'] = '2020-02-29';
$check('脉细弱(s10),本次无力(s10)。', true, Guard::build($oldVisit));
$split = CompleteTcmCase::current();
$split['pulse'] = '本次医师切诊:脉细弱。';
$split['pulse_condition'] = '本次医师切诊:按之无力。';
$splitLedger = Guard::build($split); $ids = [];
foreach ($splitLedger['sources'] as $source) if (in_array($source['field'], ['pulse', 'pulse_condition'], true)) $ids[$source['field']] = $source['id'];
$check('脉细弱(' . $ids['pulse'] . '),本次按之无力(' . $ids['pulse_condition'] . ')。', true, $splitLedger);
$check('脉细弱(' . $ids['pulse'] . '),本次按之无力(' . $ids['pulse'] . ')。', false, $splitLedger);
// Parser-only reset observations certify source-reference boundaries, not
// general acceptance of these ordinary positive clinical statements.
$clauses = new ReflectionMethod(Guard::class, 'reportClauses');
$beforeClauses = new ReflectionMethod($beforeGuard, 'reportClauses');
foreach (['。', ';', ';', '!', '?', "\n", "\r\n"] as $boundary) {
$unit = '脉细弱(s10)' . $boundary . '本次无力(s3)。';
$parsed = $clauses->invoke(null, $ledger, $unit);
$expect(!in_array(true, array_column($parsed, 'annotation_invalid'), true), 'Real terminator resets exam context.');
$expect($parsed === $beforeClauses->invoke(null, $ledger, $unit), 'Real boundary quote/role/polarity output unchanged.');
}
foreach (['脉细弱(s10),口干(s2),本次无力(s3)。',
'脉细弱(s10),口干,本次无力(s3)。',
'脉细弱(s10),本次口干(s2),当前无力(s3)。',
'脉细弱(s10),腹部按之无力(s3),本次无力(s3)。',
'舌质淡红(s9),当前口干(s2),本次少津(s3)。',
'脉象资料见上文(s3),本次无力(s3)。',
'问诊:2026-10-23“' . $ledger['sources']['s3']['quote'] . '”(s3),本次无力(s3)。',
'切诊:2026-10-23“' . $ledger['sources']['s10']['quote'] . '”(s10),本次无力(s3)。'] as $unit) {
$parsed = $clauses->invoke(null, $ledger, $unit);
$expect(!in_array(true, array_column($parsed, 'annotation_invalid'), true), 'Ordinary/new or source-quoted subject cannot seed an exam continuation: ' . $unit);
$expect($parsed === $beforeClauses->invoke(null, $ledger, $unit), 'Unrelated/source-quoted boundary results unchanged.');
}
$index = Index::build($ledger);
$expect($clauses->invoke(null, $ledger, $makeHead($index)) === $beforeClauses->invoke(null, $ledger, $makeHead($index)),
'All six actual source-quoted role clauses keep exact offsets, annotations, prose and parser flags.');
$expect(serialize($ledger) === $ledgerBytes, 'All source ledger bytes unchanged.');
$expect(!class_exists('app\\adminapi\\logic\\tcm\\DiagnosisAiLogic', false), 'Inference Logic not loaded.');
echo json_encode(['scope' => 'r25d checker-only temporal-comma integration', 'assertions' => $checks, 'cases' => $cases,
'failures' => $failures], JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR), "\n";
exit($failures === [] ? 0 : 1);