['ok'=>false, 'error'=>$reason]; if (!is_string($output['fingerprint'] ?? null) || !hash_equals(self::fingerprint($question, $sources), $output['fingerprint'])) return $fail('stale_or_wrong_evidence'); if (!in_array($output['status'] ?? null, ['supported','partial','not_found','conflicting','uncertain'], true)) return $fail('invalid_status'); if (!is_string($output['explanation'] ?? null) || trim($output['explanation']) === '' || mb_strlen($output['explanation']) > 1500) return $fail('invalid_explanation'); if (!is_array($output['evidence'] ?? null) || count($output['evidence']) > 10) return $fail('invalid_evidence'); $lookup = []; foreach ($sources as $source) { if (!is_int($source['id'] ?? null) || !is_string($source['text'] ?? null) || isset($lookup[$source['id']])) return $fail('invalid_sources'); $lookup[$source['id']] = $source['text']; } $evidence = []; foreach ($output['evidence'] as $item) { if (!is_array($item) || !is_int($item['sourceId'] ?? null) || !is_string($item['quote'] ?? null)) return $fail('invalid_quote'); $quote = $item['quote']; if (trim($quote) === '' || mb_strlen($quote) > 1000 || !isset($lookup[$item['sourceId']]) || !str_contains($lookup[$item['sourceId']], $quote)) return $fail('quote_not_in_source'); $evidence[] = ['sourceId'=>$item['sourceId'], 'quote'=>$quote]; } if (in_array($output['status'], ['supported','partial'], true) && $evidence === []) return $fail('evidence_required'); if ($output['status'] === 'conflicting' && count(array_unique(array_column($evidence, 'quote'))) < 2) return $fail('two_distinct_passages_required'); return ['ok'=>true, 'assessment'=>[ 'status'=>$output['status'], 'explanation'=>$output['explanation'], 'evidence'=>$evidence, 'fingerprint'=>$output['fingerprint'], 'provenance'=>'model_assessment', 'requiresHumanReview'=>true, 'citationTestPerformed'=>false, ]]; } }