95 lines
5.9 KiB
PHP
95 lines
5.9 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
require dirname(__DIR__) . '/vendor/autoload.php';
|
|
|
|
use app\adminapi\logic\tcm\DiagnosisAiLogic;
|
|
use app\adminapi\logic\tcm\PatientAiReportLogic;
|
|
use app\adminapi\service\AiRequiredSource;
|
|
use app\adminapi\service\TcmEvidenceGuard;
|
|
|
|
// Only the dictionary lookup of the pure enrichment helper is needed here.
|
|
final class SourceStateDictionary
|
|
{
|
|
public static function name(string $table): self
|
|
{
|
|
if ($table !== 'dict_data') throw new RuntimeException('Unexpected I/O dependency');
|
|
return new self();
|
|
}
|
|
public function whereIn(...$args): self { return $this; }
|
|
public function field(...$args): self { return $this; }
|
|
public function select(): self { return $this; }
|
|
public function toArray(): array { return []; }
|
|
}
|
|
class_alias(SourceStateDictionary::class, 'think\facade\Db');
|
|
$checks = 0;
|
|
function sourceStateExpect(bool $condition, string $description): void
|
|
{
|
|
global $checks;
|
|
++$checks;
|
|
if (!$condition) throw new RuntimeException($description);
|
|
}
|
|
function sourceStateSection(array $submission, string $key)
|
|
{
|
|
foreach ($submission['required_content'] as $section) if ($section['key'] === $key) return $section['value'];
|
|
throw new RuntimeException('Missing required section');
|
|
}
|
|
|
|
$empty = AiRequiredSource::submission([]);
|
|
sourceStateExpect(count($empty['required_content']) === 13, 'All thirteen empty categories remain explicit');
|
|
foreach ($empty['required_content'] as $section) sourceStateExpect($section['value'] === '无', 'Empty category is no data, not a denial');
|
|
$row = ['id' => 71, 'sleep_condition' => '睡眠一般', 'stool_condition' => '',
|
|
'current_medications' => null, 'appetite' => '口干;大便未记录;否认腹痛',
|
|
'family_history' => '无', 'allergy_history_text' => '否认药物过敏史',
|
|
'tongue' => '本次舌淡红苔少', 'tongue_text' => '', 'pulse' => '本次脉细弱',
|
|
'pulse_text' => null, 'tongue_images' => [], 'status' => 0, 'show_card' => false];
|
|
$submission = AiRequiredSource::submission(['diagnoses' => [$row]]);
|
|
$present = sourceStateSection($submission, 'present_illness')[0];
|
|
sourceStateExpect($present['sleep_condition'] === '睡眠一般', 'Actual observation is verbatim');
|
|
sourceStateExpect($present['stool_condition'] === '', 'Missing scalar does not become 无');
|
|
sourceStateExpect($present['current_medications'] === null, 'Unknown medication is not an assertion of no medication');
|
|
sourceStateExpect($present['appetite'] === $row['appetite'], 'Mixed positive, unknown and denial text stays whole');
|
|
sourceStateExpect($present['weight_change'] === null, 'Absent field remains null');
|
|
$other = sourceStateSection($submission, 'other_history')[0];
|
|
sourceStateExpect($other['family_history'] === '无', 'An actual raw source string is never rewritten');
|
|
sourceStateExpect($other['allergy_history_text'] === '否认药物过敏史', 'Explicit denial alias survives');
|
|
$exam = sourceStateSection($submission, 'diagnosis_info')[0];
|
|
sourceStateExpect($exam['tongue_images'] === [], 'Empty attachment array never becomes an invented reference');
|
|
$basic = sourceStateSection($submission, 'basic_info')[0];
|
|
sourceStateExpect($basic['status'] === 0 && $basic['show_card'] === false, 'Nonclinical zero and false retain type');
|
|
$guard = TcmEvidenceGuard::assess($row, $submission);
|
|
sourceStateExpect(!$guard['restricted'], 'Empty aliases do not erase complete raw tongue and pulse');
|
|
sourceStateExpect($guard['current_tongue'] === $row['tongue'] && $guard['current_pulse'] === $row['pulse'], 'Actual source exams are unchanged');
|
|
sourceStateExpect(str_contains(json_encode($guard['facts'], JSON_UNESCAPED_UNICODE), '口干;大便未记录;否认腹痛'), 'Source-only evidence retains mixed source text');
|
|
|
|
$enrich = (new ReflectionClass(PatientAiReportLogic::class))->getMethod('enrichDiagnosisRows');
|
|
$binary = ['trauma_history', 'surgery_history', 'allergy_history', 'family_history', 'pregnancy_history'];
|
|
$coded = $enrich->invoke(null, [array_fill_keys($binary, 0), array_fill_keys($binary, 1),
|
|
['family_history' => 0, 'family_history_text' => '本次询问明确否认家族史'],
|
|
['allergy_history' => '否认药物过敏史']]);
|
|
foreach ($binary as $field) {
|
|
sourceStateExpect($coded[0][$field] === 0 && $coded[0][$field . '_text'] === '未记录', 'Default zero is not confirmed negative');
|
|
sourceStateExpect($coded[1][$field] === 1 && $coded[1][$field . '_text'] === '有', 'Positive legacy flag is retained');
|
|
}
|
|
sourceStateExpect($coded[2]['family_history_text'] === '本次询问明确否认家族史', 'Enrichment never replaces recorded interview text');
|
|
sourceStateExpect($coded[3]['allergy_history'] === '否认药物过敏史', 'Raw textual denial remains unchanged');
|
|
foreach ([null, '', 'unknown', false, 'invalid'] as $value) {
|
|
$result = $enrich->invoke(null, [['gender' => $value]]);
|
|
sourceStateExpect($result[0]['gender_text'] === '未知', 'Blank or invalid gender is not cast to female');
|
|
}
|
|
$gender = (new ReflectionClass(DiagnosisAiLogic::class))->getMethod('genderText');
|
|
foreach ([0 => '女', 1 => '男'] as $value => $label) {
|
|
foreach ([$value, (string) $value] as $encoded) {
|
|
$result = $enrich->invoke(null, [['gender' => $encoded]]);
|
|
sourceStateExpect($result[0]['gender_text'] === $label && $gender->invoke(null, $encoded) === $label, 'Demographics agree with defined source codes');
|
|
}
|
|
}
|
|
$safe = (new ReflectionClass(PatientAiReportLogic::class))->getMethod('sanitizeSnapshotForUpstream')
|
|
->invoke(null, $submission);
|
|
$safeExam = sourceStateSection($safe, 'diagnosis_info')[0];
|
|
sourceStateExpect($safeExam['tongue_images']['attachment_count'] === 0, 'Sanitization counts empty attachments correctly');
|
|
$safePresent = sourceStateSection($safe, 'present_illness')[0];
|
|
sourceStateExpect($safePresent['current_medications'] === null && $safePresent['stool_condition'] === '', 'Transport projection keeps field absence distinct from denial');
|
|
echo "AI source state preservation: {$checks} assertions passed (pure, no network/database).\n";
|