feat: Divi-Freigabe je Fähigkeit – CompatibilityMatrix aus Problemen mit Art und Fähigkeiten (ManifestCheck::problems), automatische Freigabe unveränderter Fähigkeiten (Filter lit_mcp_divi_auto_release), Mirror-Probleme im Transient (Format 2), ReleaseStatus trägt die Matrix
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
710444e88d
commit
c4efeb3ba4
@@ -0,0 +1,136 @@
|
||||
<?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);
|
||||
}
|
||||
|
||||
/** @return list<DiviFeature> The features that write (ReleaseGate::assertWritable(), ReleaseStatus::writable()). */
|
||||
public static function writeFeatures(): array
|
||||
{
|
||||
return array_values(array_filter(
|
||||
DiviFeature::cases(),
|
||||
static fn (DiviFeature $feature): bool => $feature->isWrite()
|
||||
));
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/** @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));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Integrations\Divi\Release;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
/** The release state of one Divi feature (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
|
||||
* SOURCE_AUTO; null otherwise.
|
||||
*/
|
||||
public function __construct(
|
||||
public readonly DiviFeature $feature,
|
||||
public readonly ReleaseState $state,
|
||||
public readonly array $problems = [],
|
||||
public readonly ?string $source = null
|
||||
) {
|
||||
}
|
||||
|
||||
public function usable(): bool
|
||||
{
|
||||
return $this->state === ReleaseState::Released;
|
||||
}
|
||||
|
||||
/** @return array{id: string, label: string, state: string, problems: list<string>} For refusals (D6). */
|
||||
public function summary(): array
|
||||
{
|
||||
return [
|
||||
'id' => $this->feature->value,
|
||||
'label' => $this->feature->label(),
|
||||
'state' => $this->state->value,
|
||||
'problems' => $this->problems,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{id: string, label: string, state: string, problems: list<string>, source: ?string}
|
||||
* For lit-site-status and the admin tab.
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
return $this->summary() + ['source' => $this->source];
|
||||
}
|
||||
}
|
||||
@@ -13,8 +13,9 @@ use Throwable;
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
/**
|
||||
* The checks of all adapter manifests (spec §4.3), as label => check. Code checks (calls with the argument count
|
||||
* the adapter passes, constants) and mirror checks decide the release gate; registration checks (post types,
|
||||
* The checks of all adapter manifests (spec §4.3), as label => check for reports, and as problems with their Divi
|
||||
* 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.
|
||||
*
|
||||
* Not final so the resolver can be tested with a mock.
|
||||
@@ -32,7 +33,8 @@ class ManifestCheck
|
||||
|
||||
/**
|
||||
* @param array<string, Manifest>|null $manifests null = DiviAdapters::manifests().
|
||||
* @param array<string, string>|null $expected Mirror ID => sha256; null = DiviReference::expected().
|
||||
* @param array<string, string>|null $expected Mirror ID or "sig:<symbol>" => sha256;
|
||||
* null = DiviReference::expected().
|
||||
* @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().
|
||||
@@ -71,12 +73,7 @@ class ManifestCheck
|
||||
$checks = [];
|
||||
foreach ($this->manifests as $manifest) {
|
||||
foreach ($manifest->mirrors as $mirror) {
|
||||
$label = sprintf('%s: Nachbau von %s – %s', $manifest->area, $mirror->label(), $mirror->what);
|
||||
$checks[$label] = function () use ($mirror): bool {
|
||||
$expected = ($this->expected ??= DiviReference::expected())[$mirror->id] ?? null;
|
||||
|
||||
return $expected !== null && ($this->current)($mirror) === $expected;
|
||||
};
|
||||
$checks[self::mirrorLabel($manifest, $mirror)] = fn (): bool => $this->matches($mirror);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,12 +108,7 @@ class ManifestCheck
|
||||
{
|
||||
$failed = [];
|
||||
foreach ($checks as $label => $check) {
|
||||
try {
|
||||
$ok = (bool) $check();
|
||||
} catch (Throwable) {
|
||||
$ok = false;
|
||||
}
|
||||
if (! $ok) {
|
||||
if (! self::passes($check)) {
|
||||
$failed[] = (string) $label;
|
||||
}
|
||||
}
|
||||
@@ -124,6 +116,67 @@ class ManifestCheck
|
||||
return $failed;
|
||||
}
|
||||
|
||||
/** @return list<Problem> Code and mirror problems with their features – what the release matrix is built of. */
|
||||
public function problems(): array
|
||||
{
|
||||
return array_merge($this->codeProblems(), $this->mirrorProblems());
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls (missing or other argument count: call; no reference: reference-missing; other signature: signature)
|
||||
* and constants. Reflection only – cheap enough for every request.
|
||||
*
|
||||
* @return list<Problem>
|
||||
*/
|
||||
public function codeProblems(): array
|
||||
{
|
||||
$problems = [];
|
||||
foreach ($this->manifests as $manifest) {
|
||||
foreach ($manifest->calls as $call) {
|
||||
$problem = $this->callProblem($manifest, $call);
|
||||
if ($problem !== null) {
|
||||
$problems[] = $problem;
|
||||
}
|
||||
}
|
||||
foreach ($manifest->constants as $constant) {
|
||||
if (! defined($constant->name)) {
|
||||
$problems[] = new Problem(
|
||||
$manifest->area . ': ' . $constant->label(),
|
||||
$manifest->featuresOf($constant),
|
||||
Problem::CONSTANT
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $problems;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @return list<Problem>
|
||||
*/
|
||||
public function mirrorProblems(): array
|
||||
{
|
||||
$problems = [];
|
||||
foreach ($this->manifests as $manifest) {
|
||||
foreach ($manifest->mirrors as $mirror) {
|
||||
if (! self::passes(fn (): bool => $this->matches($mirror))) {
|
||||
$problems[] = new Problem(
|
||||
self::mirrorLabel($manifest, $mirror),
|
||||
$manifest->featuresOf($mirror),
|
||||
Problem::MIRROR
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $problems;
|
||||
}
|
||||
|
||||
/** The function or method exists and takes the number of arguments the adapter passes. */
|
||||
public static function accepts(Call $call): bool
|
||||
{
|
||||
@@ -145,6 +198,53 @@ class ManifestCheck
|
||||
&& ($reflection->isVariadic() || $reflection->getNumberOfParameters() >= $call->args));
|
||||
}
|
||||
|
||||
private function callProblem(Manifest $manifest, Call $call): ?Problem
|
||||
{
|
||||
$label = $manifest->area . ': ' . $call->label();
|
||||
$features = $manifest->featuresOf($call);
|
||||
if (! self::passes(static fn (): bool => self::accepts($call))) {
|
||||
return new Problem($label, $features, Problem::CALL);
|
||||
}
|
||||
|
||||
$expected = $this->expected()[DiviReference::signatureKey($call)] ?? null;
|
||||
if ($expected === null) {
|
||||
return new Problem($label . ' – Referenz der Signatur fehlt', $features, Problem::REFERENCE_MISSING);
|
||||
}
|
||||
|
||||
return self::passes(static fn (): bool => SignatureFingerprint::of($call) === $expected)
|
||||
? null
|
||||
: new Problem($label . ' – Signatur geändert', $features, Problem::SIGNATURE);
|
||||
}
|
||||
|
||||
/** 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;
|
||||
|
||||
return $expected !== null && ($this->current)($mirror) === $expected;
|
||||
}
|
||||
|
||||
/** @return array<string, string> */
|
||||
private function expected(): array
|
||||
{
|
||||
return $this->expected ??= DiviReference::expected();
|
||||
}
|
||||
|
||||
private static function mirrorLabel(Manifest $manifest, Mirror $mirror): string
|
||||
{
|
||||
return sprintf('%s: Nachbau von %s – %s', $manifest->area, $mirror->label(), $mirror->what);
|
||||
}
|
||||
|
||||
/** @param callable(): bool $check A check that throws failed. */
|
||||
private static function passes(callable $check): bool
|
||||
{
|
||||
try {
|
||||
return (bool) $check();
|
||||
} catch (Throwable) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static function installed(Mirror $mirror): ?string
|
||||
{
|
||||
$dir = DiviEnvironment::themeDir();
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Integrations\Divi\Release;
|
||||
|
||||
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).
|
||||
*/
|
||||
final class Problem
|
||||
{
|
||||
public const CALL = 'call';
|
||||
public const SIGNATURE = 'signature';
|
||||
public const MIRROR = 'mirror';
|
||||
public const CONSTANT = 'constant';
|
||||
public const REFERENCE_MISSING = 'reference-missing';
|
||||
public const RESOLVER = 'resolver';
|
||||
|
||||
private const KINDS = [
|
||||
self::CALL,
|
||||
self::SIGNATURE,
|
||||
self::MIRROR,
|
||||
self::CONSTANT,
|
||||
self::REFERENCE_MISSING,
|
||||
self::RESOLVER,
|
||||
];
|
||||
private const HARD = [self::CALL, self::MIRROR, self::CONSTANT, 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.
|
||||
*/
|
||||
public function __construct(
|
||||
public readonly string $label,
|
||||
public readonly array $features,
|
||||
public readonly string $kind
|
||||
) {
|
||||
}
|
||||
|
||||
/** Locks its features whatever the release (the code no longer fits Divi or a mirror differs). */
|
||||
public function isHard(): bool
|
||||
{
|
||||
return in_array($this->kind, self::HARD, true);
|
||||
}
|
||||
|
||||
public function concerns(DiviFeature $feature): bool
|
||||
{
|
||||
return $this->features === [] || in_array($feature, $this->features, true);
|
||||
}
|
||||
|
||||
/** @return array{label: string, kind: string, features: list<string>} For the transient. */
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'label' => $this->label,
|
||||
'kind' => $this->kind,
|
||||
'features' => array_map(static fn (DiviFeature $feature): string => $feature->value, $this->features),
|
||||
];
|
||||
}
|
||||
|
||||
/** The problem of toArray(), or null for anything else (an unknown kind or feature included). */
|
||||
public static function fromArray(mixed $row): ?self
|
||||
{
|
||||
if (
|
||||
! is_array($row)
|
||||
|| ! is_string($row['label'] ?? null)
|
||||
|| ! in_array($row['kind'] ?? null, self::KINDS, true)
|
||||
|| ! is_array($row['features'] ?? null)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$features = [];
|
||||
foreach ($row['features'] as $value) {
|
||||
$feature = is_string($value) ? DiviFeature::tryFrom($value) : null;
|
||||
if ($feature === null) {
|
||||
return null;
|
||||
}
|
||||
$features[] = $feature;
|
||||
}
|
||||
|
||||
return new self($row['label'], $features, $row['kind']);
|
||||
}
|
||||
}
|
||||
@@ -24,7 +24,9 @@ final class ReleaseGate
|
||||
|
||||
private static ?int $probe = null;
|
||||
|
||||
/** Spec §9: a status that cannot be determined locks writes like an incompatible Divi (and is logged). */
|
||||
private const UNRESOLVED = 'Der Freigabe-Zustand ließ sich nicht ermitteln (Details im PHP-Error-Log).';
|
||||
|
||||
/** Spec §9: a status that cannot be determined locks every feature as incompatible (and is logged). */
|
||||
public static function status(): ReleaseStatus
|
||||
{
|
||||
if (self::$status !== null) {
|
||||
@@ -35,12 +37,7 @@ final class ReleaseGate
|
||||
self::$status = (self::$resolver ?? static fn (): ReleaseStatus => (new ReleaseResolver())->resolve())();
|
||||
} catch (Throwable $e) {
|
||||
Log::failure('divi release status', $e);
|
||||
self::$status = new ReleaseStatus(
|
||||
ReleaseState::Incompatible,
|
||||
'',
|
||||
null,
|
||||
['Der Freigabe-Zustand ließ sich nicht ermitteln (Details im PHP-Error-Log).']
|
||||
);
|
||||
self::$status = ReleaseStatus::of('', CompatibilityMatrix::allIncompatible(self::UNRESOLVED));
|
||||
}
|
||||
|
||||
return self::$status;
|
||||
|
||||
@@ -10,13 +10,24 @@ use LIT\MCP\Plugin;
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
/**
|
||||
* Computes the release status (spec §5.2–§5.4): code checks live (cheap), mirror checks cached in a transient under
|
||||
* stateKey() – Divi version, plugin version, fingerprints.json and the modification times of the mirrored Divi
|
||||
* files, so a Divi update or hotfix is checked again at once.
|
||||
* Computes the release status (spec §5.2–§5.4) as a matrix of Divi features (E1). Code and signature checks run
|
||||
* live (Reflection, cheap; a hotfix without a new version number is seen at once), mirror checks are cached in a
|
||||
* transient under stateKey() – Divi version, plugin version, fingerprints.json (mirror and signature references) and
|
||||
* the modification times of the mirrored Divi files.
|
||||
*
|
||||
* Per feature: a hard problem (call, mirror, constant) makes it incompatible, released version or not. A released
|
||||
* version (DiviReleases::TESTED, source code; ReleaseStore, source database) releases every other feature – its
|
||||
* release covers signatures. Otherwise, with the automatic release on (filter lit_mcp_divi_auto_release, default
|
||||
* true) and a known version: a changed signature makes the feature incompatible, a missing reference untested,
|
||||
* no problem releases it (source auto). Without the automatic release every other feature is untested.
|
||||
*/
|
||||
final class ReleaseResolver
|
||||
{
|
||||
public const CACHE = 'lit_mcp_divi_state';
|
||||
public const AUTO_RELEASE_FILTER = 'lit_mcp_divi_auto_release';
|
||||
|
||||
/** Version of the transient's layout; an entry without it (the list of labels before) is a miss. */
|
||||
private const CACHE_FORMAT = 2;
|
||||
|
||||
/**
|
||||
* @param (Closure(): ?string)|null $version null = DiviEnvironment::version().
|
||||
@@ -34,22 +45,17 @@ final class ReleaseResolver
|
||||
{
|
||||
$version = ($this->version ?? DiviEnvironment::version(...))() ?? '';
|
||||
$check = $this->check ?? new ManifestCheck();
|
||||
$problems = array_merge(
|
||||
ManifestCheck::failed($check->codeChecks()),
|
||||
$this->mirrorProblems($check, $version)
|
||||
);
|
||||
$problems = array_merge($check->codeProblems(), $this->mirrorProblems($check, $version));
|
||||
|
||||
if ($problems !== []) {
|
||||
return new ReleaseStatus(ReleaseState::Incompatible, $version, null, $problems);
|
||||
}
|
||||
if (in_array($version, DiviReleases::TESTED, true)) {
|
||||
return ReleaseStatus::released($version);
|
||||
return ReleaseStatus::of($version, self::matrix($problems, ReleaseStatus::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);
|
||||
}
|
||||
$entry = ($this->store ?? new ReleaseStore())->find($version, Plugin::VERSION);
|
||||
|
||||
return $entry !== null
|
||||
? new ReleaseStatus(ReleaseState::Released, $version, ReleaseStatus::SOURCE_DATABASE, [], $entry)
|
||||
: new ReleaseStatus(ReleaseState::Untested, $version);
|
||||
return ReleaseStatus::of($version, self::matrix($problems, null, $version !== '' && self::automatic()));
|
||||
}
|
||||
|
||||
/** What the cached mirror results and a check report are valid for. */
|
||||
@@ -69,17 +75,111 @@ final class ReleaseResolver
|
||||
return hash('sha256', implode('|', $parts));
|
||||
}
|
||||
|
||||
/** @return list<string> */
|
||||
/** Only a literal true switches the automatic release on (fail closed). */
|
||||
private static function automatic(): bool
|
||||
{
|
||||
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).
|
||||
*/
|
||||
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 list<Problem> */
|
||||
private function mirrorProblems(ManifestCheck $check, string $version): array
|
||||
{
|
||||
$key = ($this->stateKey ?? self::stateKey(...))($version);
|
||||
$cached = get_transient(self::CACHE);
|
||||
if (is_array($cached) && ($cached['key'] ?? null) === $key && is_array($cached['problems'] ?? null)) {
|
||||
return array_values(array_filter($cached['problems'], 'is_string'));
|
||||
$cached = self::cached(get_transient(self::CACHE), $key);
|
||||
if ($cached !== null) {
|
||||
return $cached;
|
||||
}
|
||||
|
||||
$problems = ManifestCheck::failed($check->mirrorChecks());
|
||||
set_transient(self::CACHE, ['key' => $key, 'problems' => $problems], DAY_IN_SECONDS);
|
||||
$problems = $check->mirrorProblems();
|
||||
set_transient(
|
||||
self::CACHE,
|
||||
[
|
||||
'format' => self::CACHE_FORMAT,
|
||||
'key' => $key,
|
||||
'problems' => array_map(static fn (Problem $problem): array => $problem->toArray(), $problems),
|
||||
],
|
||||
DAY_IN_SECONDS
|
||||
);
|
||||
|
||||
return $problems;
|
||||
}
|
||||
|
||||
/** @return list<Problem>|null null = miss: none, another key, the old format or any unreadable row. */
|
||||
private static function cached(mixed $entry, string $key): ?array
|
||||
{
|
||||
if (
|
||||
! is_array($entry)
|
||||
|| ($entry['format'] ?? null) !== self::CACHE_FORMAT
|
||||
|| ($entry['key'] ?? null) !== $key
|
||||
|| ! is_array($entry['problems'] ?? null)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$problems = [];
|
||||
foreach ($entry['problems'] as $row) {
|
||||
$problem = Problem::fromArray($row);
|
||||
if ($problem === null) {
|
||||
return null;
|
||||
}
|
||||
$problems[] = $problem;
|
||||
}
|
||||
|
||||
return $problems;
|
||||
}
|
||||
|
||||
@@ -10,11 +10,15 @@ use WP_Error;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
/** Whether LIT MCP may write with the installed Divi (spec §5.2), with the texts of spec §5.6. */
|
||||
/**
|
||||
* The release of the installed Divi (spec §5.2): the matrix of its features (E1) and, for callers that know no
|
||||
* features, the overall state (the worst feature state) with every problem, and the texts of spec §5.6.
|
||||
*/
|
||||
final class ReleaseStatus
|
||||
{
|
||||
public const SOURCE_CODE = 'code';
|
||||
public const SOURCE_DATABASE = 'database';
|
||||
public const SOURCE_AUTO = 'auto';
|
||||
|
||||
private const UNTESTED = 'Achtung: %s ist auf dieser Website noch nicht für LIT MCP freigegeben. Schreibende '
|
||||
. 'Divi-Werkzeuge sind gesperrt, bis ein Administrator unter Einstellungen → LIT MCP → Divi die Prüfung '
|
||||
@@ -27,19 +31,35 @@ final class ReleaseStatus
|
||||
. 'Divi-Werkzeuge funktionieren weiter.';
|
||||
private const NOTICE = 'Achtung: %s ist für LIT MCP %s; Lesen funktioniert, Schreiben ist gesperrt.';
|
||||
|
||||
public readonly CompatibilityMatrix $matrix;
|
||||
|
||||
/**
|
||||
* Without $matrix every feature is in $state (CompatibilityMatrix::uniform()); with one, use of().
|
||||
*
|
||||
* @param string $version Divi version, '' when unknown (Divi is not the active template).
|
||||
* @param string|null $source SOURCE_CODE or SOURCE_DATABASE for a released version.
|
||||
* @param string|null $source SOURCE_CODE, SOURCE_DATABASE or SOURCE_AUTO for a released version.
|
||||
* @param list<string> $problems Failed code and mirror checks (ManifestCheck labels).
|
||||
* @param array<string, mixed>|null $release The database entry of a SOURCE_DATABASE release.
|
||||
* @throws LogicException When $matrix is in another state than $state.
|
||||
*/
|
||||
public function __construct(
|
||||
public readonly ReleaseState $state,
|
||||
public readonly string $version,
|
||||
public readonly ?string $source = null,
|
||||
public readonly array $problems = [],
|
||||
public readonly ?array $release = null
|
||||
public readonly ?array $release = null,
|
||||
?CompatibilityMatrix $matrix = null
|
||||
) {
|
||||
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);
|
||||
}
|
||||
|
||||
/** @param array<string, mixed>|null $release The database entry of a SOURCE_DATABASE release. */
|
||||
public static function of(string $version, CompatibilityMatrix $matrix, ?array $release = null): self
|
||||
{
|
||||
return new self($matrix->state(), $version, $matrix->source(), $matrix->problems(), $release, $matrix);
|
||||
}
|
||||
|
||||
public static function released(string $version, string $source = self::SOURCE_CODE): self
|
||||
@@ -47,9 +67,10 @@ final class ReleaseStatus
|
||||
return new self(ReleaseState::Released, $version, $source);
|
||||
}
|
||||
|
||||
/** Every writing feature is usable – for callers that know no feature (DiviToolGate, DiviContentComposer). */
|
||||
public function writable(): bool
|
||||
{
|
||||
return $this->state === ReleaseState::Released;
|
||||
return $this->matrix->usable(CompatibilityMatrix::writeFeatures());
|
||||
}
|
||||
|
||||
public function versionLabel(): string
|
||||
|
||||
Reference in New Issue
Block a user