self::$range, 'section_order' => $answer === '' ? [] : self::$sectionOrder];
}
}
}
namespace {
require dirname(__DIR__) . '/vendor/autoload.php';
use app\adminapi\logic\tcm\DiagnosisAiLogic as Logic;
use app\adminapi\service\TcmLongReportContract as Metadata;
$checks = 0;
function focusedExpect(bool $ok, string $message): void
{
++$GLOBALS['checks'];
if (!$ok) throw new \RuntimeException($message);
}
function focusedBlock(string $query, string $tag): array
{
focusedExpect(preg_match_all('~<' . $tag . '>\n(.*?)\n' . $tag . '>~su', $query, $matches) === 1,
'Exactly one intact JSON boundary: ' . $tag);
return [json_decode($matches[1][0], true, 512, JSON_THROW_ON_ERROR), $matches[1][0]];
}
$prepare = (new \ReflectionClass(Logic::class))->getMethod('prepareLongReportContinuation');
$flags = JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP | JSON_THROW_ON_ERROR;
$sourceQuote = '本次询问无已知制剂辅料过敏;字段含&原文。';
$sources = [
's21' => ['id' => 's21', 'field' => 'present_illness', 'scope' => 'current', 'date' => '2026-06-12', 'quote' => $sourceQuote],
's22' => ['id' => 's22', 'field' => 'content', 'scope' => 'historical', 'date' => '2024-08-03', 'quote' => '历史询问无已知制剂反应。'],
's23' => ['id' => 's23', 'field' => 'tongue', 'scope' => 'current', 'date' => '', 'quote' => '未记录'],
];
$originalSources = json_encode(['all_original_categories' => ['A', 'B'], 'sources' => $sources], $flags);
$question = '原始问题:完整六节1800至2200字;原来源全部保留。';
$single = ['task' => 'custom', 'profile' => 'qwen', 'upstream_timeout' => 90,
'query' => "\n" . $originalSources . "\n\n\n" . $question . "\n",
'inputs' => ['herbs_json' => $originalSources, 'raw_bytes' => "a\r\nb\n"],
'files' => [['type' => 'document', 'url' => 'https://synthetic.invalid/original.pdf']],
'tcm_evidence_guard' => ['fact_state_ledger' => ['sources' => $sources]],
'_clinical_evidence_index' => ['valid' => true, 'unchanged_supplement' => "索引\r\n原字节"],
'_report_stage' => 'single', '_report_deadline_ns' => 123456789];
$prefix = "一、四诊\r\n\r\n工程输入测试前缀;仅检验原字节。\r\n\r\n二、八纲\n\n";
$rejected = "工程被拒片段&\"\n\n";
$issues = [
['code' => 'NEGATION_SCOPE_WIDENED', 'source_id' => 's21'],
['code' => 'NEGATION_SCOPE_WIDENED', 'source_id' => 's21'],
['code' => 'NEGATION_SCOPE_WIDENED', 'source_id' => 's22'],
['code' => 'NEGATION_SCOPE_WIDENED', 'source_id' => 's99'],
['code' => '未知码&', 'source_id' => 's23'],
];
$original = serialize([$single, $prefix, $rejected, $issues]);
$result = $prepare->invoke(null, $single, $prefix, $rejected, $issues);
[$data, $rawData] = focusedBlock($result['query'], 'REPORT_CONTINUATION_DATA');
[$focus, $rawFocus] = focusedBlock($result['query'], 'REPORT_CONTINUATION_FOCUS');
$prefixCharacters = mb_strlen($prefix, 'UTF-8');
$expectedSources = [];
foreach (['s21', 's22', 's23'] as $id) $expectedSources[] = ['source_id' => $id, 'field' => $sources[$id]['field'],
'scope' => $sources[$id]['scope'], 'date' => $sources[$id]['date'], 'quote' => $sources[$id]['quote']];
$expectedData = ['delivered_prefix' => $prefix, 'delivered_characters' => $prefixCharacters,
'original_whole_report_range' => [1800, 2200], 'remaining_suffix_character_range' => [1800 - $prefixCharacters, 2200 - $prefixCharacters],
'resume_existing_section' => 2, 'already_delivered_section_headings' => [1, 2], 'restart_undelivered_section' => null,
'rejected_undelivered_unit' => $rejected, 'issues' => $issues, 'issue_source_quotations' => $expectedSources];
focusedExpect($data === $expectedData && $rawData === json_encode($expectedData, $flags),
'Every original correction field and its ordering, source/date/quote and prefix/rejection bytes remain exactly serialized as before.');
focusedExpect(str_starts_with($result['query'], $single['query'])
&& serialize([$single, $prefix, $rejected, $issues]) === $original,
'Appending a repair action cannot rewrite the original source/question or caller inputs.');
foreach (['inputs', 'files', 'profile', 'upstream_timeout', 'tcm_evidence_guard', '_clinical_evidence_index', '_report_deadline_ns'] as $key) {
focusedExpect($result[$key] === $single[$key], 'Delivery inputs, channel, budget and index are copied unchanged: ' . $key);
}
focusedExpect($result['_validated_report_prefix'] === $prefix && $result['_report_stage'] === 'continuation',
'The exact accepted prefix remains separate from the repair prompt.');
focusedExpect(strpos($result['query'], '') < strpos($result['query'], '')
&& str_contains(substr($result['query'], strpos($result['query'], '')), '当前唯一动作:'),
'The focused data and final active instruction follow the entire unchanged original JSON.');
focusedExpect($focus['active_issue_codes'] === ['NEGATION_SCOPE_WIDENED', '未知码&']
&& $focus['resume_existing_section'] === 2 && $focus['restart_undelivered_section'] === null
&& $focus['remaining_suffix_character_range'] === $data['remaining_suffix_character_range'],
'Only actual codes are listed and the existing section/remaining range are copied without a second budget.');
focusedExpect($focus['current_source_references'] === [['source_id' => 's21', 'reference_text' => '相关原文见s21']],
'Only an actual affected current source gets a neutral locator; duplicates, missing IDs and historical sources do not.');
$action = substr($result['query'], strpos($result['query'], ''));
focusedExpect(str_contains($action, '接续第2节正文') && str_contains($action, '后节只回指实际相关原来源')
&& str_contains($action, '已记录阳性证据解释推断及局限') && str_contains($action, '不得复用被拒否认段落')
&& !str_contains($action, $sourceQuote) && !str_contains($action, $rejected),
'The focused repair supplies an action, not a composed clinical replacement or another copy of the bad paragraph.');
focusedExpect(str_contains($rawData, '\\u003C/REPORT_CONTINUATION_DATA\\u003E')
&& str_contains($rawFocus, '\\u003C/REPORT_CONTINUATION_FOCUS\\u003E')
&& str_contains($rawFocus, '\\u0026') && substr_count($result['query'], '') === 1,
'Source/rejected/code injection stays escaped data; decoded original bytes remain intact.');
foreach ([[], [['code' => '', 'source_id' => 's21']], [['code' => 'FUTURE_CODE', 'source_id' => 's21']]] as $otherIssues) {
$other = $prepare->invoke(null, $single, $prefix, $rejected, $otherIssues);
[$otherData] = focusedBlock($other['query'], 'REPORT_CONTINUATION_DATA');
[$otherFocus] = focusedBlock($other['query'], 'REPORT_CONTINUATION_FOCUS');
$otherAction = substr($other['query'], strpos($other['query'], ''));
focusedExpect($otherData['issues'] === $otherIssues && $otherFocus['current_source_references'] === []
&& !str_contains($otherAction, '本次修复否认范围错误'),
'Empty/unknown issues preserve raw data and do not invent a denial-repair branch or affected source.');
focusedExpect($otherFocus['active_issue_codes'] === ($otherIssues !== [] && $otherIssues[0]['code'] !== '' ? ['FUTURE_CODE'] : []),
'The final focus does not expand a generic error-code catalog.');
}
Metadata::$sectionOrder = [1];
$first = $prepare->invoke(null, $single, "一、四诊\n\n", $rejected, [['code' => 'NEGATION_SCOPE_WIDENED', 'source_id' => 's21']]);
$firstAction = substr($first['query'], strpos($first['query'], ''));
focusedExpect(str_contains($firstAction, '接续第1节正文') && str_contains($firstAction, '对应采集角色、原日期、完整原文引号及真实sN')
&& !str_contains($firstAction, '本次修复否认范围错误'),
'A first-section failure keeps the exact role/date/quotation grammar instead of switching to later source references.');
Metadata::$sectionOrder = [1, 2, 3, 4, 5];
foreach (['REPORT_FINAL_CONTRACT_INVALID' => false, 'REPORT_FINAL_SECTION_UNDELIVERED' => true] as $code => $restart) {
$tail = $prepare->invoke(null, $single, $prefix, $rejected, [['code' => $code, 'source_id' => '']]);
[$tailFocus] = focusedBlock($tail['query'], 'REPORT_CONTINUATION_FOCUS');
$tailAction = substr($tail['query'], strpos($tail['query'], ''));
focusedExpect($tailFocus['resume_existing_section'] === ($restart ? 6 : 5)
&& $tailFocus['restart_undelivered_section'] === ($restart ? 6 : null)
&& str_contains($tailAction, $restart ? '从“六、中药方药及复核监测”标题重写' : '接续第5节正文'),
'Section six restarts only under the original undelivered-six flag; a generic shape failure resumes its existing section.');
}
Metadata::$sectionOrder = [1, 2];
foreach ([[0, 0], [0, null], [null, 30], [null, null], null] as $remaining) {
Metadata::$range = $remaining === null ? null : array_map(static fn ($bound) => $bound === null ? null : $bound + $prefixCharacters, $remaining);
$bounded = $prepare->invoke(null, $single, $prefix, $rejected, []);
[$boundedData] = focusedBlock($bounded['query'], 'REPORT_CONTINUATION_DATA');
[$boundedFocus] = focusedBlock($bounded['query'], 'REPORT_CONTINUATION_FOCUS');
$boundedAction = substr($bounded['query'], strpos($bounded['query'], ''));
focusedExpect($boundedData['remaining_suffix_character_range'] === $remaining
&& $boundedFocus['remaining_suffix_character_range'] === $remaining,
'Zero and null limits retain their exact existing remaining-range semantics.');
focusedExpect(str_contains($boundedAction, 'null表示未规定而非0,0必须按0执行')
&& str_contains($boundedAction, $remaining === [0, 0] ? '剩余上限为0,不输出新增字符' : '接续第2节正文')
&& ($remaining !== [0, 0] || !str_contains($boundedAction, '现在只输出该位置修复后的剩余报告正文')),
'An exhausted range permits no added text, while an unspecified upper limit is never silently treated as zero.');
}
Metadata::$range = [1800, 2200];
$qualifiedQuote = '问诊记录:引号"、反斜杠\\、&,无明显甲、乙;后句';
$qualifiedDenial = '无明显甲、乙';
$qualifiedStart = strpos($qualifiedQuote, $qualifiedDenial);
$qualifiedSingle = $single;
$qualifiedSingle['tcm_evidence_guard']['fact_state_ledger']['sources']['s21']['quote'] = $qualifiedQuote;
$qualifiedSingle['_clinical_evidence_index'] = ['valid' => true,
'version' => \app\adminapi\service\TcmClinicalEvidenceIndex::VERSION, 'sources' => ['s21' => [
'id' => 's21', 'path' => 'current.present_illness', 'field' => 'present_illness', 'scope' => 'current',
'date' => '2026-06-12', 'quote' => $qualifiedQuote, 'record_state' => 'recorded',
'negative_assertions' => [['quote' => $qualifiedDenial, 'start' => $qualifiedStart,
'end' => $qualifiedStart + strlen($qualifiedDenial), 'scope' => 'current', 'date' => '2026-06-12']],
]]];
$qualifiedRejected = "无甲及乙。\r\n&\"\\";
$qualifiedIssues = [['code' => 'NEGATION_SCOPE_WIDENED', 'source_id' => 's21']];
$qualifiedBefore = serialize([$qualifiedSingle, $prefix, $qualifiedRejected, $qualifiedIssues]);
$with = $prepare->invoke(null, $qualifiedSingle, $prefix, $qualifiedRejected, $qualifiedIssues);
$unavailable = $qualifiedSingle;
$unavailable['_clinical_evidence_index']['valid'] = false;
$without = $prepare->invoke(null, $unavailable, $prefix, $qualifiedRejected, $qualifiedIssues);
[$context, $rawContext] = focusedBlock($with['query'], 'QUALIFIED_DENIAL_REPAIR_CONTEXT');
$newBlock = "\n\n" . $rawContext . "\n";
focusedExpect(str_replace($newBlock, '', $with['query']) === $without['query'],
'The new block is the only added query bytes; the existing complete query, DATA and final FOCUS remain identical.');
focusedExpect(strpos($with['query'], '') < strpos($with['query'], '')
&& strpos($with['query'], '') < strpos($with['query'], ''),
'The escaped source data is appended between the two existing blocks, keeping the original active action last.');
focusedExpect(focusedBlock($with['query'], 'REPORT_CONTINUATION_DATA') === focusedBlock($without['query'], 'REPORT_CONTINUATION_DATA')
&& focusedBlock($with['query'], 'REPORT_CONTINUATION_FOCUS') === focusedBlock($without['query'], 'REPORT_CONTINUATION_FOCUS'),
'Both prior JSON blocks have strict decoded and serialized equality.');
focusedExpect(serialize([$qualifiedSingle, $prefix, $qualifiedRejected, $qualifiedIssues]) === $qualifiedBefore,
'Source/index/input/prefix/rejected/issues bytes and ordering remain unchanged after assembly.');
foreach ($qualifiedSingle as $key => $value) {
if ($key === 'query' || $key === '_report_stage') continue;
focusedExpect($with[$key] === $value, 'Every inherited input, file, budget, profile and source field is unchanged: ' . $key);
}
focusedExpect($context['rejected_unit_sha256'] === hash('sha256', $qualifiedRejected)
&& $context['matched_candidates'][0]['rejected_phrase'] === '无甲'
&& $context['matched_candidates'][0]['canonical_clause'] === substr($qualifiedQuote, 0, strpos($qualifiedQuote, ';')),
'Only an exact rejected lexical atom and full canonical source fragment enter the new data.');
focusedExpect(str_contains($rawContext, '\\u003C/QUALIFIED_DENIAL_REPAIR_CONTEXT\\u003E')
&& str_contains($rawContext, '\\u0026') && substr_count($with['query'], '') === 1,
'Source and rejected-boundary injection cannot close the supplemental JSON block.');
focusedExpect(substr($with['query'], strpos($with['query'], ''))
=== substr($without['query'], strpos($without['query'], '')),
'No new clinical instruction, formula preference or edited denial is appended to the existing final action.');
foreach ([0, null] as $upper) {
Metadata::$range = [$prefixCharacters, $upper === null ? null : $prefixCharacters];
$bound = $prepare->invoke(null, $qualifiedSingle, $prefix, $qualifiedRejected, $qualifiedIssues);
[$boundFocus] = focusedBlock($bound['query'], 'REPORT_CONTINUATION_FOCUS');
focusedExpect($boundFocus['remaining_suffix_character_range'] === [0, $upper]
&& ($upper !== 0 || str_contains($bound['query'], '剩余上限为0,不输出新增字符')),
'A valid supplement cannot enlarge zero/null remaining suffix bounds.');
}
Metadata::$range = [1800, 2200];
Metadata::$sectionOrder = [1];
$firstQualified = $prepare->invoke(null, $qualifiedSingle, $prefix, $qualifiedRejected, $qualifiedIssues);
focusedExpect(!str_contains($firstQualified['query'], ''),
'The valid index still adds no later-section source supplement to section one.');
Metadata::$sectionOrder = [1, 2, 3, 4, 5];
$restartQualified = $prepare->invoke(null, $qualifiedSingle, $prefix, $qualifiedRejected,
array_merge($qualifiedIssues, [['code' => 'REPORT_FINAL_SECTION_UNDELIVERED', 'source_id' => '']]));
[$restartFocus] = focusedBlock($restartQualified['query'], 'REPORT_CONTINUATION_FOCUS');
focusedExpect($restartFocus['resume_existing_section'] === 6 && $restartFocus['restart_undelivered_section'] === 6
&& str_contains($restartQualified['query'], '从“六、中药方药及复核监测”标题重写'),
'Supplement insertion preserves the existing undelivered-six restart flag and action.');
focusedExpect((new \ReflectionClass(Metadata::class))->getFileName() === __FILE__
&& !class_exists('app\\adminapi\\service\\TcmClinicalFactStateGuard', false)
&& !class_exists('app\\adminapi\\service\\TcmClinicalEvidenceContract', false)
&& !class_exists('app\\common\\service\\DifyChatService', false),
'Only input preparation runs with supplied range/section metadata; no mutable peer checker, model transport or report acceptance runs.');
echo "TCM focused continuation input: {$checks} assertions passed (pure assembly, metadata stub only).\n";
}