Some checks failed
PR Fast Feedback / fast-feedback (pull_request) Failing after 4m7s
Implemented deterministic Baseline Result Semantics (Spec 383), introducing CompareSubjectResult and CompareEvidenceResult. Replaced generic arrays with strict Data Transfer Objects for Baseline engine output.
194 lines
8.0 KiB
PHP
194 lines
8.0 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Support\Baselines;
|
|
|
|
use App\Support\Inventory\InventoryPolicyTypeMeta;
|
|
|
|
final class SubjectResolver
|
|
{
|
|
public function capability(string $policyType): SupportCapabilityRecord
|
|
{
|
|
$contract = InventoryPolicyTypeMeta::baselineSupportContract($policyType);
|
|
|
|
return new SupportCapabilityRecord(
|
|
policyType: $policyType,
|
|
subjectClass: SubjectClass::from($contract['subject_class']),
|
|
compareCapability: $contract['compare_capability'],
|
|
captureCapability: $contract['capture_capability'],
|
|
resolutionPath: ResolutionPath::from($contract['resolution_path']),
|
|
configSupported: (bool) $contract['config_supported'],
|
|
runtimeValid: (bool) $contract['runtime_valid'],
|
|
sourceModelExpected: $contract['source_model_expected'],
|
|
);
|
|
}
|
|
|
|
public function describeForCompare(string $policyType, ?string $subjectExternalId = null, ?string $subjectKey = null): SubjectDescriptor
|
|
{
|
|
return $this->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
|
|
{
|
|
return new ResolutionOutcomeRecord(
|
|
resolutionOutcome: ResolutionOutcome::MissingLocalEvidence,
|
|
reasonCode: 'missing_local_evidence',
|
|
operatorActionCategory: $descriptor->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_inventory_only',
|
|
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::UnresolvedAmbiguousIdentity,
|
|
reasonCode: 'unresolved_ambiguous_identity',
|
|
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;
|
|
}
|
|
|
|
$fallbackExternalId = is_string($subjectExternalId) && trim($subjectExternalId) !== ''
|
|
? trim($subjectExternalId)
|
|
: 'unknown';
|
|
|
|
return 'identity-required:'.trim($policyType).':'.$fallbackExternalId;
|
|
}
|
|
}
|