Fixed lexical forms only; unrecognized pulse wording stays other. */ public static function forms(string $unit): array { if (!mb_check_encoding($unit, 'UTF-8')) return ['other']; $found = []; $probe = mb_substr($unit, 0, self::MAX_PROBE_CHARS, 'UTF-8'); // Only a complete, narrow workflow-rule clause is masked here. A nearby // request to collect pulse remains visible to the other form checks. $probe = preg_replace_callback('/[^,,。;;!!??\r\n]+/u', static function (array $match) use (&$found): string { $clause = trim($match[0], " \t|*"); $workflow = '(?:本流程|当前流程|本工作流|当前工作流|本次问诊|此次问诊|本次评估|当前评估|问诊流程|咨询流程|线上问诊|远程问诊)'; $noun = '(?:脉象|脉诊|切脉|把脉|诊脉|舌脉)'; if (preg_match('/^(?:' . $workflow . '(?:中|下)?[::\s]*(?:不采集|不使用|不依据|不纳入|不进行|禁止采集|无需采集|无需补采|无需提供)' . $noun . '(?:信息|资料|评估|辨证)?|' . $noun . '(?:不纳入|不用于|不作为)' . $workflow . '(?:的)?(?:评估|依据|辨证)?)$/uD', $clause) === 1) { $found['workflow_rule_narration'] = true; return ' '; } return $match[0]; }, $probe) ?? $probe; $row = '/^[ \t]*(?:\|[ \t]*)?(?:(?:[-*+]|\d+[.)、])[ \t]+)?(?:\*\*)?(?:脉象|脉诊|舌脉|脉)(?:\*\*)?[ \t]*(?:[::]|\|)/mu'; if (preg_match($row, $probe) === 1) $found['pulse_row'] = true; $missing = '/(?:(?:脉象|脉诊|脉(?!率|压|搏|络|管))[\s::|*]*(?:未记录|未采集|缺失|缺少|缺乏|未提供|未见记录|未获取|暂无|不详|待补|资料不足|信息不足)' . '|(?:缺|缺少|缺乏|未采集|未记录|未提供|未获取)(?:的)?(?:脉象|脉诊|脉(?!率|压|搏|络|管))' . '|\bpulse\b(?![\s_-]*(?:rate|pressure)\b)[\s:]*(?:(?:is|was)[\s]+)?(?:not[\s]+(?:recorded|collected|provided)|missing|unavailable))/iu'; $feature = '/(?:脉(?:象|诊)?[::\s|*]*(?:(?:呈|为|见|可见|表现为|按之)[::\s]*)?(?:不|未|无)?(?:' . self::FEATURES . ')' . '|(?:' . self::FEATURES . ')脉(?!络|管|压|率))/u'; foreach (['missing_pulse_statement' => $missing, 'pulse_feature' => $feature] as $form => $pattern) { if (preg_match($pattern, $probe) !== 1) continue; $found[$form] = true; $probe = preg_replace($pattern, ' ', $probe) ?? $probe; } // A row label is already represented, but any unclassified pulse wording // (including active solicitation) gets other, even in a mixed paragraph. $probe = preg_replace($row, ' ', $probe) ?? $probe; if ($found === [] || preg_match('/(?:' . self::PULSE . ')/iu', $probe) === 1) $found['other'] = true; return array_values(array_filter(self::FORMS, static fn (string $form): bool => isset($found[$form]))); } /** @return array{trace_id:string,pulse_forms:list,clinical_issue_codes:list,unit_chars:int} */ public static function event(AssistantSseTiming $timing, string $unit, array $issues): array { $codes = []; foreach ($issues as $issue) { if (is_array($issue) && is_string($issue['code'] ?? null)) $codes[] = $issue['code']; } // Reuse the existing exact issue allowlist and its sixteen-code bound. $safe = $timing->event('upstream_done', ['clinical_issue_codes' => $codes]); return [ 'trace_id' => $timing->id(), 'pulse_forms' => self::forms($unit), 'clinical_issue_codes' => $safe['clinical_issue_codes'], 'unit_chars' => mb_strlen($unit, 'UTF-8'), ]; } public static function log(AssistantSseTiming $timing, string $unit, array $issues): void { Log::write('diagnosis ai clinical rejection ' . json_encode(self::event($timing, $unit, $issues)), 'info'); } }