90 lines
7.3 KiB
PHP
90 lines
7.3 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
|
|
// Input-policy contract only: no concurrent Guard/Contract or runtime classes.
|
|
require dirname(__DIR__) . '/app/adminapi/service/TcmClinicalEvidenceIndex.php';
|
|
use app\adminapi\service\TcmClinicalEvidenceIndex as Index;
|
|
|
|
$checks = 0;
|
|
function priorityScopeExpect(bool $ok, string $why): void { ++$GLOBALS['checks']; if (!$ok) throw new RuntimeException($why); }
|
|
$cases = [
|
|
[
|
|
['s2', 'present_illness', 'current', '2026-02-12', '步行后倦怠较明显,休息可缓;饮水常分多次,每次量未测。'],
|
|
['s19', 'content', 'historical', '2026-02-12', '既有检验记录:2026-02-10 ALT 20U/L,肌酐75μmol/L。'],
|
|
],
|
|
[
|
|
['s47', 'present_illness', 'current', '2025-07-08', '晨起眩晕每周约2次;单次饮水约150mL,全天总量未记录。'],
|
|
['s90', 'past_history', 'current', '2025-07-08', '本次询问无已知制剂辅料过敏;随访变化未记录。'],
|
|
['s106', 'content', 'historical', '', '历史检查记录:2025-06-30血红蛋白130g/L;其他检验种类未记录。'],
|
|
],
|
|
];
|
|
$emptyPrompt = Index::prompt([]);
|
|
$sourceIndependentPolicy = substr($emptyPrompt, 0, strpos($emptyPrompt, '<SOURCE_EXAM_ROLE_INDEX>'));
|
|
foreach ($cases as $case) {
|
|
$ledger = ['sources' => [], 'assertions' => []];
|
|
foreach ($case as [$id, $field, $scope, $date, $quote]) $ledger['sources'][$id] = [
|
|
'id' => $id, 'field' => $field, 'path' => $scope . '.' . $field . '.' . $id,
|
|
'scope' => $scope, 'date' => $date, 'quote' => $quote];
|
|
$originalLedger = serialize($ledger);
|
|
$index = Index::build($ledger); $originalIndex = serialize($index);
|
|
$prompt = Index::prompt($index);
|
|
priorityScopeExpect(serialize($ledger) === $originalLedger && serialize($index) === $originalIndex,
|
|
'Qualitative/quantitative findings and baseline laboratory source payloads are never rewritten by policy generation.');
|
|
preg_match('~<SOURCE_EXAM_ROLE_INDEX>\s*(.*?)\s*</SOURCE_EXAM_ROLE_INDEX>~su', $prompt, $match);
|
|
$rows = json_decode($match[1], true, 64, JSON_THROW_ON_ERROR);
|
|
priorityScopeExpect(count($rows) === count($case), 'No source disappears when the instructions distinguish known and missing portions.');
|
|
foreach ($rows as $row) {
|
|
$source = $ledger['sources'][$row['id']];
|
|
priorityScopeExpect($row['field'] === $source['field'] && $row['path'] === $source['path']
|
|
&& $row['scope'] === $source['scope'] && $row['date'] === $source['date']
|
|
&& implode('', array_column($row['spans'], 'quote')) === $source['quote'],
|
|
'Exact activity qualifiers, per-occasion/total values, negative qualifiers and embedded lab dates survive unchanged.');
|
|
}
|
|
$policy = substr($prompt, 0, strpos($prompt, '<SOURCE_EXAM_ROLE_INDEX>'));
|
|
foreach (['每个主要治法限定语及既定优先顺序', '其实际功用怎样支持', '不能只声称‘一致’或‘覆盖’',
|
|
'不把作用相近的不同治法视为同义', '不能为迁就方名倒改第五节', '任一主要限定或优先目标无法解释时,不列方名药味,说明缺证点及所需补采',
|
|
'定性程度、活动或诱发关系、饮水方式及既有检验项目和原日期', '缺量化值须明确每次、全天或具体时段,缺检查须明确检验种类',
|
|
'不能因定量或特定检验缺失,把已有程度、摄入方式或实验室基线整体改成未知'] as $rule) {
|
|
priorityScopeExpect(str_contains($policy, $rule), 'Both differing source cases receive the same generic explanation/scope requirement: ' . $rule);
|
|
}
|
|
priorityScopeExpect(str_contains($policy, '完整句或分号分隔的完整片段')
|
|
&& str_contains($policy, '保留时间、询问或记录语境、已知及程度等限定词,不能只摘否认核心')
|
|
&& str_contains($policy, '原有全部资料仍须阅读') && str_contains($policy, '索引不包含新的诊断规则'),
|
|
'Strict canonical quotation, complete original-source reading and the no-new-diagnostic-rule boundary remain intact.');
|
|
priorityScopeExpect($policy === $sourceIndependentPolicy,
|
|
'The reasoning policy is identical for unrelated source payloads and supplies no case-dependent selection or answer prefill.');
|
|
}
|
|
$pulseSource = ['id' => 's7', 'field' => 'pulse', 'path' => 'current.pulse', 'scope' => 'current',
|
|
'date' => '2026-03-04', 'quote' => '脉缓而有力。'];
|
|
$palpationSource = ['id' => 's8', 'field' => 'content', 'path' => 'current.doctor_note.content', 'scope' => 'current',
|
|
'date' => '2026-03-04', 'quote' => '切诊前臂触温偏凉。'];
|
|
$historicalPalpation = array_replace($palpationSource, ['path' => 'historical.doctor_note.content',
|
|
'scope' => 'historical', 'date' => '2025-04-02']);
|
|
foreach ([[$pulseSource], [$pulseSource, $palpationSource], [$pulseSource, $historicalPalpation]] as $caseNumber => $sources) {
|
|
$ledger = ['sources' => array_column($sources, null, 'id')]; $before = serialize($ledger);
|
|
$index = Index::build($ledger); $prompt = Index::prompt($index);
|
|
$policy = substr($prompt, 0, strpos($prompt, '<SOURCE_EXAM_ROLE_INDEX>'));
|
|
foreach (['望诊', '闻诊', '问诊', '切诊', '舌象', '脉象'] as $label) priorityScopeExpect(str_contains($policy, '“' . $label . ':”'),
|
|
'Every exact mandatory prefix is specified even when its source role has no observation.');
|
|
priorityScopeExpect(str_contains($policy, '每项独立段落且不得省略;六项齐全后才能开始第二节八纲')
|
|
&& str_contains($policy, '独立脉象段不能代替切诊段'),
|
|
'The second-section boundary requires all six independent paragraphs; pulse never substitutes for palpation.');
|
|
priorityScopeExpect(str_contains($policy, '仅当本次确无其他已记录切诊内容时,仍须独立写“切诊:其他触诊未记录,脉象见独立脉象项。”')
|
|
&& str_contains($policy, '有其他本次切诊记录则按原日期、原文与sN引用,不得改成未记录'),
|
|
'The closed missing-palpation metadata is conditional and cannot erase an actual current palpation observation.');
|
|
$hasCurrentPalpation = false;
|
|
foreach ($index['sources'] as $source) {
|
|
priorityScopeExpect($source['quote'] === $ledger['sources'][$source['id']]['quote']
|
|
&& $source['scope'] === $ledger['sources'][$source['id']]['scope']
|
|
&& $source['date'] === $ledger['sources'][$source['id']]['date'], 'Mandatory roles do not rewrite source text or time/scope.');
|
|
foreach ($source['spans'] as $span) if ($source['scope'] === 'current' && $span['role'] === 'palpation' && $span['recorded']) $hasCurrentPalpation = true;
|
|
}
|
|
priorityScopeExpect(serialize($ledger) === $before && $hasCurrentPalpation === ($caseNumber === 1)
|
|
&& $index['sources']['s7']['spans'][0]['role'] === 'pulse',
|
|
'Pulse-only and historical-only cases remain distinct from a real current palpation source; no observation is fabricated.');
|
|
}
|
|
priorityScopeExpect(!class_exists('app\\adminapi\\service\\TcmClinicalFactStateGuard', false)
|
|
&& !class_exists('app\\adminapi\\service\\TcmClinicalEvidenceContract', false),
|
|
'Pure input tests do not load concurrent validation workers or claim model-output acceptance.');
|
|
echo "TCM priority/scope input prompt: {$checks} assertions passed (pure Index only; shared integration deferred).\n";
|