'未记录固定刺痛。']); $polarity = TcmClinicalFactStateGuard::validate($ledger, '当前无固定刺痛。'); $pulse = TcmConsultationPulsePolicy::validate($ledger, '脉象未记录。'); foreach (['polarity' => $polarity, 'pulse' => $pulse] as $kind => $review) { $codes = array_column($review['issues'], 'code'); clinicalTelemetryExpect(!$review['valid'] && $codes !== [], 'Synthetic guard must produce concrete issues: ' . $kind); $event = $timing->event('upstream_done', [ 'status' => 'error', 'error_code' => 'TCM_FACT_STATE_CONFLICT', 'clinical_issue_codes' => $codes, 'clinical_issue_count' => count($codes), 'clinical_repair_count' => 1, ]); clinicalTelemetryExpect($event['clinical_issue_codes'] === $codes && $event['clinical_issue_count'] === count($codes) && $event['clinical_repair_count'] === 1, 'Actual pulse and polarity issues remain distinguishable: ' . $kind); clinicalTelemetryExpect($event['trace_id'] === $timing->id() && $event['phase'] === 'upstream_done', 'Clinical diagnostics retain their request trace and phase'); } clinicalTelemetryExpect($timing->event('upstream_done', ['error_code' => 'PULSE_EXCLUDED_FROM_ASSESSMENT'])['error_code'] === 'PULSE_EXCLUDED_FROM_ASSESSMENT', 'The precise pulse failure is an allowed terminal error code'); $private = [ 'patient_id' => 7654321, 'patient_name' => 'PRIVATE_PATIENT', 'source_id' => 'PRIVATE_SOURCE_ID', 'source_ids' => ['s999'], 'content' => 'PRIVATE_MODEL_BODY', 'prompt' => 'PRIVATE_PROMPT', 'token' => 'PRIVATE_TOKEN', 'url' => 'https://private.invalid/PRIVATE_URL', 'clinical_fact_state_issues' => [['code' => 'UNKNOWN_TO_NEGATIVE', 'source_id' => 'PRIVATE_SOURCE_ID', 'quote' => 'PRIVATE_QUOTE']], ]; $badCodes = ['PRIVATE_MODEL_BODY', 'UNKNOWN_NEW_FAILURE', 'UNKNOWN_TO_NEGATIVE:s999', "UNKNOWN_TO_NEGATIVE\nPRIVATE_TOKEN", ' UNKNOWN_TO_NEGATIVE', 'unknown_to_negative', '', 123, 1.25, true, false, null, ['code' => 'UNKNOWN_TO_NEGATIVE'], (object) ['code' => 'UNKNOWN_TO_NEGATIVE']]; $event = $timing->event('upstream_done', $private + [ 'clinical_issue_codes' => array_merge($badCodes, ['UNKNOWN_TO_NEGATIVE', 'PULSE_EXCLUDED_FROM_ASSESSMENT', 'UNKNOWN_TO_NEGATIVE', 'PULSE_EXCLUDED_FROM_ASSESSMENT']), 'clinical_issue_count' => 4, 'clinical_repair_count' => 1, ]); clinicalTelemetryExpect($event['clinical_issue_codes'] === ['UNKNOWN_TO_NEGATIVE', 'PULSE_EXCLUDED_FROM_ASSESSMENT'], 'Only exact safe strings survive, once each, in first occurrence order'); clinicalTelemetryExpect(array_keys($event) === ['trace_id', 'phase', 'request_elapsed_ms', 'clinical_issue_count', 'clinical_repair_count', 'clinical_issue_codes'], 'No raw source metadata, identity, content, URL or credential field survives'); clinicalTelemetryExpect($timing->event('upstream_done', ['clinical_issue_codes' => $badCodes])['clinical_issue_codes'] === [], 'An array containing only unlisted or invalid values becomes an empty safe list'); clinicalTelemetryExpect($timing->event('upstream_done', ['clinical_issue_codes' => ['PRIVATE_ARRAY_KEY' => 'UNKNOWN_TO_POSITIVE']])['clinical_issue_codes'] === ['UNKNOWN_TO_POSITIVE'], 'Arbitrary input keys are not persisted'); foreach (['UNKNOWN_TO_NEGATIVE', 42, 1.5, true, null, (object) ['code' => 'UNKNOWN_TO_NEGATIVE']] as $bad) { clinicalTelemetryExpect(!array_key_exists('clinical_issue_codes', $timing->event('upstream_done', ['clinical_issue_codes' => $bad])), 'The list itself must be an array'); } foreach (['clinical_issue_count', 'clinical_repair_count'] as $field) { foreach (['2', 2.5, true, null, [], new stdClass()] as $bad) { clinicalTelemetryExpect(!array_key_exists($field, $timing->event('upstream_done', [$field => $bad])), 'Clinical counts use integer-only timing validation'); } clinicalTelemetryExpect($timing->event('upstream_done', [$field => -1])[$field] === 0, 'Clinical counts cannot become negative'); } foreach (['prepare', 'sse_start', 'attachment', 'first_delta', 'sse_done'] as $phase) { clinicalTelemetryExpect(!array_key_exists('clinical_issue_codes', $timing->event($phase, ['clinical_issue_codes' => ['UNKNOWN_TO_NEGATIVE']])), 'Issue enums belong to the upstream completion diagnostic'); } // The table spans every issue emitted by the five local guards used by this stream. $allowed = [ 'FACT_STATE_LEDGER_UNAVAILABLE', 'FACT_STATE_INVALID_UTF8', 'SOURCE_ANNOTATION_INVALID', 'UNSUPPORTED_NEGATIVE_REFERENCE', 'ATTRIBUTED_SOURCE_QUOTE_INVALID', 'UNKNOWN_TO_NEGATIVE', 'UNKNOWN_TO_POSITIVE', 'POSITIVE_TO_NEGATIVE', 'NEGATIVE_TO_POSITIVE', 'NEGATION_SCOPE_WIDENED', 'NEGATIVE_NOT_IN_SOURCE', 'NEGATIVE_TO_UNKNOWN', 'POSITIVE_TO_UNKNOWN', 'RECORDED_TO_UNKNOWN', 'TONGUE_FEATURE_NOT_IN_CURRENT_SOURCE', 'PULSE_FEATURE_NOT_IN_CURRENT_SOURCE', 'CONSULTATION_INVALID_UTF8', 'PULSE_EXCLUDED_FROM_ASSESSMENT', 'EVIDENCE_INDEX_UNAVAILABLE', 'EVIDENCE_INVALID_UTF8', 'EXAM_ROLE_MISSING', 'EXAM_HEADING_METADATA_UNSUPPORTED', 'EXAM_FACT_PROTOCOL_INVALID', 'EXAM_NEGATIVE_HEADING_UNSUPPORTED', 'LONGITUDINAL_FACT_PROTOCOL_INVALID', 'EXAM_KNOWN_ROLE_TO_UNKNOWN', 'EVIDENCE_SOURCE_DATE_MISMATCH', 'LONGITUDINAL_SOURCE_NOT_HISTORICAL', 'EXAM_SOURCE_QUOTE_INVALID', 'EXAM_SOURCE_NOT_CURRENT', 'EXAM_ROLE_MISMATCH', 'DENIAL_SOURCE_QUOTE_INVALID', 'UNQUOTED_PATIENT_DENIAL', 'HERBAL_PREMISE_SELF_CONFLICT', 'REPORT_BOUND_EXCEEDED', 'REPORT_FINAL_CONTRACT_INVALID', 'REPORT_CONTINUATION_REPEATED_SECTION', 'REPORT_FINAL_SECTION_UNDELIVERED', ]; foreach ($allowed as $code) { clinicalTelemetryExpect($timing->event('upstream_done', ['clinical_issue_codes' => [$code]])['clinical_issue_codes'] === [$code], 'A current local guard issue must remain observable: ' . $code); } $bounded = $timing->event('upstream_done', [ 'clinical_issue_codes' => array_merge(array_fill(0, 20, 'UNKNOWN_NEW_FAILURE'), array_fill(0, 20, $allowed[0]), $allowed), ]); clinicalTelemetryExpect($bounded['clinical_issue_codes'] === array_slice($allowed, 0, 16), 'The maximum is sixteen unique accepted codes, independent of invalid or duplicate entries'); // Exercise the same immediate File-channel write used before SSE exits. $app = new \think\App(dirname(__DIR__) . '/'); $directory = sys_get_temp_dir() . '/zyt-clinical-telemetry-' . bin2hex(random_bytes(8)); $app->config->set(['default' => 'clinical', 'channels' => ['clinical' => [ 'type' => 'File', 'path' => $directory, 'single' => 'timing', 'realtime_write' => false, ]]], 'log'); $app->make('log'); $file = $directory . '/timing.log'; try { $timing->log('upstream_done', $private + [ 'status' => 'error', 'error_code' => 'PULSE_EXCLUDED_FROM_ASSESSMENT', 'clinical_issue_codes' => ['PULSE_EXCLUDED_FROM_ASSESSMENT', 'UNKNOWN_TO_NEGATIVE', 'PRIVATE_QUOTE'], 'clinical_issue_count' => 3, 'clinical_repair_count' => 1, ]); clinicalTelemetryExpect(is_file($file), 'The telemetry is written before framework request-end flushing'); $logged = is_file($file) ? (string) file_get_contents($file) : ''; clinicalTelemetryExpect(str_contains($logged, '"clinical_issue_codes":["PULSE_EXCLUDED_FROM_ASSESSMENT","UNKNOWN_TO_NEGATIVE"]') && str_contains($logged, '"clinical_issue_count":3') && str_contains($logged, '"clinical_repair_count":1') && str_contains($logged, '"error_code":"PULSE_EXCLUDED_FROM_ASSESSMENT"') && str_contains($logged, $timing->id()), 'Persisted failure contains only safe diagnostics correlated to this request'); foreach (['PRIVATE_', '7654321', 's999', 'private.invalid', 'clinical_fact_state_issues', 'source_id'] as $secret) { clinicalTelemetryExpect(!str_contains($logged, $secret), 'Persisted diagnostics must discard all raw source and private payloads'); } } finally { if (is_file($file)) unlink($file); if (is_dir($directory)) rmdir($directory); } echo 'Clinical failure telemetry: ' . $checks . " checks passed (pure offline fixtures).\n";