feat: jedes Divi-Tool und lit/content-create erklärt seine Divi-Fähigkeiten (RequiresDiviFeatures, DeclaresRequirements, FeatureSets); Tabelle Tool → Fähigkeiten als Test

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Jan-Niclas Loosen
2026-10-18 22:14:39 +02:00
co-authored by Claude Sonnet 5.5
parent e387d8d29e
commit fdbda8012c
39 changed files with 1131 additions and 23 deletions
@@ -18,6 +18,8 @@ use LIT\MCP\Integrations\Divi\BlockTree\NodeSummary;
use LIT\MCP\Integrations\Divi\BlockTree\Parser;
use LIT\MCP\Integrations\Divi\Pages\DiviErrors;
use LIT\MCP\Integrations\Divi\Pages\DiviPageLoader;
use LIT\MCP\Integrations\Divi\Release\DiviFeature;
use LIT\MCP\Integrations\Divi\Release\RequiresDiviFeatures;
use LIT\MCP\Integrations\Divi\ThemeBuilder\DiviThemeBuilder;
use LIT\MCP\Integrations\WordPress\PostGuard;
use LIT\MCP\Integrations\WordPress\Schema\ContentSchema;
@@ -37,7 +39,7 @@ defined('ABSPATH') || exit;
* and next_cursor resumes behind it. Needs the library right (canList); a hidden library answers its entries like
* missing IDs (LibraryAccess::item, P9/P12). Only hashes and the matched nodes' labels are returned.
*/
final class LibraryFindSimilarAbility implements Ability
final class LibraryFindSimilarAbility implements Ability, RequiresDiviFeatures
{
public const MAX_POSTS = 200;
public const DEFAULT_POSTS = 100;
@@ -121,6 +123,15 @@ final class LibraryFindSimilarAbility implements Ability
return false;
}
/**
* Scans Divi pages (PageRead) of the types that use the builder (Catalog), after the library right (Roles). A
* library entry as reference is read through LibraryAccess (Roles), no Divi call beyond that.
*/
public function diviFeatures(array $input): array
{
return [DiviFeature::PageRead, DiviFeature::Catalog, DiviFeature::Roles];
}
public function permission(array $input): bool|WP_Error
{
return $this->access->canList() ?? true;
@@ -19,6 +19,7 @@ use LIT\MCP\Integrations\Divi\Pages\DiviPage;
use LIT\MCP\Integrations\Divi\Pages\DiviSchema;
use LIT\MCP\Integrations\Divi\Pages\DiviWriteAbility;
use LIT\MCP\Integrations\Divi\Pages\DiviWriter;
use LIT\MCP\Integrations\Divi\Release\DiviFeature;
use LIT\MCP\Integrations\WordPress\PostGuard;
use WP_Error;
use WP_Post;
@@ -103,6 +104,23 @@ final class LibraryInsertAbility extends DiviWriteAbility
return null;
}
/**
* Like lit/divi-insert (StoredNodeGuard cleans the entry's nodes: Catalog, DesignRead, DynamicSources), plus the
* library entry as source: isGlobal() is a Divi call (LibraryRead), its rights are a Divi role (Roles).
*/
protected function baseFeatures(): array
{
return [
DiviFeature::PageRead,
DiviFeature::PageWrite,
DiviFeature::Catalog,
DiviFeature::DesignRead,
DiviFeature::DynamicSources,
DiviFeature::LibraryRead,
DiviFeature::Roles,
];
}
protected function change(DiviPage $page, array $input, DiviComposition $composition): DiviChange|WP_Error
{
$mode = $input['mode'] ?? null;
@@ -13,6 +13,8 @@ use LIT\MCP\Integrations\Divi\BlockTree\Outline;
use LIT\MCP\Integrations\Divi\Design\DesignSystemReader;
use LIT\MCP\Integrations\Divi\Pages\DiviPage;
use LIT\MCP\Integrations\Divi\Pages\DiviPageLoader;
use LIT\MCP\Integrations\Divi\Release\DiviFeature;
use LIT\MCP\Integrations\Divi\Release\RequiresDiviFeatures;
use LIT\MCP\Integrations\WordPress\PostGuard;
use WP_Error;
use WP_Post;
@@ -27,7 +29,7 @@ defined('ABSPATH') || exit;
* left out, without title or count). With id,
* everything but a library entry the user may read answers like a missing ID (ruling P12).
*/
final class LibraryListAbility implements Ability
final class LibraryListAbility implements Ability, RequiresDiviFeatures
{
/** Rows looked at per call beyond the entries already shown (the cursor), skipping hidden ones. */
public const MAX_SCAN = 500;
@@ -121,6 +123,19 @@ final class LibraryListAbility implements Ability
return false;
}
/**
* describe() asks Divi whether an entry is global (LibraryRead); listing needs the library role (Roles). One
* entry (id) is loaded and outlined (PageRead, DesignRead).
*/
public function diviFeatures(array $input): array
{
$features = [DiviFeature::LibraryRead, DiviFeature::Roles];
return isset($input['id'])
? array_merge($features, [DiviFeature::PageRead, DiviFeature::DesignRead])
: $features;
}
public function permission(array $input): bool|WP_Error
{
if (isset($input['id'])) {
@@ -23,6 +23,8 @@ use LIT\MCP\Integrations\Divi\Pages\DiviPage;
use LIT\MCP\Integrations\Divi\Pages\DiviSchema;
use LIT\MCP\Integrations\Divi\Pages\DiviWriter;
use LIT\MCP\Integrations\Divi\Pages\VisibleEdits;
use LIT\MCP\Integrations\Divi\Release\DiviFeature;
use LIT\MCP\Integrations\Divi\Release\RequiresDiviFeatures;
use LIT\MCP\Integrations\WordPress\Content\NewDraft;
use LIT\MCP\Integrations\WordPress\PostGuard;
use LIT\MCP\Integrations\WordPress\PostSaveFollowUp;
@@ -59,7 +61,7 @@ defined('ABSPATH') || exit;
* point), page N is rolled back by DiviSaver (or reported partial), pages after N are not attempted – the error
* names all three groups (changed_posts with pages, failed_post, unchanged_posts; D10, S10).
*/
final class LibraryMakeGlobalAbility implements Ability
final class LibraryMakeGlobalAbility implements Ability, RequiresDiviFeatures
{
public const REASON_PAGE = 'Die Seite konnte nicht gespeichert werden; der neue globale Eintrag wurde wieder '
. 'entfernt.';
@@ -176,6 +178,25 @@ final class LibraryMakeGlobalAbility implements Ability
return false;
}
/**
* Both modes write pages and posts (PageRead, PageWrite, Catalog, DesignRead, DynamicSources) and ask Divi
* whether an entry is global (LibraryRead); the first mode also creates the global entry (LibraryWrite).
* One upper bound for both modes (the second mode would not reach LibraryWrite): the roles of the library.
*/
public function diviFeatures(array $input): array
{
return [
DiviFeature::PageRead,
DiviFeature::PageWrite,
DiviFeature::Catalog,
DiviFeature::DesignRead,
DiviFeature::DynamicSources,
DiviFeature::LibraryRead,
DiviFeature::LibraryWrite,
DiviFeature::Roles,
];
}
public function permission(array $input): bool|WP_Error
{
return $this->precheck($input) ?? true;
@@ -17,6 +17,8 @@ use LIT\MCP\Integrations\Divi\Pages\DiviPage;
use LIT\MCP\Integrations\Divi\Pages\DiviPageLoader;
use LIT\MCP\Integrations\Divi\Pages\DiviSaver;
use LIT\MCP\Integrations\Divi\Pages\DiviWriter;
use LIT\MCP\Integrations\Divi\Release\DiviFeature;
use LIT\MCP\Integrations\Divi\Release\RequiresDiviFeatures;
use LIT\MCP\Integrations\Divi\Schema\DiviModules;
use LIT\MCP\Integrations\WordPress\Content\NewDraft;
use LIT\MCP\Integrations\WordPress\PostSaveFollowUp;
@@ -35,7 +37,7 @@ defined('ABSPATH') || exit;
* to the entry is lit/library-make-global. All or nothing through NewDraft (ruling P6): any failure
* after the insert deletes the new entry, a failed delete is an honest partial_write.
*/
final class LibrarySaveAbility implements Ability
final class LibrarySaveAbility implements Ability, RequiresDiviFeatures
{
private const TYPE_LABELS = ['section' => 'Abschnitt', 'row' => 'Zeile', 'module' => 'Modul'];
@@ -112,6 +114,24 @@ final class LibrarySaveAbility implements Ability
return false;
}
/**
* Reads a node of any entry (PageRead), cleans it with StoredNodeGuard (Catalog, DesignRead, DynamicSources) and
* saves a new library entry (PageWrite and LibraryWrite: DiviSaver::saveNew with the library scope, D5), after
* the library right (Roles). The source is only read: a library entry or Theme Builder layout adds Roles only.
*/
public function diviFeatures(array $input): array
{
return [
DiviFeature::PageRead,
DiviFeature::PageWrite,
DiviFeature::Catalog,
DiviFeature::DesignRead,
DiviFeature::DynamicSources,
DiviFeature::LibraryWrite,
DiviFeature::Roles,
];
}
public function permission(array $input): bool|WP_Error
{
$refusal = $this->access->canSave(self::isGlobal($input));