refactor(compat): Prüfbausteine nach src/Compatibility, Matrix und ManifestCheck generisch

Requirement, Call, ConstantRequirement, Mirror, Problem, FeatureStatus, CompatibilityMatrix, Manifest, ManifestCheck sowie Fingerprints und LineDiff liegen jetzt im divi-freien Kern. Neu: GlobalRequirement, CompatibilityMatrix::fromProblems(), ManifestCheck-Option signatures, DiviManifestCheck::create() und DiviAreas. Divi-Verhalten unverändert.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Jan-Niclas Loosen
2026-11-06 12:03:30 +01:00
co-authored by Claude Opus 5.5
parent 9beb55e228
commit b511ef6348
92 changed files with 1185 additions and 636 deletions
+81
View File
@@ -0,0 +1,81 @@
<?php
declare(strict_types=1);
namespace LIT\MCP\Compatibility;
use ReflectionFunction;
use ReflectionMethod;
defined('ABSPATH') || exit;
/**
* One function or static method of a third-party plugin or theme that an adapter calls (spec §4.1), with the number
* of arguments the plugin passes. $args null: only its existence matters (a function the third-party code itself
* needs, e.g. et_() inside store_template in the Divi integration).
*/
final class Call implements Requirement
{
/** Key prefix of the signature fingerprints in the reference; no mirror ID starts with it. */
public const SIGNATURE_PREFIX = 'sig:';
/**
* @param string $symbol "function_name" or "Fully\Qualified\Class::method".
* @param list<Capability> $features [] = the default of the manifest.
*/
public function __construct(
public readonly string $symbol,
public readonly ?int $args = null,
private readonly array $features = []
) {
}
public function className(): ?string
{
return str_contains($this->symbol, '::') ? ltrim(explode('::', $this->symbol, 2)[0], '\\') : null;
}
/** The key of this call's signature fingerprint in the reference: "sig:" and the symbol without a leading backslash. */
public function signatureKey(): string
{
return self::SIGNATURE_PREFIX . ltrim($this->symbol, '\\');
}
public function member(): string
{
return str_contains($this->symbol, '::') ? explode('::', $this->symbol, 2)[1] : $this->symbol;
}
/** The function or method exists and takes the number of arguments the adapter passes. */
public function accepts(): bool
{
$class = $this->className();
if ($class === null) {
if (! function_exists($this->member())) {
return false;
}
$reflection = new ReflectionFunction($this->member());
} else {
if (! method_exists($class, $this->member())) {
return false;
}
$reflection = new ReflectionMethod($class, $this->member());
}
return $this->args === null
|| ($reflection->getNumberOfRequiredParameters() <= $this->args
&& ($reflection->isVariadic() || $reflection->getNumberOfParameters() >= $this->args));
}
public function features(): array
{
return $this->features;
}
public function label(): string
{
$class = $this->className();
return ($class === null ? '' : SourceFingerprint::shortName($class) . '::') . $this->member() . '()';
}
}
+230
View File
@@ -0,0 +1,230 @@
<?php
declare(strict_types=1);
namespace LIT\MCP\Compatibility;
defined('ABSPATH') || exit;
/**
* The release state of every capability of one integration (E1). A capability the constructor did not get a status
* for is incompatible (fail closed), so of() always answers. The overall state of the matrix is its worst state.
*/
final class CompatibilityMatrix
{
public const MISSING = 'Für diese Fähigkeit liegt kein Prüfergebnis vor.';
/** Where a release comes from: the plugin's list of tested versions, the site's database, the automatic check. */
public const SOURCE_CODE = 'code';
public const SOURCE_DATABASE = 'database';
public const SOURCE_AUTO = 'auto';
/** @var array<string, FeatureStatus> keyed by capability ID, in the order of the capabilities */
private array $features = [];
/**
* @param list<Capability> $capabilities
* @param array<string, FeatureStatus> $statuses keyed by capability ID
*/
public function __construct(array $capabilities, array $statuses)
{
foreach ($capabilities as $capability) {
$status = $statuses[$capability->id()] ?? null;
$this->features[$capability->id()] = $status instanceof FeatureStatus && $status->feature === $capability
? $status
: self::missing($capability);
}
}
private static function missing(Capability $capability): FeatureStatus
{
return new FeatureStatus($capability, ReleaseState::Incompatible, [self::MISSING]);
}
/** @param list<Capability> $capabilities */
public static function allReleased(array $capabilities, string $source = self::SOURCE_CODE): self
{
return self::uniform($capabilities, ReleaseState::Released, [], $source);
}
/** @param list<Capability> $capabilities */
public static function allIncompatible(array $capabilities, string $problem): self
{
return self::uniform($capabilities, ReleaseState::Incompatible, [$problem]);
}
/**
* Every capability in $state – the matrix of a status built without one (ReleaseStatus' constructor).
*
* @param list<Capability> $capabilities
* @param list<string> $problems
*/
public static function uniform(
array $capabilities,
ReleaseState $state,
array $problems = [],
?string $source = null
): self {
$released = $state === ReleaseState::Released;
$statuses = [];
foreach ($capabilities as $capability) {
$statuses[$capability->id()] = new FeatureStatus(
$capability,
$state,
$released ? [] : $problems,
$released ? ($source ?? self::SOURCE_CODE) : null
);
}
return new self($capabilities, $statuses);
}
/**
* The matrix the problems of a check add up to (spec §5.2–§5.4): per capability a hard problem (call, mirror,
* constant) makes it incompatible, released version or not; a released version releases every other one; without
* release the automatic release decides (changed signature: incompatible, missing reference: untested, no
* problem: released from SOURCE_AUTO), else it is untested.
*
* @param list<Capability> $capabilities
* @param list<Problem> $problems
* @param string|null $source SOURCE_CODE or SOURCE_DATABASE for a released version, else null.
* @param bool $automatic The automatic release applies (no release, known version, filter on).
*/
public static function fromProblems(array $capabilities, array $problems, ?string $source, bool $automatic): self
{
$statuses = [];
foreach ($capabilities as $capability) {
$concerning = array_values(array_filter(
$problems,
static fn (Problem $problem): bool => $problem->concerns($capability)
));
$statuses[$capability->id()] = self::statusOf($capability, $concerning, $source, $automatic);
}
return new self($capabilities, $statuses);
}
/** @param list<Problem> $problems The problems concerning $capability. */
private static function statusOf(
Capability $capability,
array $problems,
?string $source,
bool $automatic
): FeatureStatus {
$hard = array_filter($problems, static fn (Problem $problem): bool => $problem->isHard());
if ($hard !== []) {
return new FeatureStatus($capability, ReleaseState::Incompatible, self::labels($hard));
}
if ($source !== null) {
return new FeatureStatus($capability, ReleaseState::Released, [], $source);
}
if (! $automatic) {
return new FeatureStatus($capability, ReleaseState::Untested, self::labels($problems));
}
$changed = array_filter($problems, static fn (Problem $problem): bool => $problem->kind === Problem::SIGNATURE);
if ($changed !== []) {
return new FeatureStatus($capability, ReleaseState::Incompatible, self::labels($changed));
}
return $problems === []
? new FeatureStatus($capability, ReleaseState::Released, [], self::SOURCE_AUTO)
: new FeatureStatus($capability, ReleaseState::Untested, self::labels($problems));
}
/**
* @param array<int, Problem> $problems
* @return list<string>
*/
private static function labels(array $problems): array
{
return array_values(array_map(static fn (Problem $problem): string => $problem->label, $problems));
}
public function of(Capability $feature): FeatureStatus
{
return $this->features[$feature->id()] ?? self::missing($feature);
}
/** @param list<Capability> $features */
public function usable(array $features): bool
{
foreach ($features as $feature) {
if (! $this->of($feature)->usable()) {
return false;
}
}
return true;
}
/**
* The statuses of those of $features that are not usable – each once, in the order of $features. The one answer
* to "what locks this call" for the gates, the adapters and the
* refusals.
*
* @param list<Capability> $features
* @return list<FeatureStatus>
*/
public function locked(array $features): array
{
$locked = [];
foreach ($features as $feature) {
$status = $this->of($feature);
if (! $status->usable()) {
$locked[$feature->id()] = $status;
}
}
return array_values($locked);
}
/** @return list<FeatureStatus> */
public function unusable(): array
{
return array_values(array_filter(
$this->features,
static fn (FeatureStatus $status): bool => ! $status->usable()
));
}
/** Incompatible before untested before released. */
public function state(): ReleaseState
{
$states = array_map(static fn (FeatureStatus $status): ReleaseState => $status->state, $this->features);
return match (true) {
in_array(ReleaseState::Incompatible, $states, true) => ReleaseState::Incompatible,
in_array(ReleaseState::Untested, $states, true) => ReleaseState::Untested,
default => ReleaseState::Released,
};
}
/** The common source of a fully released matrix; null when not all released or the sources differ. */
public function source(): ?string
{
$sources = array_unique(array_map(
static fn (FeatureStatus $status): string => (string) $status->source,
$this->features
));
return $this->state() === ReleaseState::Released && count($sources) === 1 ? (string) reset($sources) : null;
}
/** @return list<string> Every problem label once, in feature order. */
public function problems(): array
{
$problems = [];
foreach ($this->features as $status) {
$problems = array_merge($problems, $status->problems);
}
return array_values(array_unique($problems));
}
/** @return list<array{id: string, label: string, state: string, problems: list<string>, source: ?string}> */
public function toArray(): array
{
return array_values(array_map(static fn (FeatureStatus $status): array => $status->toArray(), $this->features));
}
}
@@ -2,14 +2,14 @@
declare(strict_types=1);
namespace LIT\MCP\Integrations\Divi\Release;
namespace LIT\MCP\Compatibility;
defined('ABSPATH') || exit;
/** A Divi constant an adapter reads (spec §4.1); it must be defined. */
/** A constant of a third-party plugin or theme that an adapter reads (spec §4.1); it must be defined. */
final class ConstantRequirement implements Requirement
{
/** @param list<DiviFeature> $features [] = the default of the manifest. */
/** @param list<Capability> $features [] = the default of the manifest. */
public function __construct(public readonly string $name, private readonly array $features = [])
{
}
@@ -2,20 +2,20 @@
declare(strict_types=1);
namespace LIT\MCP\Integrations\Divi\Release;
namespace LIT\MCP\Compatibility;
defined('ABSPATH') || exit;
/** The release state of one Divi feature (E1): released (with its source), untested or incompatible. */
/** The release state of one capability (E1): released (with its source), untested or incompatible. */
final class FeatureStatus
{
/**
* @param list<string> $problems Labels of the problems behind the state (Problem::$label).
* @param string|null $source For a released feature: ReleaseStatus::SOURCE_CODE, SOURCE_DATABASE or
* @param string|null $source For a released feature: CompatibilityMatrix::SOURCE_CODE, SOURCE_DATABASE or
* SOURCE_AUTO; null otherwise.
*/
public function __construct(
public readonly DiviFeature $feature,
public readonly Capability $feature,
public readonly ReleaseState $state,
public readonly array $problems = [],
public readonly ?string $source = null
@@ -31,7 +31,7 @@ final class FeatureStatus
public function summary(): array
{
return [
'id' => $this->feature->value,
'id' => $this->feature->id(),
'label' => $this->feature->label(),
'state' => $this->state->value,
'problems' => $this->problems,
+32
View File
@@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace LIT\MCP\Compatibility;
defined('ABSPATH') || exit;
/** A global variable an adapter reads (e.g. a function-pointer or settings global of a plugin); it must be set. */
final class GlobalRequirement implements Requirement
{
/** @param list<Capability> $features [] = the default of the manifest. */
public function __construct(public readonly string $name, private readonly array $features = [])
{
}
public function label(): string
{
return 'Globale Variable ' . $this->name;
}
public function features(): array
{
return $this->features;
}
/** isset, not array_key_exists: a global that is null cannot be used by the adapter either. */
public function isSet(): bool
{
return isset($GLOBALS[$this->name]);
}
}
@@ -2,7 +2,7 @@
declare(strict_types=1);
namespace LIT\MCP\Integrations\Divi\Release;
namespace LIT\MCP\Compatibility;
defined('ABSPATH') || exit;
@@ -2,40 +2,36 @@
declare(strict_types=1);
namespace LIT\MCP\Integrations\Divi\Release;
namespace LIT\MCP\Compatibility;
defined('ABSPATH') || exit;
/**
* The Divi dependencies of one adapter (spec §4.1) – the single source for DiviCompatibility, the release gate,
* the admin check and DiviBoundaryTest. Pure data, no WordPress calls.
* The external dependencies of one adapter (spec §4.1) – the single source for the compatibility checks, the release
* gate, the admin check and DiviBoundaryTest. Pure data, no WordPress calls. Only area and calls are needed; the
* other fields are optional (the Divi integration uses all of them).
*/
final class Manifest
{
public const PAGES = 'Seiten';
public const LIBRARY = 'Bibliothek';
public const THEME_BUILDER = 'Theme Builder';
public const DESIGN = 'Designsystem';
public const CATALOG = 'Katalog';
/** @var list<ConstantRequirement> */
public readonly array $constants;
/**
* @param string $area One of the constants above (the five areas of spec §3).
* @param list<Call> $calls Divi functions and static methods the adapter calls.
* @param list<string|ConstantRequirement> $constants Divi constants it reads; a plain name stands for a
* @param string $area Area label for reports (the Divi integration: DiviAreas).
* @param list<Call> $calls Functions and static methods the adapter calls.
* @param list<string|ConstantRequirement> $constants Constants it reads; a plain name stands for a
* ConstantRequirement without features of its own.
* @param list<string> $postTypes Divi post types it needs registered.
* @param list<string> $taxonomies Divi taxonomies it needs registered.
* @param list<string> $blockTypes Divi block types it needs (registered or in Divi's
* module metadata).
* @param list<string> $keys Other Divi names it uses as strings: meta keys, options,
* @param list<string> $postTypes Post types it needs registered.
* @param list<string> $taxonomies Taxonomies it needs registered.
* @param list<string> $blockTypes Block types it needs (registered or known
* to the ManifestCheck knowsBlock closure).
* @param list<string> $keys Other names it uses as strings: meta keys, options,
* internal post types that need no registration check,
* query arguments.
* @param list<Mirror> $mirrors Divi functions whose storage logic it repeats.
* @param list<DiviFeature> $features The features of every call, mirror and constant that names
* @param list<Mirror> $mirrors Functions whose storage logic it repeats.
* @param list<Capability> $features The features of every call, mirror and constant that names
* none itself (featuresOf()).
* @param list<GlobalRequirement> $globals Global variables it reads (isset check).
*/
public function __construct(
public readonly string $area,
@@ -46,7 +42,8 @@ final class Manifest
public readonly array $blockTypes = [],
public readonly array $keys = [],
public readonly array $mirrors = [],
public readonly array $features = []
public readonly array $features = [],
public readonly array $globals = []
) {
$this->constants = array_map(
static fn (string|ConstantRequirement $constant): ConstantRequirement => is_string($constant)
@@ -56,13 +53,13 @@ final class Manifest
);
}
/** @return list<Requirement> Calls, constants and mirrors – everything the release gate checks as code. */
/** @return list<Requirement> Calls, constants, mirrors and globals – everything the release gate checks as code. */
public function requirements(): array
{
return [...$this->calls, ...$this->constants, ...$this->mirrors];
return [...$this->calls, ...$this->constants, ...$this->mirrors, ...$this->globals];
}
/** @return list<DiviFeature> The features $requirement names itself, else this manifest's default. */
/** @return list<Capability> The features $requirement names itself, else this manifest's default. */
public function featuresOf(Requirement $requirement): array
{
$own = $requirement->features();
@@ -71,8 +68,8 @@ final class Manifest
}
/**
* @return list<string> Every Divi name the adapter's code may contain (DiviBoundaryTest): called functions and
* classes, constants, post types, taxonomies, keys and the names of mirrored functions.
* @return list<string> Every external name the adapter's code may contain (DiviBoundaryTest): called functions and
* classes, constants, globals, post types, taxonomies, keys and the names of mirrored functions.
*/
public function names(): array
{
@@ -86,6 +83,7 @@ final class Manifest
return array_values(array_unique(array_merge(
$names,
$constants,
array_map(static fn (GlobalRequirement $global): string => $global->name, $this->globals),
$this->postTypes,
$this->taxonomies,
$this->keys,
@@ -2,18 +2,16 @@
declare(strict_types=1);
namespace LIT\MCP\Integrations\Divi\Release;
namespace LIT\MCP\Compatibility;
use Closure;
use LIT\MCP\Integrations\Divi\Schema\DiviModules;
use ReflectionFunction;
use ReflectionMethod;
use LogicException;
use Throwable;
defined('ABSPATH') || exit;
/**
* The checks of all adapter manifests (spec §4.3), as label => check for reports, and as problems with their Divi
* The checks of all adapter manifests (spec §4.3), as label => check for reports, and as problems with their
* features for the release matrix (problems(), E1). Code checks (calls with the argument count the adapter passes
* and their signature, constants) and mirror checks decide the release gate; registration checks (post types,
* taxonomies, block types) depend on the request context (stage3 finding 6) and only show in reports.
@@ -22,32 +20,30 @@ defined('ABSPATH') || exit;
*/
class ManifestCheck
{
/** @var array<string, Manifest> */
private array $manifests;
/** @var Closure(Mirror): ?string */
private Closure $current;
/** @var Closure(string): bool */
private Closure $knowsBlock;
/**
* @param array<string, Manifest>|null $manifests null = DiviAdapters::manifests().
* @param array<string, string>|null $expected Mirror ID or "sig:<symbol>" => sha256;
* null = DiviReference::expected().
* @param array<string, Manifest> $manifests Area key => manifest.
* @param array<string, string>|null $expected Mirror ID or "sig:<symbol>" => sha256; null = none known.
* @param (Closure(Mirror): ?string)|null $current Hash of the installed mirror, null = unreadable;
* null = SourceFingerprint in the Divi theme directory.
* @param (Closure(string): bool)|null $knowsBlock null = DiviModules::knowsBlock().
* no closure = every mirror is unreadable (a problem).
* @param (Closure(string): bool)|null $knowsBlock Whether a block type is known; required when a manifest
* has block types, which would otherwise pass unchecked.
* @param bool $signatures false = no signature checks: neither reference-missing
* nor signature problems (calls are still checked for
* existence and argument count).
* @throws LogicException When a manifest has block types but $knowsBlock is null.
*/
public function __construct(
?array $manifests = null,
private array $manifests,
private ?array $expected = null,
?Closure $current = null,
?Closure $knowsBlock = null
private ?Closure $current = null,
private ?Closure $knowsBlock = null,
private bool $signatures = true
) {
$this->manifests = $manifests ?? DiviAdapters::manifests();
$this->current = $current ?? self::installed(...);
$this->knowsBlock = $knowsBlock ?? DiviModules::knowsBlock(...);
foreach ($manifests as $manifest) {
if ($manifest->blockTypes !== [] && $knowsBlock === null) {
throw new LogicException('block types need a knowsBlock closure, else they pass unchecked');
}
}
}
/** @return array<string, Closure(): bool> */
@@ -62,6 +58,9 @@ class ManifestCheck
$checks[$manifest->area . ': ' . $constant->label()] =
static fn (): bool => defined($constant->name);
}
foreach ($manifest->globals as $global) {
$checks[$manifest->area . ': ' . $global->label()] = static fn (): bool => $global->isSet();
}
}
return $checks;
@@ -92,7 +91,7 @@ class ManifestCheck
$checks[$manifest->area . ': Taxonomie ' . $taxonomy] =
static fn (): bool => taxonomy_exists($taxonomy);
}
foreach ($manifest->blockTypes as $block) {
foreach ($this->knowsBlock === null ? [] : $manifest->blockTypes as $block) {
$checks[$manifest->area . ': Block-Typ ' . $block] = fn (): bool => ($this->knowsBlock)($block);
}
}
@@ -123,8 +122,8 @@ class ManifestCheck
}
/**
* Calls (missing or other argument count: call; no reference: reference-missing; other signature: signature)
* and constants. Reflection only – cheap enough for every request.
* Calls (missing or other argument count: call; no reference: reference-missing; other signature: signature;
* the last two only with signatures), constants and globals. Reflection only – cheap enough for every request.
*
* @return list<Problem>
*/
@@ -147,6 +146,15 @@ class ManifestCheck
);
}
}
foreach ($manifest->globals as $global) {
if (! $global->isSet()) {
$problems[] = new Problem(
$manifest->area . ': ' . $global->label(),
$manifest->featuresOf($global),
Problem::GLOBAL
);
}
}
}
return $problems;
@@ -154,8 +162,8 @@ class ManifestCheck
/**
* Mirrors that differ from their reference, cannot be read or have no reference – kind mirror in every case:
* a mirror repeats how Divi writes, so it never runs unverified, whatever the release. Reads Divi files; the
* resolver caches the result.
* a mirror repeats how the third-party code writes, so it never runs unverified, whatever the release. Reads
* files; the resolver caches the result.
*
* @return list<Problem>
*/
@@ -180,22 +188,7 @@ class ManifestCheck
/** The function or method exists and takes the number of arguments the adapter passes. */
public static function accepts(Call $call): bool
{
$class = $call->className();
if ($class === null) {
if (! function_exists($call->member())) {
return false;
}
$reflection = new ReflectionFunction($call->member());
} else {
if (! method_exists($class, $call->member())) {
return false;
}
$reflection = new ReflectionMethod($class, $call->member());
}
return $call->args === null
|| ($reflection->getNumberOfRequiredParameters() <= $call->args
&& ($reflection->isVariadic() || $reflection->getNumberOfParameters() >= $call->args));
return $call->accepts();
}
private function callProblem(Manifest $manifest, Call $call): ?Problem
@@ -206,7 +199,11 @@ class ManifestCheck
return new Problem($label, $features, Problem::CALL);
}
$expected = $this->expected()[DiviReference::signatureKey($call)] ?? null;
if (! $this->signatures) {
return null;
}
$expected = ($this->expected ?? [])[$call->signatureKey()] ?? null;
if ($expected === null) {
return new Problem($label . ' – Referenz der Signatur fehlt', $features, Problem::REFERENCE_MISSING);
}
@@ -219,15 +216,9 @@ class ManifestCheck
/** The mirror's reference exists and the installed source has the same fingerprint. */
private function matches(Mirror $mirror): bool
{
$expected = $this->expected()[$mirror->id] ?? null;
$expected = ($this->expected ?? [])[$mirror->id] ?? null;
return $expected !== null && ($this->current)($mirror) === $expected;
}
/** @return array<string, string> */
private function expected(): array
{
return $this->expected ??= DiviReference::expected();
return $expected !== null && $this->current !== null && ($this->current)($mirror) === $expected;
}
private static function mirrorLabel(Manifest $manifest, Mirror $mirror): string
@@ -244,13 +235,4 @@ class ManifestCheck
return false;
}
}
private static function installed(Mirror $mirror): ?string
{
$dir = DiviEnvironment::themeDir();
return $dir === null
? null
: SourceFingerprint::of($dir . '/' . $mirror->file, $mirror->function, $mirror->class)->hash;
}
}
+48
View File
@@ -0,0 +1,48 @@
<?php
declare(strict_types=1);
namespace LIT\MCP\Compatibility;
defined('ABSPATH') || exit;
/**
* A function of a third-party plugin or theme whose logic an adapter repeats instead of calling it (spec §4.1
* "mirrors"). Its fingerprint (SourceFingerprint) must match the one in fingerprints.json, else the third-party code
* changed what we copy.
*/
final class Mirror implements Requirement
{
/**
* @param string $id Key in fingerprints.json and name of its reference source (the Divi integration
* keeps them in tests/Fixtures/divi/reference/<id>.txt – an example, not a rule).
* @param string $file File of the third-party code, relative to its plugin or theme directory.
* @param string $function Function or method name.
* @param string|null $class Class of a method – the short name is enough (the file fixes where it
* lives, and a short name is no external name for DiviBoundaryTest); null for a
* function.
* @param string $what What the plugin repeats (German, for reports).
* @param list<Capability> $features [] = the default of the manifest.
*/
public function __construct(
public readonly string $id,
public readonly string $file,
public readonly string $function,
public readonly ?string $class,
public readonly string $what,
private readonly array $features = []
) {
}
public function features(): array
{
return $this->features;
}
public function label(): string
{
$class = $this->class === null ? '' : SourceFingerprint::shortName($this->class) . '::';
return $class . $this->function . '()';
}
}
@@ -2,15 +2,17 @@
declare(strict_types=1);
namespace LIT\MCP\Integrations\Divi\Release;
namespace LIT\MCP\Compatibility;
use Closure;
defined('ABSPATH') || exit;
/**
* One failed manifest check and the Divi features it concerns (Manifest::featuresOf()). The kind decides how the
* resolver weighs it (E1): call, mirror and constant lock their features for every Divi version, released or not;
* signature and reference-missing only matter for a version without release (automatic release). A problem without
* features concerns every feature (fail closed).
* One failed manifest check and the capabilities it concerns (Manifest::featuresOf()). The kind decides how the
* resolver weighs it (E1): call, mirror, constant and global lock their features for every version, released
* or not; signature and reference-missing only matter for a version without release (automatic release). A problem
* without features concerns every feature (fail closed).
*/
final class Problem
{
@@ -18,6 +20,7 @@ final class Problem
public const SIGNATURE = 'signature';
public const MIRROR = 'mirror';
public const CONSTANT = 'constant';
public const GLOBAL = 'global';
public const REFERENCE_MISSING = 'reference-missing';
public const RESOLVER = 'resolver';
@@ -26,15 +29,16 @@ final class Problem
self::SIGNATURE,
self::MIRROR,
self::CONSTANT,
self::GLOBAL,
self::REFERENCE_MISSING,
self::RESOLVER,
];
private const HARD = [self::CALL, self::MIRROR, self::CONSTANT, self::RESOLVER];
private const HARD = [self::CALL, self::MIRROR, self::CONSTANT, self::GLOBAL, self::RESOLVER];
/**
* @param string $label German, e.g. "Seiten: et_x()" (the labels of ManifestCheck).
* @param list<DiviFeature> $features
* @param string $kind One of the constants above.
* @param string $label German, e.g. "Seiten: et_x()" (the labels of ManifestCheck).
* @param list<Capability> $features
* @param string $kind One of the constants above.
*/
public function __construct(
public readonly string $label,
@@ -43,13 +47,13 @@ final class Problem
) {
}
/** Locks its features whatever the release (the code no longer fits Divi or a mirror differs). */
/** Locks its features whatever the release (the third-party code no longer fits or a mirror differs). */
public function isHard(): bool
{
return in_array($this->kind, self::HARD, true);
}
public function concerns(DiviFeature $feature): bool
public function concerns(Capability $feature): bool
{
return $this->features === [] || in_array($feature, $this->features, true);
}
@@ -60,12 +64,16 @@ final class Problem
return [
'label' => $this->label,
'kind' => $this->kind,
'features' => array_map(static fn (DiviFeature $feature): string => $feature->value, $this->features),
'features' => array_map(static fn (Capability $feature): string => $feature->id(), $this->features),
];
}
/** The problem of toArray(), or null for anything else (an unknown kind or feature included). */
public static function fromArray(mixed $row): ?self
/**
* The problem of toArray(), or null for anything else (an unknown kind or feature included).
*
* @param Closure(string): ?Capability $capability Finds a capability by its ID, null for an unknown one.
*/
public static function fromArray(mixed $row, Closure $capability): ?self
{
if (
! is_array($row)
@@ -78,7 +86,7 @@ final class Problem
$features = [];
foreach ($row['features'] as $value) {
$feature = is_string($value) ? DiviFeature::tryFrom($value) : null;
$feature = is_string($value) ? $capability($value) : null;
if ($feature === null) {
return null;
}
@@ -2,7 +2,7 @@
declare(strict_types=1);
namespace LIT\MCP\Integrations\Divi\Release;
namespace LIT\MCP\Compatibility;
defined('ABSPATH') || exit;
@@ -2,11 +2,14 @@
declare(strict_types=1);
namespace LIT\MCP\Integrations\Divi\Release;
namespace LIT\MCP\Compatibility;
defined('ABSPATH') || exit;
/** One thing an adapter needs from Divi (a call, a mirrored function or a constant) and the features it serves. */
/**
* One thing an adapter needs from a third-party plugin or theme (a call, a mirrored function or a constant) and the
* features it serves.
*/
interface Requirement
{
/** Name for reports, e.g. "GlobalPreset::get_data()" (without the manifest area). */
@@ -16,7 +19,7 @@ interface Requirement
* The features this requirement serves itself; [] = the default of the manifest that lists it
* (Manifest::featuresOf() resolves that).
*
* @return list<DiviFeature>
* @return list<Capability>
*/
public function features(): array;
}
@@ -2,7 +2,7 @@
declare(strict_types=1);
namespace LIT\MCP\Integrations\Divi\Release;
namespace LIT\MCP\Compatibility;
use ReflectionException;
use ReflectionFunction;
@@ -14,14 +14,15 @@ use ReflectionType;
defined('ABSPATH') || exit;
/**
* Fingerprint of the signature of one Divi function or static method (spec §4.2, E1 of the plan "Divi-Fähigkeiten"),
* read with Reflection from the running PHP process – so it needs the symbol to be loaded (in WordPress Divi is) and
* never includes a file. The class of a method is autoloaded the way ManifestCheck::accepts() already does it.
* Fingerprint of the signature of one function or static method of a third-party plugin or theme (spec §4.2, E1 of
* the plan "Divi-Fähigkeiten"), read with Reflection from the running PHP process – so it needs the symbol to be
* loaded (in WordPress the plugin or theme is, once active) and never includes a file. The class of a method is
* autoloaded the way ManifestCheck::accepts() already does it.
*
* What the hash covers: function or method; for a method its visibility and whether it is static; per parameter the
* type (nullable and union included; no type reads as "mixed"), by reference, variadic and whether it is optional
* (has a default); the return type (none reads as "mixed"). What it leaves out:
* - Parameter names: the plugin passes every argument to Divi positionally (PHP 8 named arguments are never used),
* - Parameter names: the plugin passes every argument positionally (PHP 8 named arguments are never used),
* so a rename cannot break a call, and a harmless rename must not lock a feature.
* - Default values: they are expressions (constants, arrays) that Reflection may be unable to evaluate in some
* requests and that change spelling without changing meaning; only "has a default" matters for the number of
@@ -2,18 +2,18 @@
declare(strict_types=1);
namespace LIT\MCP\Integrations\Divi\Release;
namespace LIT\MCP\Compatibility;
use RuntimeException;
defined('ABSPATH') || exit;
/**
* Fingerprint of one Divi function or method (spec §4.2), read with token_get_all(): the file is never included,
* so functions Divi loads only in some requests (theme-builder/api.php, admin-ajax only) are readable in every
* request. The hash covers the tokens from "function" to the closing brace without comments and whitespace –
* new comments or reformatting keep it, any code change changes it. $source is the verbatim text of those
* tokens for the human diff (wp lit-mcp divi-check --diff).
* Fingerprint of one function or method of a third-party plugin or theme (spec §4.2), read with token_get_all(): the
* file is never included, so functions loaded only in some requests (e.g. Divi's theme-builder/api.php, admin-ajax
* only) are readable in every request. The hash covers the tokens from "function" to the closing brace without
* comments and whitespace – new comments or reformatting keep it, any code change changes it. $source is the verbatim
* text of those tokens for the human diff (wp lit-mcp divi-check --diff).
*/
final class SourceFingerprint
{
+2 -2
View File
@@ -9,15 +9,15 @@ use LIT\MCP\Admin\AdminPostAction;
use LIT\MCP\Admin\AdminTab;
use LIT\MCP\Admin\SettingsPage;
use LIT\MCP\ChangeLog\ChangeLog;
use LIT\MCP\Compatibility\ReleaseState;
use LIT\MCP\Integrations\Divi\Release\CheckReport;
use LIT\MCP\Integrations\Divi\Release\CheckReportStore;
use LIT\MCP\Integrations\Divi\Release\DiviCheckRoute;
use LIT\MCP\Integrations\Divi\Release\DiviEnvironment;
use LIT\MCP\Integrations\Divi\Release\FeatureOverview;
use LIT\MCP\Integrations\Divi\Release\FeatureStatus;
use LIT\MCP\Compatibility\FeatureStatus;
use LIT\MCP\Integrations\Divi\Release\ReleaseGate;
use LIT\MCP\Integrations\Divi\Release\ReleaseResolver;
use LIT\MCP\Integrations\Divi\Release\ReleaseState;
use LIT\MCP\Integrations\Divi\Release\ReleaseStatus;
use LIT\MCP\Integrations\Divi\Release\ReleaseStore;
use LIT\MCP\Integrations\Divi\Release\WriteProbe;
@@ -5,17 +5,17 @@ declare(strict_types=1);
namespace LIT\MCP\Integrations\Divi\Cli;
use Closure;
use LIT\MCP\Compatibility\LineDiff;
use LIT\MCP\Compatibility\SourceFingerprint;
use LIT\MCP\Integrations\Divi\Release\CheckReport;
use LIT\MCP\Integrations\Divi\Release\CheckReportStore;
use LIT\MCP\Integrations\Divi\Release\CheckStep;
use LIT\MCP\Integrations\Divi\Release\CompatibilityCheck;
use LIT\MCP\Integrations\Divi\Release\CompatibilityMatrix;
use LIT\MCP\Compatibility\CompatibilityMatrix;
use LIT\MCP\Integrations\Divi\Release\DiviAdapters;
use LIT\MCP\Integrations\Divi\Release\DiviEnvironment;
use LIT\MCP\Integrations\Divi\Release\DiviReference;
use LIT\MCP\Integrations\Divi\Release\LineDiff;
use LIT\MCP\Integrations\Divi\Release\ReleaseGate;
use LIT\MCP\Integrations\Divi\Release\SourceFingerprint;
use RuntimeException;
defined('ABSPATH') || exit;
@@ -6,10 +6,11 @@ namespace LIT\MCP\Integrations\Divi\Design;
use ET\Builder\Packages\GlobalData\GlobalData;
use ET\Builder\Packages\GlobalData\GlobalPreset;
use LIT\MCP\Integrations\Divi\Release\Call;
use LIT\MCP\Compatibility\Call;
use LIT\MCP\Integrations\Divi\Release\DiviAdapter;
use LIT\MCP\Integrations\Divi\Release\DiviAreas;
use LIT\MCP\Integrations\Divi\Release\DiviFeature;
use LIT\MCP\Integrations\Divi\Release\Manifest;
use LIT\MCP\Compatibility\Manifest;
defined('ABSPATH') || exit;
@@ -29,7 +30,7 @@ class DesignSystemReader implements DiviAdapter
public static function manifest(): Manifest
{
return new Manifest(
Manifest::DESIGN,
DiviAreas::DESIGN,
calls: [
new Call(GlobalData::class . '::get_global_colors', 0),
new Call(GlobalData::class . '::get_global_variables', 0),
@@ -8,12 +8,13 @@ use ET\Builder\Framework\UserRole\UserRole;
use ET\Builder\Packages\Conversion\Conversion;
use ET\Builder\Packages\GlobalData\GlobalPreset;
use ET\Builder\Packages\ModuleLibrary\ModuleRegistration;
use LIT\MCP\Integrations\Divi\Release\Call;
use LIT\MCP\Compatibility\Call;
use LIT\MCP\Integrations\Divi\Release\DiviAdapter;
use LIT\MCP\Integrations\Divi\Release\DiviAreas;
use LIT\MCP\Integrations\Divi\Release\DiviFeature;
use LIT\MCP\Integrations\Divi\Release\DiviLocked;
use LIT\MCP\Integrations\Divi\Release\Manifest;
use LIT\MCP\Integrations\Divi\Release\Mirror;
use LIT\MCP\Compatibility\Manifest;
use LIT\MCP\Compatibility\Mirror;
use LIT\MCP\Integrations\Divi\Release\ReleaseGate;
use RuntimeException;
use WP_Error;
@@ -43,7 +44,7 @@ class DesignSystemWriter implements DiviAdapter
public static function manifest(): Manifest
{
return new Manifest(
Manifest::DESIGN,
DiviAreas::DESIGN,
calls: [
// Read by the preset tools as well as by the design system tool.
new Call(GlobalPreset::class . '::get_data', 0, [DiviFeature::DesignRead, DiviFeature::PresetWrite]),
+4 -3
View File
@@ -4,10 +4,11 @@ declare(strict_types=1);
namespace LIT\MCP\Integrations\Divi\Design;
use LIT\MCP\Integrations\Divi\Release\Call;
use LIT\MCP\Compatibility\Call;
use LIT\MCP\Integrations\Divi\Release\DiviAdapter;
use LIT\MCP\Integrations\Divi\Release\DiviAreas;
use LIT\MCP\Integrations\Divi\Release\DiviFeature;
use LIT\MCP\Integrations\Divi\Release\Manifest;
use LIT\MCP\Compatibility\Manifest;
defined('ABSPATH') || exit;
@@ -26,7 +27,7 @@ class FontCatalog implements DiviAdapter
public static function manifest(): Manifest
{
return new Manifest(
Manifest::DESIGN,
DiviAreas::DESIGN,
calls: [
new Call('et_builder_get_fonts', 0),
new Call('et_builder_get_custom_fonts', 0),
@@ -6,12 +6,13 @@ namespace LIT\MCP\Integrations\Divi\Design;
use ET\Builder\VisualBuilder\Fonts\FontsUtility;
use ET\Builder\VisualBuilder\Hooks\HooksRegistration;
use LIT\MCP\Integrations\Divi\Release\Call;
use LIT\MCP\Compatibility\Call;
use LIT\MCP\Integrations\Divi\Release\DiviAdapter;
use LIT\MCP\Integrations\Divi\Release\DiviAreas;
use LIT\MCP\Integrations\Divi\Release\DiviFeature;
use LIT\MCP\Integrations\Divi\Release\DiviLocked;
use LIT\MCP\Integrations\Divi\Release\Manifest;
use LIT\MCP\Integrations\Divi\Release\Mirror;
use LIT\MCP\Compatibility\Manifest;
use LIT\MCP\Compatibility\Mirror;
use LIT\MCP\Integrations\Divi\Release\ReleaseGate;
use RuntimeException;
@@ -35,7 +36,7 @@ class FontInstaller implements DiviAdapter
public static function manifest(): Manifest
{
return new Manifest(
Manifest::DESIGN,
DiviAreas::DESIGN,
calls: [
new Call(FontsUtility::class . '::mime_types_font', 0),
new Call(HooksRegistration::class . '::check_filetype_and_ext_font', 3),
@@ -5,11 +5,12 @@ declare(strict_types=1);
namespace LIT\MCP\Integrations\Divi\Design;
use ET\Builder\Packages\GlobalData\GlobalData;
use LIT\MCP\Integrations\Divi\Release\Call;
use LIT\MCP\Compatibility\Call;
use LIT\MCP\Integrations\Divi\Release\DiviAdapter;
use LIT\MCP\Integrations\Divi\Release\DiviAreas;
use LIT\MCP\Integrations\Divi\Release\DiviFeature;
use LIT\MCP\Integrations\Divi\Release\DiviLocked;
use LIT\MCP\Integrations\Divi\Release\Manifest;
use LIT\MCP\Compatibility\Manifest;
use LIT\MCP\Integrations\Divi\Release\ReleaseGate;
use RuntimeException;
@@ -39,7 +40,7 @@ class GlobalColorsAdapter implements DiviAdapter
public static function manifest(): Manifest
{
return new Manifest(
Manifest::DESIGN,
DiviAreas::DESIGN,
calls: [
new Call(GlobalData::class . '::get_global_colors', 0),
new Call(GlobalData::class . '::sanitize_global_colors_data', 1),
@@ -5,11 +5,12 @@ declare(strict_types=1);
namespace LIT\MCP\Integrations\Divi\Design;
use ET\Builder\Packages\GlobalData\GlobalData;
use LIT\MCP\Integrations\Divi\Release\Call;
use LIT\MCP\Compatibility\Call;
use LIT\MCP\Integrations\Divi\Release\DiviAdapter;
use LIT\MCP\Integrations\Divi\Release\DiviAreas;
use LIT\MCP\Integrations\Divi\Release\DiviFeature;
use LIT\MCP\Integrations\Divi\Release\DiviLocked;
use LIT\MCP\Integrations\Divi\Release\Manifest;
use LIT\MCP\Compatibility\Manifest;
use LIT\MCP\Integrations\Divi\Release\ReleaseGate;
use RuntimeException;
use WP_Error;
@@ -28,7 +29,7 @@ class GlobalVariablesAdapter implements DiviAdapter
public static function manifest(): Manifest
{
return new Manifest(
Manifest::DESIGN,
DiviAreas::DESIGN,
calls: [
new Call(GlobalData::class . '::get_global_variables', 0),
new Call(GlobalData::class . '::set_global_variables', 2),
@@ -5,12 +5,13 @@ declare(strict_types=1);
namespace LIT\MCP\Integrations\Divi\Design;
use InvalidArgumentException;
use LIT\MCP\Integrations\Divi\Release\Call;
use LIT\MCP\Compatibility\Call;
use LIT\MCP\Integrations\Divi\Release\DiviAdapter;
use LIT\MCP\Integrations\Divi\Release\DiviAreas;
use LIT\MCP\Integrations\Divi\Release\DiviFeature;
use LIT\MCP\Integrations\Divi\Release\DiviLocked;
use LIT\MCP\Integrations\Divi\Release\Manifest;
use LIT\MCP\Integrations\Divi\Release\Mirror;
use LIT\MCP\Compatibility\Manifest;
use LIT\MCP\Compatibility\Mirror;
use LIT\MCP\Integrations\Divi\Release\ReleaseGate;
use RuntimeException;
@@ -61,7 +62,7 @@ class ThemeOptionsAdapter implements DiviAdapter
public static function manifest(): Manifest
{
return new Manifest(
Manifest::DESIGN,
DiviAreas::DESIGN,
calls: [
new Call('et_sanitize_font_choices', 1),
new Call('et_sanitize_global_font_weight', 1),
+3 -2
View File
@@ -4,7 +4,8 @@ declare(strict_types=1);
namespace LIT\MCP\Integrations\Divi;
use LIT\MCP\Integrations\Divi\Release\ManifestCheck;
use LIT\MCP\Compatibility\ManifestCheck;
use LIT\MCP\Integrations\Divi\Release\DiviManifestCheck;
defined('ABSPATH') || exit;
@@ -29,7 +30,7 @@ final class DiviCompatibility
/** @return array<string, callable(): bool> */
public static function defaults(?ManifestCheck $check = null): array
{
$check ??= new ManifestCheck();
$check ??= DiviManifestCheck::create();
return array_merge($check->codeChecks(), $check->registrationChecks(), $check->mirrorChecks());
}
@@ -5,12 +5,13 @@ declare(strict_types=1);
namespace LIT\MCP\Integrations\Divi\Library;
use LIT\MCP\Abilities\WriteFailed;
use LIT\MCP\Integrations\Divi\Release\Call;
use LIT\MCP\Compatibility\Call;
use LIT\MCP\Integrations\Divi\Release\DiviAdapter;
use LIT\MCP\Integrations\Divi\Release\DiviAreas;
use LIT\MCP\Integrations\Divi\Release\DiviFeature;
use LIT\MCP\Integrations\Divi\Release\DiviLocked;
use LIT\MCP\Integrations\Divi\Release\Manifest;
use LIT\MCP\Integrations\Divi\Release\Mirror;
use LIT\MCP\Compatibility\Manifest;
use LIT\MCP\Compatibility\Mirror;
use LIT\MCP\Integrations\Divi\Release\ReleaseGate;
use LIT\MCP\Integrations\WordPress\Content\NewDraft;
use RuntimeException;
@@ -55,7 +56,7 @@ class DiviLibraryStore implements DiviAdapter
public static function manifest(): Manifest
{
return new Manifest(
Manifest::LIBRARY,
DiviAreas::LIBRARY,
calls: [
// isGlobal() serves listing and describing as well as the global edits.
new Call(
+4 -3
View File
@@ -6,10 +6,11 @@ namespace LIT\MCP\Integrations\Divi\Library;
use Closure;
use LIT\MCP\Abilities\Errors;
use LIT\MCP\Integrations\Divi\Release\Call;
use LIT\MCP\Compatibility\Call;
use LIT\MCP\Integrations\Divi\Release\DiviAdapter;
use LIT\MCP\Integrations\Divi\Release\DiviAreas;
use LIT\MCP\Integrations\Divi\Release\DiviFeature;
use LIT\MCP\Integrations\Divi\Release\Manifest;
use LIT\MCP\Compatibility\Manifest;
use WP_Error;
defined('ABSPATH') || exit;
@@ -35,7 +36,7 @@ class DiviRoles implements DiviAdapter
public static function manifest(): Manifest
{
return new Manifest(
Manifest::LIBRARY,
DiviAreas::LIBRARY,
calls: [new Call('et_pb_is_allowed', 1)],
features: [DiviFeature::Roles]
);
+5 -4
View File
@@ -4,12 +4,13 @@ declare(strict_types=1);
namespace LIT\MCP\Integrations\Divi\Pages;
use LIT\MCP\Integrations\Divi\Release\Call;
use LIT\MCP\Compatibility\Call;
use LIT\MCP\Integrations\Divi\Release\DiviAdapter;
use LIT\MCP\Integrations\Divi\Release\DiviAreas;
use LIT\MCP\Integrations\Divi\Release\DiviFeature;
use LIT\MCP\Integrations\Divi\Release\DiviLocked;
use LIT\MCP\Integrations\Divi\Release\Manifest;
use LIT\MCP\Integrations\Divi\Release\Mirror;
use LIT\MCP\Compatibility\Manifest;
use LIT\MCP\Compatibility\Mirror;
use LIT\MCP\Integrations\Divi\Release\ReleaseGate;
use RuntimeException;
use WP_Post;
@@ -35,7 +36,7 @@ final class DiviPages implements DiviAdapter
public static function manifest(): Manifest
{
return new Manifest(
Manifest::PAGES,
DiviAreas::PAGES,
calls: [
// Also called by DiviThemeBuilder::clearSiteCaches() – a break locks both features.
new Call(
@@ -8,10 +8,11 @@ use Closure;
use LIT\MCP\Integrations\Divi\Compose\Placeholders;
use LIT\MCP\Integrations\Divi\Compose\DynamicContentRules;
use LIT\MCP\Integrations\Divi\Compose\LoopScope;
use LIT\MCP\Integrations\Divi\Release\Call;
use LIT\MCP\Compatibility\Call;
use LIT\MCP\Integrations\Divi\Release\DiviAdapter;
use LIT\MCP\Integrations\Divi\Release\DiviAreas;
use LIT\MCP\Integrations\Divi\Release\DiviFeature;
use LIT\MCP\Integrations\Divi\Release\Manifest;
use LIT\MCP\Compatibility\Manifest;
use LIT\MCP\Log;
use Throwable;
@@ -56,7 +57,7 @@ class DynamicContentMap implements DiviAdapter
public static function manifest(): Manifest
{
return new Manifest(
Manifest::CATALOG,
DiviAreas::CATALOG,
calls: [new Call(self::REGISTRY . '::get_options', 2)],
features: [DiviFeature::DynamicSources]
);
-47
View File
@@ -1,47 +0,0 @@
<?php
declare(strict_types=1);
namespace LIT\MCP\Integrations\Divi\Release;
defined('ABSPATH') || exit;
/**
* One Divi function or static method an adapter calls (spec §4.1), with the number of arguments the plugin passes.
* $args null: only its existence matters (a function Divi itself needs, e.g. et_() inside store_template).
*/
final class Call implements Requirement
{
/**
* @param string $symbol "function_name" or "Fully\Qualified\Class::method".
* @param list<DiviFeature> $features [] = the default of the manifest.
*/
public function __construct(
public readonly string $symbol,
public readonly ?int $args = null,
private readonly array $features = []
) {
}
public function className(): ?string
{
return str_contains($this->symbol, '::') ? ltrim(explode('::', $this->symbol, 2)[0], '\\') : null;
}
public function member(): string
{
return str_contains($this->symbol, '::') ? explode('::', $this->symbol, 2)[1] : $this->symbol;
}
public function features(): array
{
return $this->features;
}
public function label(): string
{
$class = $this->className();
return ($class === null ? '' : SourceFingerprint::shortName($class) . '::') . $this->member() . '()';
}
}
@@ -5,6 +5,7 @@ declare(strict_types=1);
namespace LIT\MCP\Integrations\Divi\Release;
use Closure;
use LIT\MCP\Compatibility\ManifestCheck;
use LIT\MCP\Integrations\Divi\Schema\DiviModules;
use LIT\MCP\Integrations\Divi\Schema\ShortformCheck;
use LIT\MCP\Integrations\WordPress\Content\ContentComposer;
@@ -40,7 +41,7 @@ class CompatibilityCheck
public static function create(ContentComposer $composer): self
{
return new self(
new ManifestCheck(),
DiviManifestCheck::create(),
static fn (): array => DiviModules::isAvailable()
? ShortformCheck::problems((new DiviModules())->catalog())
: ['Divi 5 ist in dieser Anfrage nicht geladen.'],
@@ -1,148 +0,0 @@
<?php
declare(strict_types=1);
namespace LIT\MCP\Integrations\Divi\Release;
defined('ABSPATH') || exit;
/**
* The release state of every Divi feature (E1). A feature the constructor did not get is incompatible (fail
* closed), so of() always answers. The overall state of the matrix is its worst feature state.
*/
final class CompatibilityMatrix
{
public const MISSING = 'Für diese Fähigkeit liegt kein Prüfergebnis vor.';
/** @var array<string, FeatureStatus> keyed by DiviFeature value, in DiviFeature::cases() order */
private array $features = [];
/** @param array<string, FeatureStatus> $features keyed by DiviFeature value */
public function __construct(array $features)
{
foreach (DiviFeature::cases() as $feature) {
$status = $features[$feature->value] ?? null;
$this->features[$feature->value] = $status instanceof FeatureStatus && $status->feature === $feature
? $status
: new FeatureStatus($feature, ReleaseState::Incompatible, [self::MISSING]);
}
}
public static function allReleased(string $source = ReleaseStatus::SOURCE_CODE): self
{
return self::uniform(ReleaseState::Released, [], $source);
}
public static function allIncompatible(string $problem): self
{
return self::uniform(ReleaseState::Incompatible, [$problem]);
}
/**
* Every feature in $state – the matrix of a status built without one (ReleaseStatus' constructor).
*
* @param list<string> $problems
*/
public static function uniform(ReleaseState $state, array $problems = [], ?string $source = null): self
{
$released = $state === ReleaseState::Released;
$features = [];
foreach (DiviFeature::cases() as $feature) {
$features[$feature->value] = new FeatureStatus(
$feature,
$state,
$released ? [] : $problems,
$released ? ($source ?? ReleaseStatus::SOURCE_CODE) : null
);
}
return new self($features);
}
public function of(DiviFeature $feature): FeatureStatus
{
return $this->features[$feature->value];
}
/** @param list<DiviFeature> $features */
public function usable(array $features): bool
{
foreach ($features as $feature) {
if (! $this->of($feature)->usable()) {
return false;
}
}
return true;
}
/**
* The statuses of those of $features that are not usable – each once, in the order of $features. The one answer
* to "what locks this call" for the gate (DiviFeatureGate), the adapters (ReleaseGate::assertFeatures()) and the
* refusal (ReleaseStatus::refusalFor()).
*
* @param list<DiviFeature> $features
* @return list<FeatureStatus>
*/
public function locked(array $features): array
{
$locked = [];
foreach ($features as $feature) {
$status = $this->of($feature);
if (! $status->usable()) {
$locked[$feature->value] = $status;
}
}
return array_values($locked);
}
/** @return list<FeatureStatus> */
public function unusable(): array
{
return array_values(array_filter(
$this->features,
static fn (FeatureStatus $status): bool => ! $status->usable()
));
}
/** Incompatible before untested before released. */
public function state(): ReleaseState
{
$states = array_map(static fn (FeatureStatus $status): ReleaseState => $status->state, $this->features);
return match (true) {
in_array(ReleaseState::Incompatible, $states, true) => ReleaseState::Incompatible,
in_array(ReleaseState::Untested, $states, true) => ReleaseState::Untested,
default => ReleaseState::Released,
};
}
/** The common source of a fully released matrix; null when not all released or the sources differ. */
public function source(): ?string
{
$sources = array_unique(array_map(
static fn (FeatureStatus $status): string => (string) $status->source,
$this->features
));
return $this->state() === ReleaseState::Released && count($sources) === 1 ? (string) reset($sources) : null;
}
/** @return list<string> Every problem label once, in feature order. */
public function problems(): array
{
$problems = [];
foreach ($this->features as $status) {
$problems = array_merge($problems, $status->problems);
}
return array_values(array_unique($problems));
}
/** @return list<array{id: string, label: string, state: string, problems: list<string>, source: ?string}> */
public function toArray(): array
{
return array_values(array_map(static fn (FeatureStatus $status): array => $status->toArray(), $this->features));
}
}
@@ -4,6 +4,8 @@ declare(strict_types=1);
namespace LIT\MCP\Integrations\Divi\Release;
use LIT\MCP\Compatibility\Manifest;
defined('ABSPATH') || exit;
/** A class that may touch Divi (spec §3); it lists every Divi dependency in its manifest. */
@@ -4,6 +4,9 @@ declare(strict_types=1);
namespace LIT\MCP\Integrations\Divi\Release;
use LIT\MCP\Compatibility\Call;
use LIT\MCP\Compatibility\Manifest;
use LIT\MCP\Compatibility\Mirror;
use LIT\MCP\Integrations\Divi\Design\DesignSystemReader;
use LIT\MCP\Integrations\Divi\Design\DesignSystemWriter;
use LIT\MCP\Integrations\Divi\Design\FontCatalog;
@@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
namespace LIT\MCP\Integrations\Divi\Release;
defined('ABSPATH') || exit;
/** The five Divi areas of spec §3. Each value is the prefix of the labels in the admin check. */
final class DiviAreas
{
public const PAGES = 'Seiten';
public const LIBRARY = 'Bibliothek';
public const THEME_BUILDER = 'Theme Builder';
public const DESIGN = 'Designsystem';
public const CATALOG = 'Katalog';
}
@@ -8,6 +8,7 @@ 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;
defined('ABSPATH') || exit;
@@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
namespace LIT\MCP\Integrations\Divi\Release;
use LIT\MCP\Compatibility\ManifestCheck;
use LIT\MCP\Compatibility\Mirror;
use LIT\MCP\Compatibility\SourceFingerprint;
use LIT\MCP\Integrations\Divi\Schema\DiviModules;
defined('ABSPATH') || exit;
/** The ManifestCheck of Divi: its adapter manifests, signature references, theme sources and block registry. */
final class DiviManifestCheck
{
public static function create(): ManifestCheck
{
return new ManifestCheck(
DiviAdapters::manifests(),
DiviReference::expected(),
self::installed(...),
DiviModules::knowsBlock(...)
);
}
private static function installed(Mirror $mirror): ?string
{
$dir = DiviEnvironment::themeDir();
return $dir === null
? null
: SourceFingerprint::of($dir . '/' . $mirror->file, $mirror->function, $mirror->class)->hash;
}
}
@@ -4,6 +4,9 @@ declare(strict_types=1);
namespace LIT\MCP\Integrations\Divi\Release;
use LIT\MCP\Compatibility\Call;
use LIT\MCP\Compatibility\SignatureFingerprint;
use LIT\MCP\Compatibility\SourceFingerprint;
use RuntimeException;
defined('ABSPATH') || exit;
@@ -18,8 +21,7 @@ final class DiviReference
{
public const FILE = __DIR__ . '/fingerprints.json';
/** Key prefix of the signature fingerprints; no mirror ID starts with it. */
public const SIGNATURE_PREFIX = 'sig:';
public const SIGNATURE_PREFIX = Call::SIGNATURE_PREFIX;
/**
* @return array<string, string> Mirror ID or "sig:<symbol>" => sha256, sorted by key; [] when the file is
@@ -61,12 +63,6 @@ final class DiviReference
}
}
/** The key of $call's signature fingerprint: "sig:" and the symbol without a leading backslash. */
public static function signatureKey(Call $call): string
{
return self::SIGNATURE_PREFIX . ltrim($call->symbol, '\\');
}
/**
* Signature fingerprints of the calls in the running PHP process (Divi loaded).
*
@@ -82,7 +78,7 @@ final class DiviReference
if ($hash === null) {
throw new RuntimeException(sprintf('signature of %s: symbol is not loaded', $call->label()));
}
$signatures[self::signatureKey($call)] = $hash;
$signatures[$call->signatureKey()] = $hash;
}
ksort($signatures);
@@ -6,6 +6,7 @@ namespace LIT\MCP\Integrations\Divi\Release;
use Closure;
use LIT\MCP\Abilities\Ability;
use LIT\MCP\Compatibility\CompatibilityMatrix;
defined('ABSPATH') || exit;
-46
View File
@@ -1,46 +0,0 @@
<?php
declare(strict_types=1);
namespace LIT\MCP\Integrations\Divi\Release;
defined('ABSPATH') || exit;
/**
* A Divi function whose storage logic an adapter repeats instead of calling it (spec §4.1 "mirrors"). Its
* fingerprint (SourceFingerprint) must match the one in fingerprints.json, else Divi changed what we copy.
*/
final class Mirror implements Requirement
{
/**
* @param string $id Key in fingerprints.json and name of tests/Fixtures/divi/reference/<id>.txt.
* @param string $file Divi file, relative to the Divi theme directory.
* @param string $function Function or method name.
* @param string|null $class Class of a method – the short name is enough (the file fixes where it
* lives, and a short name is no Divi name for DiviBoundaryTest); null for a
* function.
* @param string $what What the plugin repeats (German, for reports).
* @param list<DiviFeature> $features [] = the default of the manifest.
*/
public function __construct(
public readonly string $id,
public readonly string $file,
public readonly string $function,
public readonly ?string $class,
public readonly string $what,
private readonly array $features = []
) {
}
public function features(): array
{
return $this->features;
}
public function label(): string
{
$class = $this->class === null ? '' : SourceFingerprint::shortName($this->class) . '::';
return $class . $this->function . '()';
}
}
+17 -5
View File
@@ -5,6 +5,9 @@ declare(strict_types=1);
namespace LIT\MCP\Integrations\Divi\Release;
use Closure;
use LIT\MCP\Compatibility\Capability;
use LIT\MCP\Compatibility\CompatibilityMatrix;
use LIT\MCP\Compatibility\FeatureStatus;
use LIT\MCP\Log;
use Throwable;
@@ -37,7 +40,10 @@ final class ReleaseGate
self::$status = (self::$resolver ?? static fn (): ReleaseStatus => (new ReleaseResolver())->resolve())();
} catch (Throwable $e) {
Log::failure('divi release status', $e);
self::$status = ReleaseStatus::of('', CompatibilityMatrix::allIncompatible(self::UNRESOLVED));
self::$status = ReleaseStatus::of(
'',
CompatibilityMatrix::allIncompatible(DiviFeature::cases(), self::UNRESOLVED)
);
}
return self::$status;
@@ -107,11 +113,17 @@ final class ReleaseGate
{
$status = self::status();
$locked = $status->matrix->locked($features);
if ($locked === []) {
return null;
}
return $locked === [] ? null : new DiviLocked(
$status,
array_map(static fn (FeatureStatus $feature): DiviFeature => $feature->feature, $locked)
);
$named = array_values(array_filter(
array_map(static fn (FeatureStatus $feature): Capability => $feature->feature, $locked),
static fn (Capability $capability): bool => $capability instanceof DiviFeature
));
// Fail closed: the lock stays; without a DiviFeature among its capabilities it names the features asked for.
return new DiviLocked($status, $named === [] ? $features : $named);
}
public static function whileProbing(int $postId, callable $write): mixed
@@ -5,6 +5,9 @@ declare(strict_types=1);
namespace LIT\MCP\Integrations\Divi\Release;
use Closure;
use LIT\MCP\Compatibility\CompatibilityMatrix;
use LIT\MCP\Compatibility\ManifestCheck;
use LIT\MCP\Compatibility\Problem;
use LIT\MCP\Plugin;
defined('ABSPATH') || exit;
@@ -44,15 +47,19 @@ final class ReleaseResolver
public function resolve(): ReleaseStatus
{
$version = ($this->version ?? DiviEnvironment::version(...))() ?? '';
$check = $this->check ?? new ManifestCheck();
$check = $this->check ?? DiviManifestCheck::create();
$problems = array_merge($check->codeProblems(), $this->mirrorProblems($check, $version));
if (in_array($version, DiviReleases::TESTED, true)) {
return ReleaseStatus::of($version, self::matrix($problems, ReleaseStatus::SOURCE_CODE, false));
return ReleaseStatus::of($version, self::matrix($problems, CompatibilityMatrix::SOURCE_CODE, false));
}
$entry = $version === '' ? null : ($this->store ?? new ReleaseStore())->find($version, Plugin::VERSION);
if ($entry !== null) {
return ReleaseStatus::of($version, self::matrix($problems, ReleaseStatus::SOURCE_DATABASE, false), $entry);
return ReleaseStatus::of(
$version,
self::matrix($problems, CompatibilityMatrix::SOURCE_DATABASE, false),
$entry
);
}
return ReleaseStatus::of($version, self::matrix($problems, null, $version !== '' && self::automatic()));
@@ -81,60 +88,10 @@ final class ReleaseResolver
return apply_filters(self::AUTO_RELEASE_FILTER, true) === true;
}
/**
* @param list<Problem> $problems
* @param string|null $source SOURCE_CODE or SOURCE_DATABASE for a released version, else null.
* @param bool $automatic The automatic release applies (no release, known version, filter on).
*/
/** @param list<Problem> $problems */
private static function matrix(array $problems, ?string $source, bool $automatic): CompatibilityMatrix
{
$features = [];
foreach (DiviFeature::cases() as $feature) {
$concerning = array_values(array_filter(
$problems,
static fn (Problem $problem): bool => $problem->concerns($feature)
));
$features[$feature->value] = self::feature($feature, $concerning, $source, $automatic);
}
return new CompatibilityMatrix($features);
}
/** @param list<Problem> $problems The problems concerning $feature. */
private static function feature(
DiviFeature $feature,
array $problems,
?string $source,
bool $automatic
): FeatureStatus {
$hard = array_filter($problems, static fn (Problem $problem): bool => $problem->isHard());
if ($hard !== []) {
return new FeatureStatus($feature, ReleaseState::Incompatible, self::labels($hard));
}
if ($source !== null) {
return new FeatureStatus($feature, ReleaseState::Released, [], $source);
}
if (! $automatic) {
return new FeatureStatus($feature, ReleaseState::Untested, self::labels($problems));
}
$changed = array_filter($problems, static fn (Problem $problem): bool => $problem->kind === Problem::SIGNATURE);
if ($changed !== []) {
return new FeatureStatus($feature, ReleaseState::Incompatible, self::labels($changed));
}
return $problems === []
? new FeatureStatus($feature, ReleaseState::Released, [], ReleaseStatus::SOURCE_AUTO)
: new FeatureStatus($feature, ReleaseState::Untested, self::labels($problems));
}
/**
* @param array<int, Problem> $problems
* @return list<string>
*/
private static function labels(array $problems): array
{
return array_values(array_map(static fn (Problem $problem): string => $problem->label, $problems));
return CompatibilityMatrix::fromProblems(DiviFeature::cases(), $problems, $source, $automatic);
}
/** @return list<Problem> */
@@ -174,7 +131,7 @@ final class ReleaseResolver
$problems = [];
foreach ($entry['problems'] as $row) {
$problem = Problem::fromArray($row);
$problem = Problem::fromArray($row, DiviFeature::tryFrom(...));
if ($problem === null) {
return null;
}
@@ -5,6 +5,9 @@ declare(strict_types=1);
namespace LIT\MCP\Integrations\Divi\Release;
use LIT\MCP\Abilities\Errors;
use LIT\MCP\Compatibility\CompatibilityMatrix;
use LIT\MCP\Compatibility\FeatureStatus;
use LIT\MCP\Compatibility\ReleaseState;
use LogicException;
use WP_Error;
@@ -16,9 +19,9 @@ defined('ABSPATH') || exit;
*/
final class ReleaseStatus
{
public const SOURCE_CODE = 'code';
public const SOURCE_DATABASE = 'database';
public const SOURCE_AUTO = 'auto';
public const SOURCE_CODE = CompatibilityMatrix::SOURCE_CODE;
public const SOURCE_DATABASE = CompatibilityMatrix::SOURCE_DATABASE;
public const SOURCE_AUTO = CompatibilityMatrix::SOURCE_AUTO;
private const FEATURE_UNTESTED = 'Achtung: %1$s ist auf dieser Website für %2$s noch nicht für LIT MCP '
. 'freigegeben. Das ist gesperrt, bis ein Administrator unter Einstellungen → LIT MCP → Divi die Prüfung '
@@ -52,7 +55,7 @@ final class ReleaseStatus
if ($matrix !== null && $matrix->state() !== $state) {
throw new LogicException('the matrix is ' . $matrix->state()->value . ', not ' . $state->value);
}
$this->matrix = $matrix ?? CompatibilityMatrix::uniform($state, $problems, $source);
$this->matrix = $matrix ?? CompatibilityMatrix::uniform(DiviFeature::cases(), $state, $problems, $source);
}
/** @param array<string, mixed>|null $release The database entry of a SOURCE_DATABASE release. */
+4 -3
View File
@@ -7,10 +7,11 @@ namespace LIT\MCP\Integrations\Divi\Schema;
use ET\Builder\Packages\ModuleLibrary\ModuleRegistration;
use LIT\MCP\Integrations\Divi\BlockTree\GlobalInstance;
use LIT\MCP\Integrations\Divi\Library\DiviLibraryStore;
use LIT\MCP\Integrations\Divi\Release\Call;
use LIT\MCP\Compatibility\Call;
use LIT\MCP\Integrations\Divi\Release\DiviAdapter;
use LIT\MCP\Integrations\Divi\Release\DiviAreas;
use LIT\MCP\Integrations\Divi\Release\DiviFeature;
use LIT\MCP\Integrations\Divi\Release\Manifest;
use LIT\MCP\Compatibility\Manifest;
use LIT\MCP\Integrations\Divi\ThemeBuilder\DiviThemeBuilder;
defined('ABSPATH') || exit;
@@ -39,7 +40,7 @@ class DiviModules implements DiviAdapter
public static function manifest(): Manifest
{
return new Manifest(
Manifest::CATALOG,
DiviAreas::CATALOG,
calls: [
new Call(ModuleRegistration::class . '::get_all_core_modules_metadata', 0),
new Call(ModuleRegistration::class . '::get_default_attrs', 2),
@@ -4,12 +4,13 @@ declare(strict_types=1);
namespace LIT\MCP\Integrations\Divi\ThemeBuilder;
use LIT\MCP\Integrations\Divi\Release\Call;
use LIT\MCP\Compatibility\Call;
use LIT\MCP\Integrations\Divi\Release\DiviAdapter;
use LIT\MCP\Integrations\Divi\Release\DiviAreas;
use LIT\MCP\Integrations\Divi\Release\DiviFeature;
use LIT\MCP\Integrations\Divi\Release\DiviLocked;
use LIT\MCP\Integrations\Divi\Release\Manifest;
use LIT\MCP\Integrations\Divi\Release\Mirror;
use LIT\MCP\Compatibility\Manifest;
use LIT\MCP\Compatibility\Mirror;
use LIT\MCP\Integrations\Divi\Release\ReleaseGate;
use RuntimeException;
use Throwable;
@@ -83,7 +84,7 @@ class DiviThemeBuilder implements DiviAdapter
$dir = 'includes/builder/frontend-builder/theme-builder/';
return new Manifest(
Manifest::THEME_BUILDER,
DiviAreas::THEME_BUILDER,
calls: [
// liveId(): reads the live post, or creates it like Divi's first save.
new Call(