Some checks failed
Main Confidence / confidence (push) Failing after 57s
## Summary - add the provider boundary catalog, boundary support types, and guardrails for platform-core versus provider-owned seams - harden provider gateway, identity resolution, operation registry, and start-gate behavior to require explicit provider bindings - add unit and feature coverage for boundary classification, runtime preservation, unsupported paths, and platform-core leakage guards - add the full Spec Kit artifact set for spec 237 and update roadmap/spec-candidate tracking ## Validation - `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Unit/Providers/ProviderBoundaryClassificationTest.php tests/Unit/Providers/ProviderBoundaryGuardrailTest.php tests/Feature/Providers/ProviderBoundaryHardeningTest.php tests/Feature/Providers/UnsupportedProviderBoundaryPathTest.php tests/Feature/Guards/ProviderBoundaryPlatformCoreGuardTest.php` - `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Unit/Providers/ProviderGatewayTest.php tests/Unit/Providers/ProviderIdentityResolverTest.php tests/Unit/Providers/ProviderOperationStartGateTest.php` - `cd apps/platform && ./vendor/bin/sail bin pint --dirty --format agent` - browser smoke: `http://localhost/admin/provider-connections?tenant_id=18000000-0000-4000-8000-000000000180` loaded with the local smoke user, the empty-state CTA reached the canonical create route, and cancel returned to the scoped list Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de> Reviewed-on: #273
150 lines
4.8 KiB
PHP
150 lines
4.8 KiB
PHP
<?php
|
|
|
|
namespace App\Support\Providers\Boundary;
|
|
|
|
use InvalidArgumentException;
|
|
|
|
final class ProviderBoundarySeam
|
|
{
|
|
public const string FOLLOW_UP_NONE = 'none';
|
|
|
|
public const string FOLLOW_UP_DOCUMENT_IN_FEATURE = 'document-in-feature';
|
|
|
|
public const string FOLLOW_UP_SPEC = 'follow-up-spec';
|
|
|
|
/**
|
|
* @param list<string> $implementationPaths
|
|
* @param list<string> $neutralTerms
|
|
* @param list<string> $retainedProviderSemantics
|
|
*/
|
|
public function __construct(
|
|
public readonly string $key,
|
|
public readonly ProviderBoundaryOwner $owner,
|
|
public readonly string $description,
|
|
public readonly array $implementationPaths,
|
|
public readonly array $neutralTerms,
|
|
public readonly array $retainedProviderSemantics,
|
|
public readonly string $followUpAction,
|
|
) {
|
|
$this->validate();
|
|
}
|
|
|
|
/**
|
|
* @param array{
|
|
* owner?: string,
|
|
* description?: string,
|
|
* implementation_paths?: list<string>,
|
|
* neutral_terms?: list<string>,
|
|
* retained_provider_semantics?: list<string>,
|
|
* follow_up_action?: string
|
|
* } $attributes
|
|
*/
|
|
public static function fromConfig(string $key, array $attributes): self
|
|
{
|
|
$owner = ProviderBoundaryOwner::tryFrom((string) ($attributes['owner'] ?? ''));
|
|
|
|
if (! $owner instanceof ProviderBoundaryOwner) {
|
|
throw new InvalidArgumentException("Provider boundary seam [{$key}] has an invalid owner.");
|
|
}
|
|
|
|
return new self(
|
|
key: $key,
|
|
owner: $owner,
|
|
description: (string) ($attributes['description'] ?? ''),
|
|
implementationPaths: self::stringList($attributes['implementation_paths'] ?? []),
|
|
neutralTerms: self::stringList($attributes['neutral_terms'] ?? []),
|
|
retainedProviderSemantics: self::stringList($attributes['retained_provider_semantics'] ?? []),
|
|
followUpAction: (string) ($attributes['follow_up_action'] ?? self::FOLLOW_UP_NONE),
|
|
);
|
|
}
|
|
|
|
public function isProviderOwned(): bool
|
|
{
|
|
return $this->owner === ProviderBoundaryOwner::ProviderOwned;
|
|
}
|
|
|
|
public function isPlatformCore(): bool
|
|
{
|
|
return $this->owner === ProviderBoundaryOwner::PlatformCore;
|
|
}
|
|
|
|
public function retainsProviderSemantics(): bool
|
|
{
|
|
return $this->retainedProviderSemantics !== [];
|
|
}
|
|
|
|
public function documentsProviderSemantic(string $term): bool
|
|
{
|
|
return in_array($term, $this->retainedProviderSemantics, true);
|
|
}
|
|
|
|
public function coversPath(string $path): bool
|
|
{
|
|
$normalizedPath = $this->normalizePath($path);
|
|
|
|
foreach ($this->implementationPaths as $implementationPath) {
|
|
if ($normalizedPath === $this->normalizePath($implementationPath)) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* @param array<mixed> $values
|
|
* @return list<string>
|
|
*/
|
|
private static function stringList(array $values): array
|
|
{
|
|
return array_values(array_filter(
|
|
array_map(static fn (mixed $value): string => trim((string) $value), $values),
|
|
static fn (string $value): bool => $value !== '',
|
|
));
|
|
}
|
|
|
|
private function validate(): void
|
|
{
|
|
if (trim($this->key) === '') {
|
|
throw new InvalidArgumentException('Provider boundary seam key cannot be empty.');
|
|
}
|
|
|
|
if (trim($this->description) === '') {
|
|
throw new InvalidArgumentException("Provider boundary seam [{$this->key}] must include a description.");
|
|
}
|
|
|
|
if ($this->implementationPaths === []) {
|
|
throw new InvalidArgumentException("Provider boundary seam [{$this->key}] must include implementation paths.");
|
|
}
|
|
|
|
if ($this->isPlatformCore() && $this->neutralTerms === []) {
|
|
throw new InvalidArgumentException("Platform-core provider boundary seam [{$this->key}] must include neutral terms.");
|
|
}
|
|
|
|
if ($this->retainsProviderSemantics() && $this->followUpAction === self::FOLLOW_UP_NONE) {
|
|
throw new InvalidArgumentException("Provider boundary seam [{$this->key}] retains provider semantics without a follow-up action.");
|
|
}
|
|
|
|
if (! in_array($this->followUpAction, $this->validFollowUpActions(), true)) {
|
|
throw new InvalidArgumentException("Provider boundary seam [{$this->key}] has an invalid follow-up action.");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @return list<string>
|
|
*/
|
|
private function validFollowUpActions(): array
|
|
{
|
|
return [
|
|
self::FOLLOW_UP_NONE,
|
|
self::FOLLOW_UP_DOCUMENT_IN_FEATURE,
|
|
self::FOLLOW_UP_SPEC,
|
|
];
|
|
}
|
|
|
|
private function normalizePath(string $path): string
|
|
{
|
|
return trim(str_replace('\\', '/', $path), '/');
|
|
}
|
|
}
|