completeness ??= new ResultCompletenessScorer(); $this->readiness ??= new ProductReadinessAssessor(); } public function metrics(ProductDTO $product, ?PreviewAnalysis $analysis, ?string $domainHint = null): array { $quality = $analysis?->getQualitySummary(); $readiness = $this->readiness->assess($product, $domainHint); return [ 'product_score' => $analysis?->getProductScore(), 'confidence' => $analysis?->getConfidenceScore(), 'quality_score' => $quality?->getScore(), 'quality_grade' => $quality?->getGrade(), 'completeness' => $this->completeness->score($product), // The card and the selected preview use the same readiness // assessor. Technical PI remains available but is not the // operator-facing completeness measure. 'content_score' => $readiness->getContentScore(), 'field_groups' => $readiness->getFieldGroups(), 'commerce_score' => $readiness->getCommerceScore(), 'domain' => $readiness->getDomain(), 'missing_commerce_fields' => $readiness->getMissingCommerceFields(), ]; } public function rank(array $results): array { if ($results === []) { return []; } foreach ($results as $index => &$result) { $result['_original_order'] = $index; } unset($result); usort($results, static function (array $left, array $right): int { foreach (['product_score', 'confidence', 'quality_score', 'completeness'] as $metric) { $comparison = ($right['intelligence'][$metric] ?? -1) <=> ($left['intelligence'][$metric] ?? -1); if ($comparison !== 0) { return $comparison; } } return $left['_original_order'] <=> $right['_original_order']; }); $best = $results[0]['intelligence']['product_score'] ?? null; $second = $results[1]['intelligence']['product_score'] ?? null; if (count($results) >= 2 && is_float($best) && is_float($second) && $best >= 0.65 && $best - $second >= 0.08) { $results[0]['recommended'] = true; } foreach ($results as &$result) { unset($result['_original_order']); } return $results; } }