139 lines
9.4 KiB
PHP
139 lines
9.4 KiB
PHP
<?php
|
||
|
||
declare(strict_types=1);
|
||
|
||
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; $cases = 0; $failures = [];
|
||
$expect = static function (bool $ok, string $message) use (&$checks, &$failures): void {
|
||
++$checks;
|
||
if (!$ok) $failures[] = $message;
|
||
};
|
||
$artifacts = dirname(__DIR__, 2) . '/app/artifacts/ai_concurrency/';
|
||
$before = file_get_contents($artifacts . 'v43r25d_before_TcmClinicalFactStateGuard.php.snapshot');
|
||
$expect(hash('sha256', $before) === 'e139ff33772ec37afacda14fb1866878c2b1572f4d0788ccc526cb0aab49cc2c', 'Exact d Guard preserved.');
|
||
if (hash('sha256', $before) !== 'e139ff33772ec37afacda14fb1866878c2b1572f4d0788ccc526cb0aab49cc2c') throw new RuntimeException('Unverified snapshot');
|
||
$before = preg_replace('/final class TcmClinicalFactStateGuard\b/', 'final class R25eBeforeGuard', $before, 1, $replaced);
|
||
$expect($replaced === 1, 'Isolated before class only.');
|
||
eval(substr($before, strlen('<?php')));
|
||
$beforeGuard = 'app\\adminapi\\service\\R25eBeforeGuard';
|
||
$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)), 'Before/current construction byte-identical.');
|
||
$expect(hash('sha256', json_encode($ledger, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES))
|
||
=== '2ebbec0b558347bf4ee128dfb5a9e63fc4e355d58f7226486a2ae96098bd57f6', 'Actual complete ledger pinned.');
|
||
$index = Index::build($ledger);
|
||
$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";
|
||
}
|
||
}
|
||
$head .= "二、八纲\n\n";
|
||
$expect(Guard::validate($ledger, $head)['valid'] && Contract::validate($index, $head, true)['valid'], 'Real six-role source-quoted prefix valid.');
|
||
$check = static function (string $unit, bool $valid, bool $oldBypass = false) use (
|
||
$expect, $ledger, $index, $head, $beforeGuard, &$cases): void {
|
||
++$cases;
|
||
$original = $unit;
|
||
$guard = Guard::validate($ledger, $head . $unit);
|
||
$contract = Contract::validate($index, $head . $unit, true);
|
||
$expect($guard['valid'] === $valid, 'Guard ' . $unit . ': ' . json_encode($guard));
|
||
$expect(($guard['valid'] && $contract['valid']) === $valid, 'Composed result ' . $unit . ': ' . json_encode($contract));
|
||
if (!$valid) $expect(in_array('SOURCE_ANNOTATION_INVALID', array_column($guard['issues'], 'code'), true), 'Every local operand must have its own actual source ' . $unit);
|
||
if ($oldBypass) $expect($beforeGuard::validate($ledger, $head . $unit)['valid'], 'Unchanged exact baseline bypass reproduced.');
|
||
foreach ([1, 13, 47] as $size) {
|
||
$visible = '';
|
||
$stream = new Stream($ledger, static function (string $delta) use (&$visible): bool { $visible .= $delta; 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, 'Stream result ' . $size . ': ' . $unit);
|
||
$expect($visible === ($valid ? $unit . "\n\n" : ''), 'Exact raw accepted bytes / no rejected unit release ' . $size);
|
||
}
|
||
$expect($unit === $original, 'Raw model unit unchanged.');
|
||
};
|
||
|
||
foreach (['且', '并且', '而且', '并', '而', '亦', '也'] as $join) {
|
||
$check('脉细弱(s10),当前无力' . $join . '滑(s10)。', false, $join === '且');
|
||
$check('脉细弱(s10),当前无力' . $join . '有力(s10)。', false, $join === '且');
|
||
$check('脉细弱(s10),当前细' . $join . '无力(s10)。', true);
|
||
$check('脉细弱(s10),当前不浮' . $join . '不滑(s10)。', true);
|
||
$check('脉细弱(s10),当前不浮' . $join . '无力(s10)。', true);
|
||
$check('脉细弱(s10),当前不浮' . $join . '滑(s10)。', false);
|
||
$check('脉细弱(s10),当前不细' . $join . '不弱(s10)。', false);
|
||
$check('脉细弱' . $join . '滑(s10)。', false);
|
||
$check('脉细弱' . $join . '无力(s10)。', true);
|
||
$check('舌质淡红(s9),当前淡红' . $join . '少津(s9)。', true);
|
||
$check('舌质淡红(s9),当前淡红' . $join . '胖大(s9)。', false);
|
||
$check('脉细弱(s10),当前' . $join . '无力(s3)。', false);
|
||
$check('脉细弱(s10),当前' . $join . '无力(s10)。', true);
|
||
}
|
||
foreach (['脉细弱(s10),当前无力并且按之滑(s10)。',
|
||
'舌质淡红、脉细弱且滑(s9、s10)。',
|
||
'脉细弱且滑、舌质淡红(s9、s10)。',
|
||
'脉细弱(s10),当前无力且细(s3)。',
|
||
'脉细弱(s10),当前无力且细(2020-05-02,s10)。',
|
||
'脉细弱(s10),既往无力且细(s10)。',
|
||
'脉细弱(s10),当前无力且细(s14)。'] as $unit) $check($unit, false);
|
||
foreach (['舌质淡红且少津(s9)。',
|
||
'舌质淡红、脉细弱且无力(s9、s10)。',
|
||
'脉细弱且无力、舌质淡红(s9、s10)。',
|
||
'脉细弱(s10),当前无力并且按之无力(s10)。',
|
||
'脉细弱(s10),当前细且弱并且无力(s1、s2、s10)。'] as $unit) $check($unit, true);
|
||
$check('脉细弱(s10),当前无力且' . str_repeat('细', 50) . '滑(s10)。', false);
|
||
$check('脉细弱(s10),当前' . str_repeat('细', 50) . '且无力(s10)。', true);
|
||
$check('脉细弱(s10),当前不浮且不' . str_repeat('滑', 50) . '细(s10)。', false);
|
||
$check('脉细弱(s10),当前不浮且不' . str_repeat('滑', 50) . '(s10)。', true);
|
||
|
||
// Ordinary subjects and real terminators retain complete parser-array
|
||
// equality to d. These are reference reset diagnostics, not clinical passes.
|
||
$parse = new ReflectionMethod(Guard::class, 'reportClauses');
|
||
$oldParse = new ReflectionMethod($beforeGuard, 'reportClauses');
|
||
foreach (['且', '并且', '而且', '并', '而', '但', '亦', '也'] as $join) {
|
||
foreach (['口干', '腹部按之无力'] as $subject) {
|
||
$unit = '脉细弱(s10),当前无力' . $join . $subject . '(s3),目前无力(s3)。';
|
||
$expect($parse->invoke(null, $ledger, $unit) === $oldParse->invoke(null, $ledger, $unit), 'Ordinary conjunction subject resets: ' . $unit);
|
||
}
|
||
}
|
||
foreach (['。', ';', ';', '!', '?', "\n"] as $end) {
|
||
$unit = '脉细弱(s10)' . $end . '当前无力且细(s3)。';
|
||
$expect($parse->invoke(null, $ledger, $unit) === $oldParse->invoke(null, $ledger, $unit), 'Real boundary still clears inherited exam kind.');
|
||
}
|
||
foreach ([['“','”'], ['「','」'], ['"','"']] as [$open, $close]) {
|
||
$unit = '切诊:2026-10-23' . $open . $ledger['sources']['s10']['quote'] . $close . '(s10),当前无力且细(s3)。';
|
||
$expect($parse->invoke(null, $ledger, $unit) === $oldParse->invoke(null, $ledger, $unit), 'Full source-quote boundary unchanged.');
|
||
}
|
||
$finite = new ReflectionMethod(Guard::class, 'finiteExamConjunctionProse');
|
||
$preserved = $parse->invoke(null, $ledger, '脉细弱(s10),当前不浮滑且不紧(s10)。');
|
||
$negativeTopics = [];
|
||
foreach ($preserved as $entry) foreach ($entry['exam_conjunction_parts'] ?? [] as $part) {
|
||
if ($part['state'] === 'negative') $negativeTopics[] = $part['topic'];
|
||
}
|
||
$expect(in_array('脉浮滑', $negativeTopics, true) && !in_array('脉浮', $negativeTopics, true)
|
||
&& !in_array('脉滑', $negativeTopics, true), 'A compound negative operand remains one original polarity scope.');
|
||
foreach (['无力且口干', '无力且腹部按之无力', '无力并且患者无固定刺痛', '无力而且目前口干',
|
||
'无力且且滑', '且滑', '无力且', '按之无力且腹部按之滑'] as $notClosed) {
|
||
$expect($finite->invoke(null, 'pulse', $notClosed) === null, 'No ordinary/malformed prose is normalized: ' . $notClosed);
|
||
}
|
||
$expect($parse->invoke(null, $ledger, $head) === $oldParse->invoke(null, $ledger, $head), 'Whole actual quoted six-role prefix unchanged.');
|
||
$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' => 'r25e finite same-exam conjunction/source/polarity integration', 'assertions' => $checks,
|
||
'cases' => $cases, 'failures' => $failures], JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR), "\n";
|
||
exit($failures === [] ? 0 : 1);
|