feat(compat): Profile, Gates und Pods über den Kompatibilitätskern

Neu: Tier, Profile, Compatibility, CapabilityLocked, CapabilityGate, CompatibilityComponent, ProvidesCompatibility, DiviProfile sowie PodsFeature, PodsCompatibility und PodsIntegration. Das CapabilityGate sperrt gesperrte und unbekannte Fähigkeits-IDs (übernimmt die Sperre unbekannter IDs vom DiviFeatureGate) und delegiert nur Divi ausdrücklich. Pods-Schema, -Lesen, -Schreiben und -Feldrechte laufen über usable(PodsFeature::…); Schreiben verlangt FieldRead und FieldWrite, damit ein Rollback keine Werte verliert. lit-site-status meldet Pods über die CompatibilityComponent.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Jan-Niclas Loosen
2026-11-06 12:26:54 +01:00
co-authored by Claude Opus 5.5
parent b511ef6348
commit c85131603b
38 changed files with 2259 additions and 135 deletions
+8 -1
View File
@@ -9,6 +9,7 @@ use LIT\MCP\Abilities\Ability;
use LIT\MCP\Abilities\ToolAvailability;
use LIT\MCP\Admin\SettingsPage;
use LIT\MCP\ChangeLog\ChangeLog;
use LIT\MCP\Compatibility\ProvidesCompatibility;
use LIT\MCP\Integrations\Bootable;
use LIT\MCP\Integrations\Divi\Admin\DiviTab;
use LIT\MCP\Integrations\Divi\Cli\DiviCheckCommand;
@@ -75,6 +76,7 @@ use LIT\MCP\Integrations\Divi\Pages\DynamicContentMap;
use LIT\MCP\Integrations\Divi\Release\CompatibilityCheck;
use LIT\MCP\Integrations\Divi\Release\DiviCheckRoute;
use LIT\MCP\Integrations\Divi\Release\FeatureOverview;
use LIT\MCP\Integrations\Divi\Release\DiviProfile;
use LIT\MCP\Integrations\Divi\Release\ReleaseGate;
use LIT\MCP\Integrations\Divi\Schema\DiviCatalogAbility;
use LIT\MCP\Integrations\Divi\Schema\DiviModules;
@@ -99,7 +101,7 @@ defined('ABSPATH') || exit;
* DiviWriter; each binds the composition to its entry via DiviWriter::composition().
* contentComposer() gives lit/content-create the Divi short form (spec §6.2). boot() registers Divi's own hooks.
*/
final class DiviIntegration implements Integration, Bootable
final class DiviIntegration implements Integration, Bootable, ProvidesCompatibility
{
private DiviModules $modules;
private DesignSystemReader $design;
@@ -186,6 +188,11 @@ final class DiviIntegration implements Integration, Bootable
return DiviModules::isAvailable();
}
public function compatibilities(): array
{
return [DiviProfile::profile()];
}
/**
* Divi's own post types (library, Theme Builder, canvases, speculation), which the content tools leave alone:
* ContentSchema's excluded post types. Static because the schema is built before this integration (Plugin).
@@ -6,7 +6,6 @@ namespace LIT\MCP\Integrations\Divi\Release;
use LIT\MCP\Abilities\Ability;
use LIT\MCP\Abilities\AbilityGuard;
use LIT\MCP\Abilities\Errors;
use LIT\MCP\ChangeLog\ChangeLog;
use LIT\MCP\Compatibility\ReleaseState;
use WP_Error;
@@ -19,8 +18,8 @@ defined('ABSPATH') || exit;
* 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. A declared ID that is no Divi capability (FeatureSets::unknownIds())
* locks the call as divi_incompatible with details.unknown – the lock fails closed.
* adapters still check each write themselves. IDs that are no Divi capability are ignored here; CapabilityGate
* checks them (and locks the unknown ones).
*/
final class DiviFeatureGate implements AbilityGuard
{
@@ -30,11 +29,6 @@ final class DiviFeatureGate implements AbilityGuard
public function refusal(Ability $ability, array $input): ?WP_Error
{
$unknown = FeatureSets::unknownIds($ability, $input);
if ($unknown !== []) {
return $this->refuseUnknown($ability, $unknown);
}
$lock = ReleaseGate::lockOf(FeatureSets::declaredBy($ability, $input));
if ($lock === null) {
return null;
@@ -53,34 +47,6 @@ final class DiviFeatureGate implements AbilityGuard
return $lock->toError();
}
/** @param non-empty-list<string> $unknown Declared IDs that are no Divi capability. */
private function refuseUnknown(Ability $ability, array $unknown): WP_Error
{
$status = ReleaseGate::status();
$ids = implode(', ', $unknown);
if (! self::readsOnly($ability)) {
$this->log?->record(
$ability->name(),
'divi',
$status->version,
sprintf('Abgelehnt: %s – unbekannte Fähigkeit %s.', $status->versionLabel(), $ids),
result: 'refused'
);
}
return Errors::make(
'divi_incompatible',
sprintf(
'%s: Das Werkzeug verlangt unbekannte Fähigkeiten (%s). Das ist gesperrt. Es wurde nichts geändert.',
$status->versionLabel(),
$ids
),
'Den Nutzer informieren, dass LIT MCP aktualisiert werden muss.',
423,
['unknown' => $unknown]
);
}
/** 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
{
@@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
namespace LIT\MCP\Integrations\Divi\Release;
use LIT\MCP\Compatibility\Profile;
use LIT\MCP\Compatibility\Tier;
defined('ABSPATH') || exit;
/** Divi as a compatibility profile: every version needs a release, the matrix comes from the release gate. */
final class DiviProfile
{
public static function profile(): Profile
{
return new Profile(
'divi',
'Divi',
Tier::ReleasePerVersion,
DiviFeature::cases(),
array_values(DiviAdapters::manifests()),
version: DiviEnvironment::version(...),
matrix: static fn () => ReleaseGate::status()->matrix
);
}
}
+2 -21
View File
@@ -17,8 +17,8 @@ final class FeatureSets
{
/**
* The capabilities $ability declares for $input: its own diviFeatures() (the Divi tools) or the IDs a core tool
* names through DeclaresRequirements (lit/content-create with divi). Anything else declares none. Every declared
* ID is a Divi ID; one that is no DiviFeature is skipped here but locks the call (unknownIds(), fail closed).
* names through DeclaresRequirements (lit/content-create with divi). Anything else declares none; an ID that is no
* DiviFeature is ignored (CapabilityGate checks the IDs of other integrations and locks unknown ones).
*
* @param array<string, mixed> $input
* @return list<DiviFeature>
@@ -44,25 +44,6 @@ final class FeatureSets
return self::merge($features);
}
/**
* The IDs a core tool declares for $input that are no DiviFeature. Such a call cannot be judged, so
* DiviFeatureGate refuses it instead of letting it run unlocked.
*
* @param array<string, mixed> $input
* @return list<string>
*/
public static function unknownIds(Ability $ability, array $input): array
{
if ($ability instanceof RequiresDiviFeatures || ! $ability instanceof DeclaresRequirements) {
return [];
}
return array_values(array_filter(
$ability->requirements($input),
static fn (string $id): bool => DiviFeature::tryFrom($id) === null
));
}
/**
* @param list<DiviFeature> ...$lists
* @return list<DiviFeature> Every capability once, in the order of first appearance.