'HERBAL_PREMISE_SELF_CONFLICT', 'source_id' => 'report']] : []; return ['valid' => $issues === [], 'issues' => $issues]; } private static function hasPostfixCurrentProposal(string $report): bool { // Keep quoted names as possible antecedents, but search only outside // quotation marks for a new recommendation. Source attribution does // not extend over a later current-use operator. Bound references to // the same sentence or the immediately following explicit 该方/旧方. $outside = preg_replace_callback('/“[^”]*”|「[^」]*」|『[^』]*』|"[^"]*"/su', static fn (array $m): string => str_repeat(' ', strlen($m[0])), $report) ?? $report; preg_match_all('/[^。;;\r\n]+/u', $outside, $sentences, PREG_OFFSET_CAPTURE); $action = '(?:沿用|续用|启用|采用|选用|使用|(?:列为|作为)(?:本次|当前)?(?:主方|候选方))'; $operator = '/(?:(?:本次|此次)\s*(?:拟|决定)?\s*(?:沿用|续用|启用)' . '|(?:建议|推荐|考虑|拟|决定)\s*(?:可|继续|仍)?\s*(?:' . $action . '|服用)' . '|(?:列为|作为)(?:本次|当前)(?:主方|候选方)' . '|(?:本次|当前|此次|现)\s*(?:予以)?推荐)/u'; $previousName = false; foreach ($sentences[0] as [$sentence, $start]) { $raw = substr($report, $start, strlen($sentence)); preg_match_all('/' . self::NAMED_FORMULA . '/u', $raw, $names, PREG_OFFSET_CAPTURE); preg_match_all($operator, $sentence, $operators, PREG_OFFSET_CAPTURE); foreach ($operators[0] as [$op, $position]) { $before = mb_substr(substr($sentence, 0, $position), -16, null, 'UTF-8'); $after = mb_substr(substr($sentence, $position + strlen($op)), 0, 12, 'UTF-8'); if (preg_match('/(?:不|勿|未|没有|禁止|避免|暂缓|无需|不能|不宜|不应|是否|能否|可否)\s*$/u', $before) || preg_match('/^\s*(?:不|勿|禁止|暂不|仅作历史)/u', $after)) continue; foreach ($names[0] as [$name, $nameStart]) { $end = $nameStart + strlen($name); if ($end <= $position && mb_strlen(substr($raw, $end, $position - $end), 'UTF-8') <= 64) return true; } if ($previousName && mb_strlen($sentence, 'UTF-8') <= 80 && preg_match('/(?:该|此|旧|上述|前述|原)(?:旧)?方(?:剂)?/u', $sentence)) return true; } $previousName = preg_match('/' . self::NAMED_FORMULA . '/u', mb_substr($raw, -80, null, 'UTF-8')) === 1; } return false; } private static function withoutVerifiedAttributedQuotes(string $report, array $ledger): string { // A citation is not provenance. Check every cited source and exact // quotation, then require attribution outside it. Even a genuine old // formula name remains a current proposal under an outside operator. $pattern = '/(?:“([^”]*)”|「([^」]*)」|『([^』]*)』|"([^"]*)")\s*[((]\s*(s[1-9][0-9]*(?:\s*[、,,]\s*s[1-9][0-9]*)*)\s*[))]/su'; preg_match_all($pattern, $report, $matches, PREG_SET_ORDER | PREG_OFFSET_CAPTURE | PREG_UNMATCHED_AS_NULL); foreach (array_reverse($matches) as $match) { $quote = ''; for ($i = 1; $i <= 4; ++$i) if ($match[$i][0] !== null) { $quote = $match[$i][0]; break; } if ($quote === '') continue; preg_match_all('/s[1-9][0-9]*/', $match[5][0], $ids); $verified = $ids[0] !== []; foreach ($ids[0] as $id) { $source = $ledger['sources'][$id] ?? null; if (!is_array($source) || ($source['id'] ?? '') !== $id || !is_string($source['quote'] ?? null) || !str_contains($source['quote'], $quote)) $verified = false; } if (!$verified) continue; $before = substr($report, 0, $match[0][1]); preg_match('/[^。;;\r\n]*$/u', $before, $context); $outside = $context[0] ?? ''; $currentOperator = preg_match('/(?:主方(?:方向)?|候选方|方药方向|拟用方)[::]|(?:考虑|推荐|建议|采用|选用|拟用|方选)[^。;\n]{0,12}$/u', $outside); $attributed = preg_match('/(?:来源|原文|记录|资料|历史|既往|旧方|当年|望诊|闻诊|问诊|切诊|舌象|脉象)/u', $outside); if ($currentOperator || !$attributed) continue; $report = substr_replace($report, '', $match[0][1], strlen($match[0][0])); } return $report; } }