(new ReflectionMethod(Logic::class, $method))->invoke(null, ...$args); $project = static fn (array $index): array => $call('initialQualifiedDenialConstraints', $index); $flags = JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP | JSON_THROW_ON_ERROR; $encode = static fn (array $data): string => json_encode($data, $flags); $snapshot = (new ReflectionMethod(Patient::class, 'buildSourceSnapshotFromRows'))->invoke(null, CompleteTcmCase::rows()); $submission = (new ReflectionMethod(Patient::class, 'sanitizeSnapshotForUpstream')) ->invoke(null, AiRequiredSource::submission($snapshot)); $ledger = Guard::build(CompleteTcmCase::current(), $submission); $index = Index::build($ledger); $original = serialize([$snapshot, $submission, $ledger, $index]); $result = $project($index); $expect($result['available'] === true && count($result['constraints']) === 2, 'Exactly two initial qualified clauses.'); $expect(array_unique(array_column($result['constraints'], 'source_id')) === ['s2'], 'No same-day historical s14 promotion.'); foreach ([[189, 243, 189, 228, 192, 198], [380, 434, 407, 434, 410, 416]] as $n => $span) { $entry = $result['constraints'][$n]; $assertion = $entry['qualified_assertions'][0]; $expect([$entry['canonical_clause_start'], $entry['canonical_clause_end'], $assertion['source_denial_start'], $assertion['source_denial_end'], $assertion['source_degree_start'], $assertion['source_degree_end']] === $span, 'Exact actual UTF-8 canonical/denial/degree intervals.'); $expect(substr($index['sources']['s2']['quote'], $span[0], $span[1] - $span[0]) === $entry['canonical_clause'] && substr($index['sources']['s2']['quote'], $span[2], $span[3] - $span[2]) === $assertion['source_denial'] && substr($index['sources']['s2']['quote'], $span[4], $span[5] - $span[4]) === '明显', 'Original bytes, no reconstructed clinical text.'); $expect($entry['reference_text'] === '相关原文见s2' && $entry['later_section_use'] === 'source_reference_only' && $entry['constraint_kind'] === 'qualified_negative_literal_conservation_v1', 'Typed neutral source use.'); } $expect($project($index) === $result && strlen($encode($result)) <= 4096, 'Deterministic complete bounded projection.'); $make = static function (string $quote, array $denials, string $path = 'current.present_illness') use ($index): array { $source = $index['sources']['s2']; $source['quote'] = $quote; $source['path'] = $path; $source['negative_assertions'] = []; foreach ($denials as $denial) { $start = strpos($quote, $denial); if ($start === false) throw new RuntimeException('Bad synthetic source fixture.'); $source['negative_assertions'][] = ['quote' => $denial, 'scope' => 'current', 'date' => $source['date'], 'start' => $start, 'end' => $start + strlen($denial)]; } return ['valid' => true, 'version' => Index::VERSION, 'sources' => ['s2' => $source]]; }; $mutations = [ static function (array &$i): void { $i['valid'] = 1; }, static function (array &$i): void { $i['version'] = 'future'; }, static function (array &$i): void { $i['sources'] = null; }, static function (array &$i): void { $i['sources']['s2']['id'] = 's3'; }, static function (array &$i): void { $i['sources']['s2']['scope'] = null; }, static function (array &$i): void { $i['sources']['s2']['record_state'] = 'unknown'; }, static function (array &$i): void { $i['sources']['s2']['path'] = 'history.0.present_illness'; }, static function (array &$i): void { $i['sources']['s2']['field'] = 'past_history'; }, static function (array &$i): void { $i['sources']['s2']['date'] = '2026-02-30'; }, static function (array &$i): void { $i['sources']['s2']['date'] = ''; }, static function (array &$i): void { $i['sources']['s2']['quote'] .= "\xFF"; }, static function (array &$i): void { $i['sources']['s2']['negative_assertions'] = null; }, static function (array &$i): void { $i['sources']['s2']['negative_assertions'][] = 'bad-row'; }, static function (array &$i): void { $i['sources']['s2']['negative_assertions'][0]['scope'] = 'historical'; }, static function (array &$i): void { $i['sources']['s2']['negative_assertions'][0]['date'] = '2026-10-22'; }, static function (array &$i): void { $i['sources']['s2']['negative_assertions'][0]['start'] = '189'; }, static function (array &$i): void { ++$i['sources']['s2']['negative_assertions'][0]['start']; }, static function (array &$i): void { $i['sources']['s2']['negative_assertions'][0]['end'] = PHP_INT_MAX; }, static function (array &$i): void { $i['sources']['s2']['negative_assertions'][0]['quote'] .= '改'; }, ]; foreach ($mutations as $n => $mutate) { $bad = $index; $mutate($bad); $out = $project($bad); $expect($out['available'] === false && $out['constraints'] === [] && is_string($out['reason']), 'All-or-none invalid metadata ' . $n); } $historical = $make('无明显甲', ['无明显甲']); $historical['sources']['s2']['scope'] = 'historical'; $expect($project($historical) === ['available' => true, 'constraints' => []], 'Historical source wins over current assertion label.'); foreach (['无已知甲', '无最近甲', '无本次甲', '无极度甲', '甲不详', '未记录甲'] as $unsupported) { $expect($project($make($unsupported, [$unsupported])) === ['available' => true, 'constraints' => []], 'No fabricated degree classification: ' . $unsupported); } foreach (['明显', '显著', '严重', '剧烈', '强烈', '轻微', '大量', '少量', '频繁', '持续', '淋漓', '明显持续'] as $degree) { $out = $project($make('原记录:无' . $degree . '甲、乙,无丙', ['无' . $degree . '甲、乙'])); $expect($out['available'] === true && $out['constraints'][0]['canonical_clause'] === '原记录:无' . $degree . '甲、乙,无丙' && implode('', $out['constraints'][0]['qualified_assertions'][0]['source_degree_tokens']) === $degree, 'Finite literal degree vocabulary.'); } $custom = $make('无明显甲、无显著乙', ['无明显甲', '无显著乙']); $dup = $custom; $dup['sources']['s2']['negative_assertions'] = array_merge($dup['sources']['s2']['negative_assertions'], $dup['sources']['s2']['negative_assertions']); $expect($project($dup) === $project($custom) && count($project($dup)['constraints']) === 1 && count($project($dup)['constraints'][0]['qualified_assertions']) === 2, 'Exact duplicates collapse; two distinct atoms retain one complete clause.'); $dup['sources']['s2']['negative_assertions'][2]['topic'] = 'conflict'; $expect($project($dup) === ['available' => false, 'reason' => 'ambiguous_assertion', 'constraints' => []], 'Conflicting duplicate metadata never silently wins.'); $cross = $make('无明显甲。无明显乙', ['无明显甲。无明显乙']); $expect(!$project($cross)['available'], 'Typed assertion crossing original sentence boundary is unavailable.'); foreach ([1024 => true, 1025 => false] as $bytes => $allowed) { $out = $project($make(str_repeat('x', $bytes - strlen('无明显甲')) . '无明显甲', ['无明显甲'])); $expect($out['available'] === $allowed && ($allowed || $out['constraints'] === []), 'Complete-clause byte cap ' . $bytes); } foreach ([4 => true, 5 => false] as $total => $allowed) { $atoms = array_map(static fn (int $n): string => '无明显甲' . $n, range(0, $total - 1)); $out = $project($make(implode('。', $atoms), $atoms)); $expect($out['available'] === $allowed && ($allowed ? count($out['constraints']) === 4 : $out['constraints'] === []), 'All-or-none distinct-clause cap ' . $total); } $sizeBase = $make('无明显甲', ['无明显甲'], 'current.p.present_illness'); $padding = 4096 - strlen($encode($project($sizeBase))); $at = $project($make('无明显甲', ['无明显甲'], 'current.' . str_repeat('p', $padding + 1) . '.present_illness')); $over = $project($make('无明显甲', ['无明显甲'], 'current.' . str_repeat('p', $padding + 2) . '.present_illness')); $expect($at['available'] && strlen($encode($at)) === 4096 && $over === ['available' => false, 'reason' => 'projection_budget', 'constraints' => []], 'Exact 4096/4097 escaped projection bytes, with no clipping.'); $injection = '原记录:&"\\,无明显甲、乙'; $escapedIndex = $make($injection, ['无明显甲、乙']); $escaped = $project($escapedIndex); $prompt = Index::prompt($escapedIndex, $escaped); $expect(json_decode($encode($escaped), true, 512, JSON_THROW_ON_ERROR) === $escaped && substr_count($prompt, '') === 1 && str_contains($prompt, '\\u003C/CURRENT_SIX_EXAM_SELECTION_MAP\\u003E') && str_contains($prompt, '\\u0026'), 'Tag/ampersand/quote/backslash survive decoding without closing the data boundary.'); // Load exact reviewed before snapshots under independent names, without bootstrapping the app. $artifact = dirname(__DIR__, 2) . '/app/artifacts/ai_concurrency/'; foreach (['TcmClinicalEvidenceIndex' => '7eb6500fb5e58554c930c72b26ce6677f797aaaeccd97f7f39df3a63a8c60832', 'DiagnosisAiLogic' => '91f01bad7fb2b3890cd2425371cb4eb5c02bd3878803f06e5949b8ecdd3b2fb7'] as $name => $hash) { $path = $artifact . 'v43r25_before_' . $name . '.php.snapshot'; $expect(hash_file('sha256', $path) === $hash, 'Exact before snapshot identity.'); $code = (string) file_get_contents($path); $code = str_replace('class ' . $name, 'class R25Before' . $name, $code); if ($name === 'DiagnosisAiLogic') $code = str_replace('use app\\adminapi\\service\\TcmClinicalEvidenceIndex;', 'use app\\adminapi\\service\\R25BeforeTcmClinicalEvidenceIndex as TcmClinicalEvidenceIndex;', $code); eval(substr($code, 5)); } $beforeLogic = 'app\\adminapi\\logic\\tcm\\R25BeforeDiagnosisAiLogic'; $old = static fn (string $method, ...$args) => (new ReflectionMethod($beforeLogic, $method))->invoke(null, ...$args); $sourceJson = json_encode($submission, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR); $expect(hash('sha256', $sourceJson) === '41adfddb9245055c65eea64b31b2d57ed2aaa8ff0cf4531da4a67d8b6d762f76' && count($submission['required_content']) === 13, 'Exact original complete source projection and all 13 sections.'); $probe = (string) file_get_contents($artifact . 'private_tcm_r25_acceptance_probe.php'); preg_match('/\$question=\x27([^\x27]+)\x27;/u', $probe, $questionMatch); $question = $questionMatch[1]; $expect(hash('sha256', $question) === '28fbaee8585d4fbde3be3b1a6ee046c1a60f18a1fe67c0769211b08afdd5d306', 'Exact original immutable question.'); $context = $call('buildCaseContext', CompleteTcmCase::current(), 0, []); $context['case_lines'] = ['独立合成测试病例,非真实患者;全量来源:' . $sourceJson]; $context['required_sections'] = AiRequiredSource::manifest($snapshot); $context['source_summary'] = $snapshot['source_summary']; $context['source_summary']['required_sections'] = $context['required_sections']; $context['case_lines'][] = $call('requiredSectionsPrompt', $context['required_sections']); $context['case_text'] = implode("\n", $context['case_lines']); $context['case_json'] = $sourceJson; $context['context_scope'] = 'synthetic_local_test'; $context['context_version'] = 'patient-context-assistant-tcm-four-diagnoses-v43'; $context['source_diagnosis_ids'] = []; $context['files'] = []; $context['tcm_evidence_guard'] = TcmEvidenceGuard::assess(CompleteTcmCase::current(), $submission); $query = $call('buildAssistantPrompt', $context, 'custom', $question); $inputs = $call('buildUpstreamInputs', $context, '病例问诊助手', $context['context_version']); $expect($query === $old('buildAssistantPrompt', $context, 'custom', $question) && $inputs === $old('buildUpstreamInputs', $context, '病例问诊助手', $context['context_version']), 'Initial raw query/inputs are unchanged before delivery supplement.'); $prepared = ['query' => $query, 'inputs' => $inputs, 'files' => [], 'profile' => 'qwen', 'user' => 'offline-synthetic', 'upstream_timeout' => 90, 'tcm_evidence_guard' => $context['tcm_evidence_guard']]; $single = $call('prepareLongReportDelivery', $prepared); $oldSingle = $old('prepareLongReportDelivery', $prepared); $expect($single['_clinical_evidence_index'] === $index, 'Actual prepared path consumes the complete fixture ledger.'); preg_match('~\n(.*?)\n~s', $single['query'], $initialMap); $expect(json_decode($initialMap[1], true, 512, JSON_THROW_ON_ERROR)['current_qualified_denial_constraints'] === $result, 'Exact typed two-clause projection is present before first generation.'); $schema = 'current_qualified_denial_constraints仅标记原始阴性的程度字节;有程度限定不等于绝对无,后节用其reference_text回指,不另改述阴性;例外重引仍须完整原句及原限定。available=false只表示补充索引不可用,全部原资料及保真规则仍适用。'; $strip = static function (string $text) use ($schema): string { $text = str_replace($schema, '', $text); return preg_replace_callback('~\n(.*?)\n~s', static function (array $m): string { $map = json_decode($m[1], true, 512, JSON_THROW_ON_ERROR); unset($map['current_qualified_denial_constraints']); return "\n" . json_encode($map, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_THROW_ON_ERROR) . "\n"; }, $text); }; $expect($strip($single['query']) === $oldSingle['query'] && substr_count($single['query'], $schema) === 1, 'Only the typed selection-map member and exact schema explanation change the complete initial prepared query.'); $withoutQuery = $single; $withoutOldQuery = $oldSingle; unset($withoutQuery['query'], $withoutOldQuery['query']); $expect($withoutQuery === $withoutOldQuery && str_starts_with($single['query'], $query), 'Every other prepared field and complete original query retained.'); $saved = json_decode((string) file_get_contents($artifact . 'tcm_complete_v43r24_single_qwen_20261024_0_qwen.rejected.json'), true, 512, JSON_THROW_ON_ERROR); $body = str_replace("\r\n", "\n", (string) file_get_contents($artifact . 'tcm_complete_v43r24_single_qwen_20261024_0_qwen.md')); $prefix = substr($body, strpos($body, '一、四诊')); $expect(mb_strlen($prefix, 'UTF-8') === 494, 'Exact original delivered prefix.'); $unit = $saved['rejections'][0]; $continued = $call('prepareLongReportContinuation', $single, $prefix, $unit['unit'], $unit['issues']); $oldContinued = $old('prepareLongReportContinuation', $oldSingle, $prefix, $unit['unit'], $unit['issues']); $expect($strip($continued['query']) === $oldContinued['query'] && str_starts_with($continued['query'], $single['query']), 'One continuation retains exactly the new initial supplement; all other continuation bytes are unchanged.'); foreach (['REPORT_CONTINUATION_DATA', 'QUALIFIED_DENIAL_REPAIR_CONTEXT', 'REPORT_CONTINUATION_FOCUS'] as $tag) { preg_match('~<' . $tag . '>\n(.*?)\n~s', $continued['query'], $m); $data = json_decode($m[1], true, 512, JSON_THROW_ON_ERROR); if ($tag === 'REPORT_CONTINUATION_DATA') $expect($data['delivered_prefix'] === $prefix && $data['rejected_undelivered_unit'] === $unit['unit'] && $data['issues'] === $unit['issues'] && $data['original_whole_report_range'] === [1800, 2200], 'Exact fixed prefix, rejected input, issues and original range.'); if ($tag === 'QUALIFIED_DENIAL_REPAIR_CONTEXT') $expect($data['rejected_unit_sha256'] === hash('sha256', $unit['unit']), 'Existing continuation context still bound to actual rejected bytes.'); if ($tag === 'REPORT_CONTINUATION_FOCUS') $expect($data['resume_existing_section'] === 2, 'Sole continuation starts actual section II.'); } $expect($continued['inputs'] === $inputs && $continued['files'] === [] && $continued['profile'] === 'qwen' && $continued['user'] === 'offline-synthetic' && $continued['upstream_timeout'] === 90, 'Original channel, inputs, attachments, identity and shared budget retained.'); $expect(serialize([$snapshot, $submission, $ledger, $index]) === $original, 'No source/index/fixture mutation.'); $expect(!class_exists('app\\common\\service\\DifyChatService', false) && !class_exists('think\\facade\\Db', false), 'No provider/config/database bootstrap.'); echo "TCM r25 independent initial qualified input: {$checks} assertions passed (pure original source + before/after preparation).\n";