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
@@ -0,0 +1,100 @@
<?php
declare(strict_types=1);
namespace LIT\MCP\Integrations\Divi\Release;
use LIT\MCP\Abilities\Ability;
use LIT\MCP\Abilities\DeclaresRequirements;
use LIT\MCP\Integrations\Divi\Library\DiviLibraryStore;
use LIT\MCP\Integrations\Divi\ThemeBuilder\DiviThemeBuilder;
use LIT\MCP\Integrations\WordPress\PostGuard;
defined('ABSPATH') || exit;
/** Helpers of RequiresDiviFeatures::diviFeatures(): joining lists and the capabilities an entry adds. */
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; an ID that is no
* DiviFeature is ignored (the core may name capabilities of other integrations).
*
* @param array<string, mixed> $input
* @return list<DiviFeature>
*/
public static function declaredBy(Ability $ability, array $input): array
{
if ($ability instanceof RequiresDiviFeatures) {
return self::merge($ability->diviFeatures($input));
}
if (! $ability instanceof DeclaresRequirements) {
return [];
}
$features = [];
foreach ($ability->requirements($input) as $id) {
$feature = DiviFeature::tryFrom($id);
if ($feature !== null) {
$features[] = $feature;
}
}
return self::merge($features);
}
/**
* @param list<DiviFeature> ...$lists
* @return list<DiviFeature> Every capability once, in the order of first appearance.
*/
public static function merge(array ...$lists): array
{
$merged = [];
foreach ($lists as $list) {
foreach ($list as $feature) {
$merged[$feature->value] ??= $feature;
}
}
return array_values($merged);
}
/**
* What the entry $postId adds to a tool that loads it. A page or post adds nothing; a library entry needs the
* Divi roles (LibraryAccess), written also the library check of global entries and the library save scope
* (DiviSaver, D5); a Theme Builder layout needs the roles, written also the template lookup of
* ThemeBuilderEdits and the Theme Builder save scope. An ID that names no entry, or one the caller may not edit,
* adds nothing – the tool answers "not found" or "forbidden" itself, and a lock never tells which kind of entry
* an ID is (ruling P12).
*
* @return list<DiviFeature>
*/
public static function ofEntry(mixed $postId, bool $write): array
{
$id = PostGuard::id($postId);
$type = $id === null ? false : get_post_type($id);
if ($id === null || ! is_string($type)) {
return [];
}
$isLibrary = $type === DiviLibraryStore::POST_TYPE;
if (! $isLibrary && DiviThemeBuilder::areaOf($type) === null) {
return [];
}
if (! current_user_can('edit_post', $id)) {
return [];
}
if ($isLibrary) {
return $write
? [DiviFeature::Roles, DiviFeature::LibraryRead, DiviFeature::LibraryWrite]
: [DiviFeature::Roles];
}
return $write
? [DiviFeature::Roles, DiviFeature::ThemeBuilderRead, DiviFeature::ThemeBuilderWrite]
: [DiviFeature::Roles];
}
}
@@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace LIT\MCP\Integrations\Divi\Release;
defined('ABSPATH') || exit;
/**
* A tool that says which Divi capabilities a call needs. DiviFeatureGate refuses the call before it runs while one of
* them is not usable; the adapters still check each write themselves (ReleaseGate::assertFeature()).
*/
interface RequiresDiviFeatures
{
/**
* The capabilities this call needs for $input, the upper bound of what the call may reach: a capability an
* adapter checks later must be in it, or the tool would stop in the middle of its work instead of before.
* [] = none (a tool that never touches Divi).
*
* @param array<string, mixed> $input
* @return list<DiviFeature>
*/
public function diviFeatures(array $input): array;
}