TenantAtlas/apps/platform/app/Support/Baselines/Matching/MatchingOutcome.php
ahmido 788efee1c2 feat(baselines): implement baseline matching canonicalization (#453)
Replaced legacy tenant and environment bindings in the BaselineDriftEngine with the new ProviderResourceIdentity framework as defined in Spec 382. This ensures cross-environment compatibility and deterministic baseline matching.

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #453
2026-06-15 22:48:48 +00:00

222 lines
6.0 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Support\Baselines\Matching;
use App\Support\Resources\ProviderResourceDescriptor;
use Illuminate\Contracts\Support\Arrayable;
use JsonSerializable;
/**
* @implements Arrayable<string, mixed>
*/
final readonly class MatchingOutcome implements Arrayable, JsonSerializable
{
public const string Resolved = 'resolved';
public const string Ambiguous = 'ambiguous';
public const string MissingProviderResource = 'missing_provider_resource';
public const string MissingLocalEvidence = 'missing_local_evidence';
public const string UnresolvedIdentity = 'unresolved_identity';
public const string Unsupported = 'unsupported';
public const string Limited = 'limited';
public const string Excluded = 'excluded';
/**
* @param array<string, string|int|float|bool|null> $proof
*/
public function __construct(
public string $status,
public string $reasonCode,
public BaselineSubjectDescriptor $subject,
public ?ProviderResourceDescriptor $matchedDescriptor = null,
public ?string $matchedSubjectKey = null,
public string $trust = 'none',
public array $proof = [],
) {}
/**
* @param array<string, string|int|float|bool|null> $proof
*/
public static function resolved(
BaselineSubjectDescriptor $subject,
ProviderResourceDescriptor $matchedDescriptor,
string $matchedSubjectKey,
string $reasonCode,
string $trust,
array $proof = [],
): self {
return new self(
status: self::Resolved,
reasonCode: $reasonCode,
subject: $subject,
matchedDescriptor: $matchedDescriptor,
matchedSubjectKey: $matchedSubjectKey,
trust: $trust,
proof: $proof,
);
}
/**
* @param array<string, string|int|float|bool|null> $proof
*/
public static function ambiguous(BaselineSubjectDescriptor $subject, array $proof = []): self
{
return new self(
status: self::Ambiguous,
reasonCode: 'ambiguous_match',
subject: $subject,
trust: 'none',
proof: $proof,
);
}
/**
* @param array<string, string|int|float|bool|null> $proof
*/
public static function missingLocalEvidence(BaselineSubjectDescriptor $subject, array $proof = []): self
{
return new self(
status: self::MissingLocalEvidence,
reasonCode: 'missing_local_evidence',
subject: $subject,
trust: 'none',
proof: $proof,
);
}
/**
* @param array<string, string|int|float|bool|null> $proof
*/
public static function missingProviderResource(BaselineSubjectDescriptor $subject, array $proof = []): self
{
return new self(
status: self::MissingProviderResource,
reasonCode: 'missing_provider_resource',
subject: $subject,
trust: 'none',
proof: $proof,
);
}
/**
* @param array<string, string|int|float|bool|null> $proof
*/
public static function unresolvedIdentity(BaselineSubjectDescriptor $subject, array $proof = []): self
{
return new self(
status: self::UnresolvedIdentity,
reasonCode: 'identity_required',
subject: $subject,
trust: 'none',
proof: $proof,
);
}
/**
* @param array<string, string|int|float|bool|null> $proof
*/
public static function unsupported(BaselineSubjectDescriptor $subject, array $proof = []): self
{
return new self(
status: self::Unsupported,
reasonCode: 'unsupported_subject',
subject: $subject,
trust: 'none',
proof: $proof,
);
}
/**
* @param array<string, string|int|float|bool|null> $proof
*/
public static function limited(BaselineSubjectDescriptor $subject, string $reasonCode = 'accepted_limitation', array $proof = []): self
{
return new self(
status: self::Limited,
reasonCode: $reasonCode,
subject: $subject,
trust: 'limited',
proof: $proof,
);
}
/**
* @param array<string, string|int|float|bool|null> $proof
*/
public static function excluded(BaselineSubjectDescriptor $subject, array $proof = []): self
{
return new self(
status: self::Excluded,
reasonCode: 'excluded_non_governed',
subject: $subject,
trust: 'none',
proof: $proof,
);
}
/**
* @param array<string, string|int|float|bool|null> $proof
*/
public function isComparable(): bool
{
return $this->status === self::Resolved;
}
public function isGap(): bool
{
return ! $this->isComparable();
}
public function requiresWarning(): bool
{
return false;
}
public function toArray(): array
{
return [
'status' => $this->status,
'reason_code' => $this->reasonCode,
'subject' => $this->subject->toArray(),
'matched_descriptor' => $this->matchedDescriptor?->toArray(),
'matched_subject_key' => $this->matchedSubjectKey,
'trust' => $this->trust,
'proof' => $this->safeProof($this->proof),
];
}
public function jsonSerialize(): array
{
return $this->toArray();
}
/**
* @param array<string, mixed> $proof
* @return array<string, string|int|float|bool|null>
*/
private function safeProof(array $proof): array
{
$safe = [];
foreach ($proof as $key => $value) {
if (! is_string($key) || trim($key) === '' || (! is_scalar($value) && $value !== null)) {
continue;
}
$safe[$key] = $value;
}
ksort($safe);
return $safe;
}
}