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~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"; }