--- server/app/adminapi/service/TcmClinicalFactStateGuard.php.before +++ server/app/adminapi/service/TcmClinicalFactStateGuard.php @@ -468,9 +468,10 @@ $evidenceView = preg_replace('/^\s*(?:(?:主要|具体|直接)(?:依据|根据)\s*[::]?|依据为)\s*/u', '', $assertionView) ?? $assertionView; $referenceView = self::sourceReferenceProse($ledger, self::negatedInferenceProse(self::inferenceHeadProse($evidenceView)), $invalid); + $patientReviewView = self::patientReviewQuestionProse($referenceView); $pendingHead = preg_match('/^\s*适用前(?:需|须|应)(?:核实|复核|确认)[::]/u', $referenceView) === 1; $questionView = ($question || $pendingHead || $pendingQuestion) - ? self::terminatedQuestionProse($referenceView) : $referenceView; + ? self::terminatedQuestionProse($patientReviewView) : $patientReviewView; preg_match('/^[。;;!?!?\r\n]+/u', $terminator, $separator); // Only the immediately preceding fully neutral question can // supply the antecedent of this closed contingency operator. @@ -1145,6 +1146,19 @@ return implode('', $pieces); } + /** A requested patient review owns its bounded whether-object, not its tail. */ + private static function patientReviewQuestionProse(string $clause): string + { + if (!preg_match('/^\s*(?:(?:仍|还)?(?:需|须|应|请)(?:进一步|再次|逐项)?)?' + . '(?:补问|询问|核实|复核|核对|追问|确认|查明)[::\s]*(?:患者|本例)[::\s]*' + . '((?:是否|有无).+)$/uD', $clause, $request)) return $clause; + // Reuse the existing neutral-object grammar and assertion boundaries. + // A refused object keeps its original action/current/record markers; + // only a proved question may remove this collection instruction. + $view = self::terminatedQuestionProse($request[1]); + return $view !== $request[1] ? $view : $clause; + } + /** A modal + collection action governs only its own neutral object list. */ private static function collectionRequestProse(string $clause): string { @@ -1624,7 +1638,7 @@ foreach ($pieces as $piece) array_push($issues, ...self::fieldIssues($ledger, $piece, $pendingFields)); return $issues; } - $text = trim($clause, " \t#>*-"); + $text = self::unwrappedFieldUnknown(trim($clause, " \t#>*-")); $carried = $pendingFields; $pendingFields = []; foreach (self::FIELD_GROUPS as $group => $definition) { @@ -1636,6 +1650,13 @@ $dataGap = self::dataGapPart($text); foreach (self::FIELD_GROUPS as $group => $definition) { $state = $ledger['field_states'][$group] ?? ['state' => 'unknown', 'source_id' => 'report', 'date' => '']; + $aliases = implode('|', array_map(static fn (string $alias): string => preg_quote($alias, '/'), $definition['aliases'])); + $value = null; + if (preg_match('/(?:' . $aliases . ')[::]\s*(.+)$/uD', $text, $match)) $value = $match[1]; + elseif (in_array($group, $carried, true)) $value = $text; + $unwrapped = $value !== null ? self::unwrappedFieldUnknown($value) : null; + $wrappedUnknown = $unwrapped !== $value; + $value = $unwrapped; if ($state['state'] !== 'unknown') { // A missing named examination is different from a missing // examination image; it cannot erase an existing result. @@ -1648,15 +1669,14 @@ || ($dataGap !== null && $dataGap['topic'] === ''))) { $issues[] = ['code' => 'RECORDED_TO_UNKNOWN', 'source_id' => $state['source_id']]; } + if ($wrappedUnknown && self::sameTime($ledger['sources'][$state['source_id']] ?? [], self::scope($text, 'current'), $text)) { + $issues[] = ['code' => 'RECORDED_TO_UNKNOWN', 'source_id' => $state['source_id']]; + } continue; } if ($dataGap !== null) continue; $dates = self::calendarDates($text); if ($dates !== [] && $state['date'] !== '' && !in_array($state['date'], $dates, true)) continue; - $aliases = implode('|', array_map(static fn (string $alias): string => preg_quote($alias, '/'), $definition['aliases'])); - $value = null; - if (preg_match('/(?:' . $aliases . ')[::]\s*(.+)$/uD', $text, $match)) $value = $match[1]; - elseif (in_array($group, $carried, true)) $value = $text; if ($value !== null && (preg_match('/^(?:' . self::UNKNOWN . '|未填|待补)/u', $value) || self::conditional($value) || self::evidenceOnly($value))) continue; $absent = preg_match('/(?:' . self::OBSERVED_ABSENT . ')[^,,::]{0,30}(?:' . $aliases . ')/u', $text) @@ -1669,6 +1689,14 @@ } } return $issues; + } + + /** Paired punctuation around one unknown value is formatting, never a finding. */ + private static function unwrappedFieldUnknown(string $value): string + { + if (preg_match('/^\s*(?:(\s*(' . self::UNKNOWN . ')\s*)|\(\s*(' . self::UNKNOWN . ')\s*\))\s*$/uD', + $value, $wrapped)) return ($wrapped[1] ?? '') !== '' ? $wrapped[1] : $wrapped[2]; + return $value; } /** Explicit lexical polarity with inherited scope across enumerated items. */