accepted = $validatedPrefix; $this->forward = \Closure::fromCallable($forward); $this->onReject = $onReject === null ? null : \Closure::fromCallable($onReject); $this->shouldAbort = $shouldAbort === null ? null : \Closure::fromCallable($shouldAbort); if ($preventPrefixRepeat) { $this->priorAnswerUnits = array_values(array_filter(array_map('trim', preg_split('/(?:\r?\n){2}/', $validatedPrefix) ?: []), static fn (string $unit): bool => $unit !== '')); } } public function push(string $delta): bool { if ($this->stopped) return false; if ($this->finalCheckpointQuery !== null && $this->shouldAbort !== null && ($this->shouldAbort)()) { $this->stopped = true; return false; } $this->pending .= $delta; if (strlen($this->accepted) + strlen($this->deferred) + strlen($this->pending) > 100000) { $this->issues = [['code' => 'REPORT_BOUND_EXCEEDED', 'source_id' => 'report']]; $this->stopped = true; return false; } // A token prefix is not a complete claim. Do not release it before its // containing paragraph is available for the source-state check. while (preg_match('/(?:\r?\n){2}/', $this->pending, $boundary, PREG_OFFSET_CAPTURE) === 1) { $end = $boundary[0][1] + strlen($boundary[0][0]); $unit = substr($this->pending, 0, $end); $this->pending = substr($this->pending, $end); if (!$this->release($unit)) return false; } return true; } public function finish(): bool { if ($this->stopped) return false; $unit = $this->pending; $this->pending = ''; if (!$this->release($unit)) return false; if ($this->finalCheckpointQuery === null) return true; $candidate = $this->accepted . $this->deferred; $review = $this->review($candidate, true); if (empty($review['valid']) && !$this->reject($this->deferred, $review['issues'])) return false; $this->finalContract = TcmLongReportContract::validate($candidate, $this->finalCheckpointQuery); if (empty($this->finalContract['valid']) && !$this->reject($this->deferred, [['code' => 'REPORT_FINAL_CONTRACT_INVALID', 'source_id' => 'report']])) { return false; } if ($this->shouldAbort !== null && ($this->shouldAbort)()) { $this->stopped = true; return false; } if ($this->deferred === '') return true; // This is exactly the original, never-delivered model tail. No repair, // trimming, padding or local composition is permitted at this boundary. if (($this->forward)($this->deferred) === false) { $this->stopped = true; return false; } $this->accepted .= $this->deferred; $this->deferred = ''; return true; } public function rejected(): bool { return $this->issues !== []; } public function issues(): array { return $this->issues; } public function warnings(): array { return $this->warnings; } public function acceptedContent(): string { return $this->accepted; } public function finalContract(): ?array { return $this->finalContract; } private function release(string $unit): bool { if ($unit === '') return true; if (in_array(trim($unit), $this->priorAnswerUnits, true) && !$this->isOrdinaryHeadingUnit($unit) && !$this->reject($unit, [['code' => 'REPORT_CONTINUATION_REPEATED_SECTION', 'source_id' => 'report']])) { return false; } if ($this->finalCheckpointQuery !== null && !$this->holdingFinal) { $offset = 0; foreach (preg_split('/(?<=\n)|(?<=\r)(?!\n)/', $unit) ?: [] as $line) { $shape = TcmLongReportContract::validate($line, $this->finalCheckpointQuery); if (in_array(6, $shape['section_order'], true)) { if ($offset > 0 && !$this->release(substr($unit, 0, $offset))) return false; $unit = substr($unit, $offset); $this->holdingFinal = true; break; } $offset += strlen($line); } } if ($this->holdingFinal) { $this->deferred .= $unit; $review = $this->review($this->accepted . $this->deferred); return !empty($review['valid']) || $this->reject($this->deferred, $review['issues']); } $candidate = $this->accepted . $unit; $review = $this->review($candidate); if (empty($review['valid']) && !$this->reject($unit, $review['issues'])) return false; if (($this->forward)($unit) === false) { $this->stopped = true; return false; } $this->accepted .= $unit; return true; } private function isOrdinaryHeadingUnit(string $unit): bool { // Ordinary answers may retain a repeated explicit Markdown heading. // Skip only duplicate detection: clinical review and raw forwarding // below still apply. Long-report continuations remain strict. if ($this->continuationQuery !== null || $this->finalCheckpointQuery !== null) return false; if (preg_match('/\A {0,3}#{1,6}[ \t]+([^\r\n]+)(?:\r?\n){0,2}\z/u', $unit, $heading) !== 1) return false; return preg_match('/[^ \t#]/u', $heading[1]) === 1; } private function review(string $candidate, bool $complete = false): array { $review = $this->pulseExcluded ? TcmConsultationPulsePolicy::validate($this->ledger, $candidate) : ['valid' => true]; if (!empty($review['valid'])) $review = TcmClinicalFactStateGuard::validate($this->ledger, $candidate); if (!empty($review['valid']) && $this->evidenceIndex !== null) { $review = TcmClinicalEvidenceContract::validate($this->evidenceIndex, $candidate, $complete); } if (!empty($review['valid']) && $this->continuationQuery !== null) { $shape = TcmLongReportContract::validate($candidate, $this->continuationQuery); if (in_array('DUPLICATE_REPORT_SECTION', array_column($shape['issues'], 'code'), true)) { $review = ['valid' => false, 'issues' => [ ['code' => 'REPORT_CONTINUATION_REPEATED_SECTION', 'source_id' => 'report'], ]]; } } if (!empty($review['valid']) && $this->finalCheckpointQuery !== null) { $review = TcmHerbalPremiseGuard::validate($candidate, $this->ledger); } return $review; } private function reject(string $unit, array $issues): bool { if (!self::BLOCKING) { // 非拦截模式:只记录问题码,调用方继续按原流程交付该段原文。 foreach ($issues as $issue) { if (!is_array($issue)) continue; $key = ($issue['code'] ?? '') . '|' . ($issue['source_id'] ?? ''); if (!isset($this->warnings[$key]) && count($this->warnings) < 32) $this->warnings[$key] = $issue; } return true; } if ($this->holdingFinal) { $issues[] = ['code' => 'REPORT_FINAL_SECTION_UNDELIVERED', 'source_id' => 'report']; } $this->issues = $issues; $this->stopped = true; if ($this->onReject !== null) ($this->onReject)($unit, $this->issues); return false; } }