feat: DiviFeatureGate ersetzt DiviToolGate – Ablehnung je Fähigkeit für Lese- und Schreib-Tools, Details nach D6, kein Hinweis mehr in warnings; DiviContentComposer::refusal prüft seine Fähigkeiten
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5.5
parent
fdbda8012c
commit
74cf0c54a8
@@ -0,0 +1,112 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Integrations\Divi\Release;
|
||||
|
||||
use LIT\MCP\Abilities\Ability;
|
||||
use LIT\MCP\Abilities\AbilityGuard;
|
||||
use LIT\MCP\ChangeLog\ChangeLog;
|
||||
use WP_Error;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
/**
|
||||
* The central release lock of the Divi tools (spec §5.5), per capability: a call is refused before it runs while one
|
||||
* of the capabilities it declares for its input is not usable – reading and writing tools alike, so a reading tool
|
||||
* answers divi_incompatible / divi_untested with the affected capability instead of failing inside Divi. A refused
|
||||
* write is logged with result "refused" (as before); a refused read changes nothing and is not. A tool that declares
|
||||
* nothing (RequiresDiviFeatures, or DeclaresRequirements for lit/content-create without divi) is left alone; the
|
||||
* adapters still check each write themselves.
|
||||
*/
|
||||
final class DiviFeatureGate implements AbilityGuard
|
||||
{
|
||||
public function __construct(private ?ChangeLog $log = null)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* The lock for the usable-ness of $features, or null when every one is usable. Public: the Divi short form of
|
||||
* lit/content-create asks the same question for its own capabilities (DiviContentComposer::refusal()).
|
||||
*
|
||||
* @param list<DiviFeature> $features
|
||||
*/
|
||||
public static function lockFor(array $features): ?DiviLocked
|
||||
{
|
||||
$unusable = [];
|
||||
foreach ($features as $feature) {
|
||||
if (! ReleaseGate::feature($feature)->usable()) {
|
||||
$unusable[$feature->value] = $feature;
|
||||
}
|
||||
}
|
||||
|
||||
return $unusable === [] ? null : new DiviLocked(ReleaseGate::status(), array_values($unusable));
|
||||
}
|
||||
|
||||
public function refusal(Ability $ability, array $input): ?WP_Error
|
||||
{
|
||||
$lock = self::lockFor(FeatureSets::declaredBy($ability, $input));
|
||||
if ($lock === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (! self::readsOnly($ability)) {
|
||||
$this->log?->record(
|
||||
$ability->name(),
|
||||
'divi',
|
||||
$lock->status->version,
|
||||
self::summary($lock),
|
||||
result: 'refused'
|
||||
);
|
||||
}
|
||||
|
||||
return $lock->toError();
|
||||
}
|
||||
|
||||
/** A refusal is the whole answer: there is no result to annotate (the lock of reading tools is gone, E2). */
|
||||
public function annotate(Ability $ability, mixed $result): mixed
|
||||
{
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Divi tools keep the optional warnings property in their output schema (as before the per-capability lock);
|
||||
* lit/content-create has its own.
|
||||
*
|
||||
* @param array<string, mixed>|null $schema
|
||||
* @return array<string, mixed>|null
|
||||
*/
|
||||
public function outputSchema(Ability $ability, ?array $schema): ?array
|
||||
{
|
||||
if ($schema === null || ($schema['type'] ?? null) !== 'object' || ! $ability instanceof RequiresDiviFeatures) {
|
||||
return $schema;
|
||||
}
|
||||
|
||||
$properties = is_array($schema['properties'] ?? null) ? $schema['properties'] : [];
|
||||
$properties['warnings'] ??= ['type' => 'array', 'items' => ['type' => 'string']];
|
||||
$schema['properties'] = $properties;
|
||||
|
||||
return $schema;
|
||||
}
|
||||
|
||||
private static function readsOnly(Ability $ability): bool
|
||||
{
|
||||
return ($ability->annotations()['readonly'] ?? false) === true;
|
||||
}
|
||||
|
||||
private static function summary(DiviLocked $lock): string
|
||||
{
|
||||
$parts = array_map(
|
||||
static fn (DiviFeature $feature): string => sprintf(
|
||||
'%s %s',
|
||||
$feature->label(),
|
||||
$lock->status->matrix->of($feature)->state === ReleaseState::Incompatible
|
||||
? 'inkompatibel'
|
||||
: 'nicht freigegeben'
|
||||
),
|
||||
$lock->features
|
||||
);
|
||||
|
||||
return sprintf('Abgelehnt: %s – %s.', $lock->status->versionLabel(), implode(', ', $parts));
|
||||
}
|
||||
}
|
||||
@@ -1,108 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Integrations\Divi\Release;
|
||||
|
||||
use Closure;
|
||||
use LIT\MCP\Abilities\Ability;
|
||||
use LIT\MCP\Abilities\AbilityGuard;
|
||||
use LIT\MCP\ChangeLog\ChangeLog;
|
||||
use LIT\MCP\Integrations\Divi\Library\LibraryAccess;
|
||||
use LIT\MCP\Integrations\Divi\ThemeBuilder\ThemeBuilderAccess;
|
||||
use WP_Error;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
/**
|
||||
* The central release lock of the Divi tools (spec §5.5): a writing tool of the groups Divi, Divi-Bibliothek or Divi
|
||||
* Theme Builder is refused before it runs while Divi is not released (logged with result "refused"); a reading tool
|
||||
* runs and gets the notice in warnings. Other tools that reach Divi (lit/content-create with divi) meet the adapter
|
||||
* lock (ReleaseGate::assertWritable()). The AbilityRegistrar receives it as an AbilityGuard from Plugin.
|
||||
*/
|
||||
final class DiviToolGate implements AbilityGuard
|
||||
{
|
||||
public const GROUPS = ['divi', LibraryAccess::GROUP, ThemeBuilderAccess::GROUP];
|
||||
|
||||
/** @var Closure(): array<string, string> */
|
||||
private Closure $groups;
|
||||
|
||||
/**
|
||||
* @param Closure(): array<string, string> $groups Ability name => integration ID (IntegrationRegistry::groupMap).
|
||||
*/
|
||||
public function __construct(Closure $groups, private ?ChangeLog $log = null)
|
||||
{
|
||||
$this->groups = $groups;
|
||||
}
|
||||
|
||||
public function covers(Ability $ability): bool
|
||||
{
|
||||
return in_array(($this->groups)()[$ability->name()] ?? '', self::GROUPS, true);
|
||||
}
|
||||
|
||||
public function refusal(Ability $ability, array $input): ?WP_Error
|
||||
{
|
||||
if (! $this->covers($ability) || self::readsOnly($ability)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$status = ReleaseGate::status();
|
||||
if ($status->writable()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$this->log?->record(
|
||||
$ability->name(),
|
||||
'divi',
|
||||
$status->version,
|
||||
sprintf(
|
||||
'Abgelehnt: %s ist %s.',
|
||||
$status->versionLabel(),
|
||||
$status->state === ReleaseState::Incompatible ? 'inkompatibel' : 'nicht freigegeben'
|
||||
),
|
||||
result: 'refused'
|
||||
);
|
||||
|
||||
return $status->refusal();
|
||||
}
|
||||
|
||||
public function annotate(Ability $ability, mixed $result): mixed
|
||||
{
|
||||
if (! is_array($result) || array_is_list($result) || ! $this->covers($ability) || ! self::readsOnly($ability)) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
$status = ReleaseGate::status();
|
||||
if ($status->writable()) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
$warnings = is_array($result['warnings'] ?? null) ? array_values($result['warnings']) : [];
|
||||
$warnings[] = $status->notice();
|
||||
$result['warnings'] = $warnings;
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed>|null $schema
|
||||
* @return array<string, mixed>|null
|
||||
*/
|
||||
public function outputSchema(Ability $ability, ?array $schema): ?array
|
||||
{
|
||||
if ($schema === null || ($schema['type'] ?? null) !== 'object' || ! $this->covers($ability)) {
|
||||
return $schema;
|
||||
}
|
||||
|
||||
$properties = is_array($schema['properties'] ?? null) ? $schema['properties'] : [];
|
||||
$properties['warnings'] ??= ['type' => 'array', 'items' => ['type' => 'string']];
|
||||
$schema['properties'] = $properties;
|
||||
|
||||
return $schema;
|
||||
}
|
||||
|
||||
private static function readsOnly(Ability $ability): bool
|
||||
{
|
||||
return ($ability->annotations()['readonly'] ?? false) === true;
|
||||
}
|
||||
}
|
||||
@@ -76,7 +76,7 @@ final class ReleaseStatus
|
||||
return new self(ReleaseState::Released, $version, $source);
|
||||
}
|
||||
|
||||
/** Every writing feature is usable – for callers that know no feature (DiviToolGate, DiviContentComposer). */
|
||||
/** Every writing feature is usable – for callers that know no feature (general write lock of DiviLocked). */
|
||||
public function writable(): bool
|
||||
{
|
||||
return $this->matrix->usable(CompatibilityMatrix::writeFeatures());
|
||||
|
||||
Reference in New Issue
Block a user