'seg_01', 'start_ms' => 0, 'end_ms' => 180000, 'text' => $quote]]; $catalog = ['blood' => [['key' => 'systolic_pressure', 'label' => '收缩压(mmHg)', 'type' => 'number']]]; if (!class_exists(Prompt::class)) { $check('prompt_helper_exists', false); } else { $prompt = Prompt::build($quote, $segments, '2026-01-01 09:00:00', $catalog); $check('dynamic_today', str_contains($prompt, '"今天":"2026-01-01"')); $check('dynamic_yesterday_cross_year', str_contains($prompt, '"昨天":"2025-12-31"')); $check('dynamic_day_before_cross_year', str_contains($prompt, '"前天":"2025-12-30"')); $leap = Prompt::build($quote, $segments, '2024-03-01 09:00:00', $catalog); $check('dynamic_leap_date', str_contains($leap, '"昨天":"2024-02-29"')); $check('schema_version', str_contains($prompt, 'followup-audio-transcript-v2')); $check('canonical_text_preserved', str_contains($prompt, $quote)); $check('segment_identity_present', str_contains($prompt, '"segment_id":"seg_01"')); $check('catalog_passed_not_invented', str_contains($prompt, '"key":"systolic_pressure"') && !str_contains($prompt, 'fasting_blood_sugar')); foreach (['不得改写', '未提及', '家属', '否定', '疑问', '既往', '剂量', '最近', 'segment_id', '不是逐字', '不执行', '不要输出 transcript', '录制时间', 'date_text'] as $rule) { $check('rule:' . $rule, str_contains($prompt, $rule)); } $check('no_prefilled_items', str_contains($prompt, '"items":[]')); $check('explicit_outer_kind_enum', str_contains($prompt, '"allowed_kinds":["blood"]')); $check('no_duplicate_transcript_payload', !str_contains($prompt, '"transcript":') && !str_contains($prompt, '"segments":')); $check('evidence_ids_only', str_contains($prompt, 'evidence_ids') && str_contains($prompt, '不要复制引文')); $check('shared_max_citations', defined(Prompt::class . '::MAX_CITATIONS') && Prompt::MAX_CITATIONS === 20); $check('shared_max_segments', defined(Prompt::class . '::MAX_SEGMENTS') && Prompt::MAX_SEGMENTS === 1000); $check('prompt_limit_is_twenty', str_contains($prompt, '最多20个') && !str_contains($prompt, '最多30个') && !str_contains($prompt, '{{MAX_CITATIONS}}')); $check('date_abstention_rule', str_contains($prompt, '同窗口另一句的日期不能借给当前事件')); foreach (['主要患者', '照护者', '通话结尾', '联系方式', '机构', '地理位置', '去年', '听写', '猜最接近', '销售', '饥饿', '逐项肯定', '连问多个', '少吃', 'N点多', '应省略该精确字段', '星期和公历日期不一致'] as $rule) { $check('attribution_rule:' . $rule, str_contains($prompt, $rule)); } $check('deterministic_same_inputs', $prompt === Prompt::build($quote, $segments, '2026-01-01 09:00:00', $catalog)); $reject = false; try { Prompt::build($quote, $segments, 'not-a-date', $catalog); } catch (DomainException $error) { $reject = true; } $check('invalid_recorded_at_rejected', $reject); $silent = ['id' => 'seg_silent', 'start_ms' => 180000, 'end_ms' => 190000, 'text' => '']; $silencePrompt = ''; try { $silencePrompt = Prompt::build($quote . "\n", [$segments[0], $silent], '2026-01-01 09:00:00', $catalog); } catch (DomainException $error) { /* Unsupported silence in a baseline helper is a failed regression, not fixture replacement. */ } $check('silent_chunk_not_fabricated', !str_contains($silencePrompt, 'seg_silent') && str_contains($silencePrompt, $quote)); foreach ([ 'unknown_text' => [['id' => 's1', 'text' => '不存在的转写', 'start_ms' => 0, 'end_ms' => 100]], 'negative_start' => [array_replace($segments[0], ['start_ms' => -1])], 'reversed_range' => [array_replace($segments[0], ['start_ms' => 180000, 'end_ms' => 0])], 'duplicate_id' => [$segments[0], $segments[0]], 'missing_segments' => [], ] as $id => $invalid) { $reject = false; try { Prompt::build($quote, $invalid, '2026-01-01 09:00:00', $catalog); } catch (DomainException $error) { $reject = true; } $check('invalid_segments:' . $id, $reject); } } $failed = array_keys(array_filter($checks, static fn (bool $ok): bool => !$ok)); echo json_encode(['suite' => 'followup-audio-transcript-prompt', 'synthetic_only' => true, 'checks' => $checks, 'passed' => count($checks) - count($failed), 'total' => count($checks), 'failures' => $failed], JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR) . PHP_EOL; exit($failed === [] ? 0 : 1);