--- server/app/adminapi/service/AssistantSseTiming.php.before +++ server/app/adminapi/service/AssistantSseTiming.php @@ -17,6 +17,7 @@ 'source_bytes', 'query_bytes', 'attachment_count', 'delta_bytes', 'delta_count', 'first_delta_ms', 'inputs_bytes', 'source_ms', 'upload_ms', 'ocr_ms', 'reuse_wait_ms', 'upload_cache_hits', 'upload_cache_misses', + 'clinical_issue_count', 'clinical_repair_count', ]; private const BOOLEAN_FIELDS = ['compaction_attempted', 'retry_attempted', 'first_delta_observed', 'compaction_cache_hit']; @@ -29,7 +30,27 @@ 'STREAM_DELIVERY_FAILED', 'REQUEST_BUILD_FAILED', 'CURL_INIT_FAILED', 'UPSTREAM_BUSY', 'INVALID_RESPONSE', 'AI_ASSISTANT_CANCELLED', 'TCM_FACT_STATE_CONFLICT', 'TCM_REPORT_INCOMPLETE', 'TASK_ANALYSIS_MISSING', - 'STREAM_CONTENT_MISMATCH', 'UPSTREAM_CLEANUP_PENDING', + 'STREAM_CONTENT_MISMATCH', 'UPSTREAM_CLEANUP_PENDING', 'PULSE_EXCLUDED_FROM_ASSESSMENT', + ]; + + /** Exact issue enums emitted by the consultation stream's local guards. */ + private const CLINICAL_ISSUE_CODES = [ + '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', ]; private string $traceId; @@ -52,11 +73,11 @@ } /** - * Only a fixed set of numeric and boolean metadata reaches the log. In particular, + * Only fixed numeric, boolean and enum metadata reaches the log. In particular, * arbitrary prompt text, patient identifiers, URLs and credentials are discarded. * * @param array $metrics - * @return array + * @return array> */ public function event(string $phase, array $metrics = []): array { @@ -81,6 +102,16 @@ } if (isset($metrics['error_code']) && in_array($metrics['error_code'], self::ERROR_CODES, true)) { $event['error_code'] = $metrics['error_code']; + } + if ($event['phase'] === 'upstream_done' && is_array($metrics['clinical_issue_codes'] ?? null)) { + $codes = []; + foreach ($metrics['clinical_issue_codes'] as $code) { + // Never accept source IDs, raw issue objects or a regex-shaped arbitrary string. + if (!is_string($code) || !in_array($code, self::CLINICAL_ISSUE_CODES, true)) continue; + $codes[$code] = true; + if (count($codes) === 16) break; + } + $event['clinical_issue_codes'] = array_keys($codes); } return $event; }