describe(operation: 'compare', policyType: $policyType, subjectExternalId: $subjectExternalId, subjectKey: $subjectKey); } public function describeForCapture(string $policyType, ?string $subjectExternalId = null, ?string $subjectKey = null): SubjectDescriptor { return $this->describe(operation: 'capture', policyType: $policyType, subjectExternalId: $subjectExternalId, subjectKey: $subjectKey); } public function resolved(SubjectDescriptor $descriptor, ?string $sourceModelFound = null): ResolutionOutcomeRecord { $outcome = $descriptor->expectsPolicy() ? ResolutionOutcome::ResolvedPolicy : ResolutionOutcome::ResolvedInventory; return new ResolutionOutcomeRecord( resolutionOutcome: $outcome, reasonCode: $outcome->value, operatorActionCategory: OperatorActionCategory::None, structural: false, retryable: false, sourceModelExpected: $descriptor->sourceModelExpected, sourceModelFound: $sourceModelFound ?? $descriptor->sourceModelExpected, ); } public function missingExpectedRecord(SubjectDescriptor $descriptor): ResolutionOutcomeRecord { $expectsPolicy = $descriptor->expectsPolicy(); return new ResolutionOutcomeRecord( resolutionOutcome: $expectsPolicy ? ResolutionOutcome::PolicyRecordMissing : ResolutionOutcome::InventoryRecordMissing, reasonCode: $expectsPolicy ? 'policy_record_missing' : 'inventory_record_missing', operatorActionCategory: $expectsPolicy ? OperatorActionCategory::RunPolicySyncOrBackup : OperatorActionCategory::RunInventorySync, structural: false, retryable: false, sourceModelExpected: $descriptor->sourceModelExpected, ); } public function structuralInventoryOnly(SubjectDescriptor $descriptor): ResolutionOutcomeRecord { return new ResolutionOutcomeRecord( resolutionOutcome: ResolutionOutcome::FoundationInventoryOnly, reasonCode: 'foundation_not_policy_backed', operatorActionCategory: OperatorActionCategory::ProductFollowUp, structural: true, retryable: false, sourceModelExpected: $descriptor->sourceModelExpected, sourceModelFound: 'inventory', ); } public function invalidSubject(SubjectDescriptor $descriptor): ResolutionOutcomeRecord { return new ResolutionOutcomeRecord( resolutionOutcome: ResolutionOutcome::InvalidSubject, reasonCode: 'invalid_subject', operatorActionCategory: OperatorActionCategory::InspectSubjectMapping, structural: false, retryable: false, sourceModelExpected: $descriptor->sourceModelExpected, ); } public function duplicateSubject(SubjectDescriptor $descriptor): ResolutionOutcomeRecord { return new ResolutionOutcomeRecord( resolutionOutcome: ResolutionOutcome::DuplicateSubject, reasonCode: 'duplicate_subject', operatorActionCategory: OperatorActionCategory::InspectSubjectMapping, structural: false, retryable: false, sourceModelExpected: $descriptor->sourceModelExpected, ); } public function ambiguousMatch(SubjectDescriptor $descriptor): ResolutionOutcomeRecord { return new ResolutionOutcomeRecord( resolutionOutcome: ResolutionOutcome::AmbiguousMatch, reasonCode: 'ambiguous_match', operatorActionCategory: OperatorActionCategory::InspectSubjectMapping, structural: false, retryable: false, sourceModelExpected: $descriptor->sourceModelExpected, ); } public function invalidSupportConfiguration(SupportCapabilityRecord $capability): ResolutionOutcomeRecord { return new ResolutionOutcomeRecord( resolutionOutcome: ResolutionOutcome::InvalidSupportConfig, reasonCode: 'invalid_support_config', operatorActionCategory: OperatorActionCategory::ProductFollowUp, structural: true, retryable: false, sourceModelExpected: $capability->sourceModelExpected, ); } public function throttled(SubjectDescriptor $descriptor): ResolutionOutcomeRecord { return new ResolutionOutcomeRecord( resolutionOutcome: ResolutionOutcome::Throttled, reasonCode: 'throttled', operatorActionCategory: OperatorActionCategory::Retry, structural: false, retryable: true, sourceModelExpected: $descriptor->sourceModelExpected, ); } public function captureFailed(SubjectDescriptor $descriptor, bool $retryable = false): ResolutionOutcomeRecord { return new ResolutionOutcomeRecord( resolutionOutcome: $retryable ? ResolutionOutcome::RetryableCaptureFailure : ResolutionOutcome::CaptureFailed, reasonCode: $retryable ? 'retryable_capture_failure' : 'capture_failed', operatorActionCategory: OperatorActionCategory::Retry, structural: false, retryable: $retryable, sourceModelExpected: $descriptor->sourceModelExpected, ); } public function budgetExhausted(SubjectDescriptor $descriptor): ResolutionOutcomeRecord { return new ResolutionOutcomeRecord( resolutionOutcome: ResolutionOutcome::BudgetExhausted, reasonCode: 'budget_exhausted', operatorActionCategory: OperatorActionCategory::Retry, structural: false, retryable: true, sourceModelExpected: $descriptor->sourceModelExpected, ); } private function describe(string $operation, string $policyType, ?string $subjectExternalId = null, ?string $subjectKey = null): SubjectDescriptor { $capability = $this->capability($policyType); $resolvedSubjectKey = $this->normalizeSubjectKey($policyType, $subjectExternalId, $subjectKey); return new SubjectDescriptor( policyType: $policyType, subjectExternalId: $subjectExternalId !== null && trim($subjectExternalId) !== '' ? trim($subjectExternalId) : null, subjectKey: $resolvedSubjectKey, subjectClass: $capability->subjectClass, resolutionPath: $capability->resolutionPath, supportMode: $capability->supportModeFor($operation), sourceModelExpected: $capability->sourceModelExpected, ); } private function normalizeSubjectKey(string $policyType, ?string $subjectExternalId, ?string $subjectKey): string { $trimmedSubjectKey = is_string($subjectKey) ? trim($subjectKey) : ''; if ($trimmedSubjectKey !== '') { return $trimmedSubjectKey; } $generated = BaselineSubjectKey::forPolicy($policyType, subjectExternalId: $subjectExternalId); if (is_string($generated) && $generated !== '') { return $generated; } $fallbackExternalId = is_string($subjectExternalId) && trim($subjectExternalId) !== '' ? trim($subjectExternalId) : 'unknown'; return trim($policyType).'|'.$fallbackExternalId; } }