refactor(compat): Freigabe-Bausteine generisch
ReleaseStore, CheckReportStore, CheckReport, CheckStep und CompatibilityCheck liegen im Kern und arbeiten je Profil (Optionen lit_mcp_<id>_releases|_check_report; Divi behält Namen und Schlüssel divi_version). Divi hängt Katalog-, Format- und Schreibtest über Profile::extraSteps an. Ein roter Prüfbericht für den aktuellen Stand sperrt LockOnFailure-Profile bis zur nächsten grünen Prüfung; Sperrgründe nennen nur Schritt-Labels, Ausnahmen gehen ins Error-Log. Profile bekommt testedVersions, stateKey, extraSteps, manifestCheck und reportVersionKey und wird mit benannten Argumenten gebaut. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
c85131603b
commit
7d6a97a54e
@@ -10,16 +10,18 @@ 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\Compatibility\CheckReport;
|
||||
use LIT\MCP\Compatibility\CheckReportStore;
|
||||
use LIT\MCP\Integrations\Divi\Release\DiviCheckRoute;
|
||||
use LIT\MCP\Integrations\Divi\Release\DiviCheckSteps;
|
||||
use LIT\MCP\Integrations\Divi\Release\DiviProfile;
|
||||
use LIT\MCP\Integrations\Divi\Release\DiviEnvironment;
|
||||
use LIT\MCP\Integrations\Divi\Release\FeatureOverview;
|
||||
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\ReleaseStatus;
|
||||
use LIT\MCP\Integrations\Divi\Release\ReleaseStore;
|
||||
use LIT\MCP\Compatibility\ReleaseStore;
|
||||
use LIT\MCP\Integrations\Divi\Release\WriteProbe;
|
||||
use LIT\MCP\Plugin;
|
||||
|
||||
@@ -159,7 +161,7 @@ final class DiviTab implements AdminTab
|
||||
return;
|
||||
}
|
||||
|
||||
($this->releases ?? new ReleaseStore())->add($version, Plugin::VERSION, get_current_user_id(), $report->id);
|
||||
$this->releases()->add($version, Plugin::VERSION, get_current_user_id(), $report->id);
|
||||
ReleaseGate::invalidate();
|
||||
$this->log?->record(
|
||||
'lit/divi-release',
|
||||
@@ -177,7 +179,7 @@ final class DiviTab implements AdminTab
|
||||
check_admin_referer(self::WITHDRAW_ACTION);
|
||||
|
||||
$version = $this->version();
|
||||
if (! ($this->releases ?? new ReleaseStore())->withdraw($version, Plugin::VERSION)) {
|
||||
if (! $this->releases()->withdraw($version, Plugin::VERSION)) {
|
||||
$action->redirect(self::SLUG, 'divi_withdraw_failed');
|
||||
|
||||
return;
|
||||
@@ -514,7 +516,7 @@ final class DiviTab implements AdminTab
|
||||
esc_html(sprintf(
|
||||
/* translators: 1: Divi version, 2: plugin version, 3: date */
|
||||
__('Divi %1$s mit LIT MCP %2$s, %3$s', 'wp-lit-mcp'),
|
||||
$report->diviVersion,
|
||||
$report->version,
|
||||
$report->pluginVersion,
|
||||
date_i18n((string) get_option('date_format'), (int) strtotime($report->createdAt))
|
||||
))
|
||||
@@ -530,7 +532,7 @@ final class DiviTab implements AdminTab
|
||||
. ' aria-hidden="true"></span><span class="screen-reader-text">%2$s:</span> %3$s',
|
||||
esc_attr($icon),
|
||||
esc_html($word),
|
||||
esc_html($step->label())
|
||||
esc_html(DiviCheckSteps::label($step))
|
||||
);
|
||||
if ($step->skipped) {
|
||||
echo ' (' . esc_html__('nicht ausgeführt', 'wp-lit-mcp') . ')';
|
||||
@@ -569,8 +571,13 @@ final class DiviTab implements AdminTab
|
||||
return ($this->stateKey ?? static fn (string $v): string => ReleaseResolver::stateKey($v))($version);
|
||||
}
|
||||
|
||||
private function releases(): ReleaseStore
|
||||
{
|
||||
return $this->releases ?? new ReleaseStore(DiviProfile::ID, DiviProfile::REPORT_VERSION_KEY);
|
||||
}
|
||||
|
||||
private function reports(): CheckReportStore
|
||||
{
|
||||
return $this->reports ?? new CheckReportStore();
|
||||
return $this->reports ?? new CheckReportStore(DiviProfile::ID, DiviProfile::REPORT_VERSION_KEY);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,10 +7,12 @@ 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\Compatibility\CheckReport;
|
||||
use LIT\MCP\Compatibility\CheckReportStore;
|
||||
use LIT\MCP\Compatibility\CheckStep;
|
||||
use LIT\MCP\Compatibility\CompatibilityCheck;
|
||||
use LIT\MCP\Integrations\Divi\Release\DiviCheckSteps;
|
||||
use LIT\MCP\Integrations\Divi\Release\DiviProfile;
|
||||
use LIT\MCP\Compatibility\CompatibilityMatrix;
|
||||
use LIT\MCP\Integrations\Divi\Release\DiviAdapters;
|
||||
use LIT\MCP\Integrations\Divi\Release\DiviEnvironment;
|
||||
@@ -109,10 +111,10 @@ final class DiviCheckCommand
|
||||
}
|
||||
|
||||
$report = ($this->check)()->run(get_current_user_id());
|
||||
($this->reports ?? new CheckReportStore())->save($report);
|
||||
($this->out)(sprintf('Divi %s mit LIT MCP %s', $report->diviVersion ?: '?', $report->pluginVersion));
|
||||
($this->reports ?? new CheckReportStore(DiviProfile::ID, DiviProfile::REPORT_VERSION_KEY))->save($report);
|
||||
($this->out)(sprintf('Divi %s mit LIT MCP %s', $report->version ?: '?', $report->pluginVersion));
|
||||
foreach ($report->steps as $step) {
|
||||
($this->out)(($step->skipped ? '–' : ($step->green() ? '✓' : '✗')) . ' ' . $step->label());
|
||||
($this->out)(($step->skipped ? '–' : ($step->green() ? '✓' : '✗')) . ' ' . DiviCheckSteps::label($step));
|
||||
foreach ($step->problems as $problem) {
|
||||
($this->out)(' ' . $problem);
|
||||
}
|
||||
|
||||
@@ -9,6 +9,8 @@ use LIT\MCP\Abilities\Ability;
|
||||
use LIT\MCP\Abilities\ToolAvailability;
|
||||
use LIT\MCP\Admin\SettingsPage;
|
||||
use LIT\MCP\ChangeLog\ChangeLog;
|
||||
use LIT\MCP\Compatibility\CompatibilityCheck;
|
||||
use LIT\MCP\Compatibility\Profile;
|
||||
use LIT\MCP\Compatibility\ProvidesCompatibility;
|
||||
use LIT\MCP\Integrations\Bootable;
|
||||
use LIT\MCP\Integrations\Divi\Admin\DiviTab;
|
||||
@@ -73,7 +75,6 @@ use LIT\MCP\Integrations\Divi\Pages\DiviSaver;
|
||||
use LIT\MCP\Integrations\Divi\Pages\DiviUpdateAbility;
|
||||
use LIT\MCP\Integrations\Divi\Pages\DiviWriter;
|
||||
use LIT\MCP\Integrations\Divi\Pages\DynamicContentMap;
|
||||
use LIT\MCP\Integrations\Divi\Release\CompatibilityCheck;
|
||||
use LIT\MCP\Integrations\Divi\Release\DiviCheckRoute;
|
||||
use LIT\MCP\Integrations\Divi\Release\FeatureOverview;
|
||||
use LIT\MCP\Integrations\Divi\Release\DiviProfile;
|
||||
@@ -190,7 +191,7 @@ final class DiviIntegration implements Integration, Bootable, ProvidesCompatibil
|
||||
|
||||
public function compatibilities(): array
|
||||
{
|
||||
return [DiviProfile::profile()];
|
||||
return [$this->profile()];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -261,7 +262,8 @@ final class DiviIntegration implements Integration, Bootable, ProvidesCompatibil
|
||||
|
||||
// Spec §6: the admin check runs as a REST request (same context as MCP calls). Its short form has no
|
||||
// Tools-tab gate: the check tests Divi, not this site's tool switches.
|
||||
$check = fn (): CompatibilityCheck => CompatibilityCheck::create($this->shortForm(null));
|
||||
$profile = $this->profile();
|
||||
$check = static fn (): CompatibilityCheck => CompatibilityCheck::forProfile($profile);
|
||||
(new DiviCheckRoute($check))->register();
|
||||
DiviCheckCommand::register($check);
|
||||
|
||||
@@ -429,6 +431,12 @@ final class DiviIntegration implements Integration, Bootable, ProvidesCompatibil
|
||||
}
|
||||
|
||||
/** @param (Closure(): bool)|null $writable */
|
||||
/** The Divi profile; the check's write test composes through the short form without a Tools-tab gate. */
|
||||
private function profile(): Profile
|
||||
{
|
||||
return DiviProfile::profile(fn (): DiviContentComposer => $this->shortForm(null));
|
||||
}
|
||||
|
||||
private function shortForm(?Closure $writable): DiviContentComposer
|
||||
{
|
||||
return new DiviContentComposer($this->schema, $this->composition(), new DiviSaver(), $this->design, $writable);
|
||||
|
||||
@@ -1,105 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Integrations\Divi\Release;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
/** The result of one admin check (spec §6.3); a release needs a fresh green one. */
|
||||
final class CheckReport
|
||||
{
|
||||
/** @param list<CheckStep> $steps */
|
||||
public function __construct(
|
||||
public readonly string $id,
|
||||
public readonly string $createdAt,
|
||||
public readonly int $userId,
|
||||
public readonly string $diviVersion,
|
||||
public readonly string $pluginVersion,
|
||||
public readonly string $stateKey,
|
||||
public readonly array $steps
|
||||
) {
|
||||
}
|
||||
|
||||
public function green(): bool
|
||||
{
|
||||
if ($this->steps === []) {
|
||||
return false;
|
||||
}
|
||||
foreach ($this->steps as $step) {
|
||||
if (! $step->green()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Green and made for exactly this Divi version, plugin version and Divi files (ReleaseResolver::stateKey()). */
|
||||
public function freshFor(string $diviVersion, string $pluginVersion, string $stateKey): bool
|
||||
{
|
||||
return $this->green()
|
||||
&& $this->diviVersion === $diviVersion
|
||||
&& $this->pluginVersion === $pluginVersion
|
||||
&& $this->stateKey === $stateKey;
|
||||
}
|
||||
|
||||
public function step(string $key): ?CheckStep
|
||||
{
|
||||
foreach ($this->steps as $step) {
|
||||
if ($step->key === $key) {
|
||||
return $step;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/** @return array<string, mixed> */
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'created_at' => $this->createdAt,
|
||||
'user_id' => $this->userId,
|
||||
'divi_version' => $this->diviVersion,
|
||||
'plugin_version' => $this->pluginVersion,
|
||||
'state_key' => $this->stateKey,
|
||||
'steps' => array_map(static fn (CheckStep $step): array => $step->toArray(), $this->steps),
|
||||
];
|
||||
}
|
||||
|
||||
public static function fromArray(mixed $data): ?self
|
||||
{
|
||||
if (! is_array($data)) {
|
||||
return null;
|
||||
}
|
||||
foreach (['id', 'created_at', 'divi_version', 'plugin_version', 'state_key'] as $key) {
|
||||
if (! is_string($data[$key] ?? null)) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
if (! is_int($data['user_id'] ?? null) || ! is_array($data['steps'] ?? null)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$steps = [];
|
||||
foreach ($data['steps'] as $step) {
|
||||
$parsed = CheckStep::fromArray($step);
|
||||
if ($parsed === null) {
|
||||
return null;
|
||||
}
|
||||
$steps[] = $parsed;
|
||||
}
|
||||
|
||||
return new self(
|
||||
$data['id'],
|
||||
$data['created_at'],
|
||||
$data['user_id'],
|
||||
$data['divi_version'],
|
||||
$data['plugin_version'],
|
||||
$data['state_key'],
|
||||
$steps
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Integrations\Divi\Release;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
/** The latest admin check report of this site (spec §6.3). Not final so the Divi tab can be tested with a mock. */
|
||||
class CheckReportStore
|
||||
{
|
||||
public const OPTION = 'lit_mcp_divi_check_report';
|
||||
|
||||
public function latest(): ?CheckReport
|
||||
{
|
||||
return CheckReport::fromArray(get_option(self::OPTION, null));
|
||||
}
|
||||
|
||||
public function save(CheckReport $report): void
|
||||
{
|
||||
update_option(self::OPTION, $report->toArray(), false);
|
||||
}
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Integrations\Divi\Release;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
/** One step of the admin check (spec §6.2); skipped = not run (the write test after missing Divi functions). */
|
||||
final class CheckStep
|
||||
{
|
||||
public const MANIFEST = 'manifest';
|
||||
public const MIRRORS = 'mirrors';
|
||||
public const CATALOG = 'catalog';
|
||||
public const FORMAT = 'format';
|
||||
public const WRITE = 'write';
|
||||
|
||||
private const LABELS = [
|
||||
self::MANIFEST => 'Divi-Funktionen, Klassen und Registrierungen',
|
||||
self::MIRRORS => 'Fingerabdrücke nachgebauter Divi-Funktionen',
|
||||
self::CATALOG => 'Modulkatalog (Kurzform-Pfade)',
|
||||
self::FORMAT => 'Format im Speicher (Parser und Serializer)',
|
||||
self::WRITE => 'Schreibtest auf einer Wegwerfseite',
|
||||
];
|
||||
|
||||
/** @param list<string> $problems */
|
||||
public function __construct(
|
||||
public readonly string $key,
|
||||
public readonly array $problems,
|
||||
public readonly bool $skipped = false
|
||||
) {
|
||||
}
|
||||
|
||||
public function green(): bool
|
||||
{
|
||||
return ! $this->skipped && $this->problems === [];
|
||||
}
|
||||
|
||||
public function label(): string
|
||||
{
|
||||
return self::LABELS[$this->key] ?? $this->key;
|
||||
}
|
||||
|
||||
/** @return array{key: string, problems: list<string>, skipped: bool} */
|
||||
public function toArray(): array
|
||||
{
|
||||
return ['key' => $this->key, 'problems' => $this->problems, 'skipped' => $this->skipped];
|
||||
}
|
||||
|
||||
public static function fromArray(mixed $data): ?self
|
||||
{
|
||||
if (! is_array($data) || ! is_string($data['key'] ?? null) || ! is_array($data['problems'] ?? null)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new self(
|
||||
$data['key'],
|
||||
array_values(array_filter($data['problems'], 'is_string')),
|
||||
($data['skipped'] ?? false) === true
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
<?php
|
||||
|
||||
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;
|
||||
use LIT\MCP\Plugin;
|
||||
use Throwable;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
/**
|
||||
* The admin check (spec §6.2) – also wp lit-mcp divi-check: manifests (code and registrations), fingerprints,
|
||||
* module catalog, format round trip and the write test. A step that throws is a red step; the write test is skipped
|
||||
* only when Divi functions are missing (it would fail anyway), not when only fingerprints changed – so
|
||||
* --update-reference can see whether everything else still works. Not final so the route and the command can be
|
||||
* tested with a mock.
|
||||
*/
|
||||
class CompatibilityCheck
|
||||
{
|
||||
/**
|
||||
* @param Closure(): list<string> $catalog Short-form problems against Divi's live catalog.
|
||||
* @param Closure(): string $version Installed Divi version ('' = unknown).
|
||||
* @param Closure(string): string $stateKey ReleaseResolver::stateKey().
|
||||
*/
|
||||
public function __construct(
|
||||
private ManifestCheck $manifests,
|
||||
private Closure $catalog,
|
||||
private FormatCheck $format,
|
||||
private WriteProbe $probe,
|
||||
private Closure $version,
|
||||
private Closure $stateKey
|
||||
) {
|
||||
}
|
||||
|
||||
public static function create(ContentComposer $composer): self
|
||||
{
|
||||
return new self(
|
||||
DiviManifestCheck::create(),
|
||||
static fn (): array => DiviModules::isAvailable()
|
||||
? ShortformCheck::problems((new DiviModules())->catalog())
|
||||
: ['Divi 5 ist in dieser Anfrage nicht geladen.'],
|
||||
new FormatCheck(),
|
||||
new WriteProbe($composer),
|
||||
static fn (): string => DiviEnvironment::version() ?? '',
|
||||
static fn (string $version): string => ReleaseResolver::stateKey($version)
|
||||
);
|
||||
}
|
||||
|
||||
public function run(int $userId): CheckReport
|
||||
{
|
||||
$manifest = self::guarded(fn (): array => ManifestCheck::failed(
|
||||
array_merge($this->manifests->codeChecks(), $this->manifests->registrationChecks())
|
||||
));
|
||||
$steps = [
|
||||
new CheckStep(CheckStep::MANIFEST, $manifest),
|
||||
new CheckStep(CheckStep::MIRRORS, self::guarded(
|
||||
fn (): array => ManifestCheck::failed($this->manifests->mirrorChecks())
|
||||
)),
|
||||
new CheckStep(CheckStep::CATALOG, self::guarded($this->catalog)),
|
||||
new CheckStep(CheckStep::FORMAT, self::guarded(fn (): array => $this->format->problems())),
|
||||
$manifest === []
|
||||
? new CheckStep(CheckStep::WRITE, self::guarded(fn (): array => $this->probe->run()))
|
||||
: new CheckStep(CheckStep::WRITE, [], true),
|
||||
];
|
||||
$version = ($this->version)();
|
||||
|
||||
return new CheckReport(
|
||||
wp_generate_uuid4(),
|
||||
gmdate('c'),
|
||||
$userId,
|
||||
$version,
|
||||
Plugin::VERSION,
|
||||
($this->stateKey)($version),
|
||||
$steps
|
||||
);
|
||||
}
|
||||
|
||||
/** @return list<string> */
|
||||
private static function guarded(Closure $step): array
|
||||
{
|
||||
try {
|
||||
return array_values(array_filter((array) $step(), 'is_string'));
|
||||
} catch (Throwable $e) {
|
||||
return ['Prüfschritt abgebrochen: ' . $e->getMessage()];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,8 @@ namespace LIT\MCP\Integrations\Divi\Release;
|
||||
|
||||
use Closure;
|
||||
use LIT\MCP\Admin\SettingsPage;
|
||||
use LIT\MCP\Compatibility\CheckReportStore;
|
||||
use LIT\MCP\Compatibility\CompatibilityCheck;
|
||||
use LIT\MCP\Integrations\Divi\Admin\DiviTab;
|
||||
use LIT\MCP\Log;
|
||||
use Throwable;
|
||||
@@ -62,7 +64,8 @@ final class DiviCheckRoute
|
||||
if (self::lock()) {
|
||||
try {
|
||||
$report = ($this->check)()->run(get_current_user_id());
|
||||
($this->reports ?? new CheckReportStore())->save($report);
|
||||
$reports = $this->reports ?? new CheckReportStore(DiviProfile::ID, DiviProfile::REPORT_VERSION_KEY);
|
||||
$reports->save($report);
|
||||
ReleaseGate::invalidate();
|
||||
$notice = 'divi_checked';
|
||||
} catch (Throwable $e) {
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Integrations\Divi\Release;
|
||||
|
||||
use Closure;
|
||||
use LIT\MCP\Compatibility\CheckStep;
|
||||
use LIT\MCP\Compatibility\CompatibilityCheck;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
/** The check steps only Divi has (module catalog, format round trip, write test) and their labels. */
|
||||
final class DiviCheckSteps
|
||||
{
|
||||
public const CATALOG = 'catalog';
|
||||
public const FORMAT = 'format';
|
||||
public const WRITE = 'write';
|
||||
|
||||
/** Labels of the Divi-only steps; manifest and mirrors come from CheckStep::genericLabel(). */
|
||||
private const LABELS = [
|
||||
self::CATALOG => 'Modulkatalog (Kurzform-Pfade)',
|
||||
self::FORMAT => 'Format im Speicher (Parser und Serializer)',
|
||||
self::WRITE => 'Schreibtest auf einer Wegwerfseite',
|
||||
];
|
||||
|
||||
/** @param list<string> $problems */
|
||||
public static function step(string $key, array $problems, bool $skipped = false): CheckStep
|
||||
{
|
||||
return new CheckStep($key, $problems, $skipped, self::LABELS[$key] ?? null);
|
||||
}
|
||||
|
||||
/**
|
||||
* The Profile::$extraSteps of Divi: module catalog, format round trip and the write test. The write test is
|
||||
* skipped only when Divi functions are missing (it would fail anyway), not when only fingerprints changed – so
|
||||
* --update-reference can see whether everything else still works.
|
||||
*
|
||||
* @param Closure(): list<string> $catalog Short-form problems against Divi's live catalog.
|
||||
* @param Closure(): list<string> $probe WriteProbe::run().
|
||||
* @return Closure(bool): list<CheckStep>
|
||||
*/
|
||||
public static function extra(Closure $catalog, FormatCheck $format, Closure $probe): Closure
|
||||
{
|
||||
return static fn (bool $manifestClean): array => [
|
||||
self::step(self::CATALOG, CompatibilityCheck::guarded($catalog)),
|
||||
self::step(self::FORMAT, CompatibilityCheck::guarded(static fn (): array => $format->problems())),
|
||||
$manifestClean
|
||||
? self::step(self::WRITE, CompatibilityCheck::guarded($probe))
|
||||
: self::step(self::WRITE, [], true),
|
||||
];
|
||||
}
|
||||
|
||||
public static function label(CheckStep $step): string
|
||||
{
|
||||
// Reports saved before labels were stored have none.
|
||||
return $step->label
|
||||
?? self::LABELS[$step->key]
|
||||
?? CheckStep::genericLabel($step->key, DiviProfile::LABEL)
|
||||
?? $step->key;
|
||||
}
|
||||
}
|
||||
@@ -4,24 +4,49 @@ declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Integrations\Divi\Release;
|
||||
|
||||
use Closure;
|
||||
use LIT\MCP\Compatibility\Profile;
|
||||
use LIT\MCP\Compatibility\Tier;
|
||||
use LIT\MCP\Integrations\Divi\Schema\DiviModules;
|
||||
use LIT\MCP\Integrations\Divi\Schema\ShortformCheck;
|
||||
use LIT\MCP\Integrations\WordPress\Content\ContentComposer;
|
||||
use LogicException;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
/** Divi as a compatibility profile: every version needs a release, the matrix comes from the release gate. */
|
||||
final class DiviProfile
|
||||
{
|
||||
public static function profile(): Profile
|
||||
public const ID = 'divi';
|
||||
public const LABEL = 'Divi';
|
||||
/** Stored reports and release rows keep this key, so data from before the generic layer stays readable. */
|
||||
public const REPORT_VERSION_KEY = 'divi_version';
|
||||
|
||||
/** @param (Closure(): ContentComposer)|null $composer The write test's composer; without it that step fails. */
|
||||
public static function profile(?Closure $composer = null): Profile
|
||||
{
|
||||
return new Profile(
|
||||
'divi',
|
||||
'Divi',
|
||||
Tier::ReleasePerVersion,
|
||||
DiviFeature::cases(),
|
||||
array_values(DiviAdapters::manifests()),
|
||||
id: self::ID,
|
||||
label: self::LABEL,
|
||||
tier: Tier::ReleasePerVersion,
|
||||
capabilities: DiviFeature::cases(),
|
||||
manifests: array_values(DiviAdapters::manifests()),
|
||||
active: static fn (): bool => DiviEnvironment::version() !== null,
|
||||
version: DiviEnvironment::version(...),
|
||||
matrix: static fn () => ReleaseGate::status()->matrix
|
||||
matrix: static fn () => ReleaseGate::status()->matrix,
|
||||
testedVersions: DiviReleases::TESTED,
|
||||
stateKey: ReleaseResolver::stateKey(...),
|
||||
extraSteps: DiviCheckSteps::extra(
|
||||
static fn (): array => DiviModules::isAvailable()
|
||||
? ShortformCheck::problems((new DiviModules())->catalog())
|
||||
: ['Divi 5 ist in dieser Anfrage nicht geladen.'],
|
||||
new FormatCheck(),
|
||||
static fn (): array => (new WriteProbe(
|
||||
$composer === null ? throw new LogicException('no composer for the write test') : $composer()
|
||||
))->run()
|
||||
),
|
||||
manifestCheck: DiviManifestCheck::create(...),
|
||||
reportVersionKey: self::REPORT_VERSION_KEY
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ namespace LIT\MCP\Integrations\Divi\Release;
|
||||
use Closure;
|
||||
use LIT\MCP\Compatibility\CompatibilityMatrix;
|
||||
use LIT\MCP\Compatibility\ManifestCheck;
|
||||
use LIT\MCP\Compatibility\ReleaseStore;
|
||||
use LIT\MCP\Compatibility\Problem;
|
||||
use LIT\MCP\Plugin;
|
||||
|
||||
@@ -53,7 +54,8 @@ final class ReleaseResolver
|
||||
if (in_array($version, DiviReleases::TESTED, true)) {
|
||||
return ReleaseStatus::of($version, self::matrix($problems, CompatibilityMatrix::SOURCE_CODE, false));
|
||||
}
|
||||
$entry = $version === '' ? null : ($this->store ?? new ReleaseStore())->find($version, Plugin::VERSION);
|
||||
$store = $this->store ?? new ReleaseStore(DiviProfile::ID, DiviProfile::REPORT_VERSION_KEY);
|
||||
$entry = $version === '' ? null : $store->find($version, Plugin::VERSION);
|
||||
if ($entry !== null) {
|
||||
return ReleaseStatus::of(
|
||||
$version,
|
||||
|
||||
@@ -1,99 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Integrations\Divi\Release;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
/**
|
||||
* Divi versions released on this site after a green admin check (spec §5.3, source "database"). An entry holds for
|
||||
* the plugin version that ran the check only.
|
||||
*
|
||||
* Not final so the resolver can be tested with a mock.
|
||||
*/
|
||||
class ReleaseStore
|
||||
{
|
||||
public const OPTION = 'lit_mcp_divi_releases';
|
||||
|
||||
/**
|
||||
* @return list<array{divi_version: string, plugin_version: string, user_id: int, released_at: string,
|
||||
* report_id: string}>
|
||||
*/
|
||||
public function entries(): array
|
||||
{
|
||||
$raw = get_option(self::OPTION, []);
|
||||
$entries = [];
|
||||
foreach (is_array($raw) ? $raw : [] as $row) {
|
||||
if (
|
||||
is_array($row)
|
||||
&& is_string($row['divi_version'] ?? null)
|
||||
&& is_string($row['plugin_version'] ?? null)
|
||||
&& is_int($row['user_id'] ?? null)
|
||||
&& is_string($row['released_at'] ?? null)
|
||||
&& is_string($row['report_id'] ?? null)
|
||||
) {
|
||||
$entries[] = [
|
||||
'divi_version' => $row['divi_version'],
|
||||
'plugin_version' => $row['plugin_version'],
|
||||
'user_id' => $row['user_id'],
|
||||
'released_at' => $row['released_at'],
|
||||
'report_id' => $row['report_id'],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $entries;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{divi_version: string, plugin_version: string, user_id: int, released_at: string,
|
||||
* report_id: string}|null
|
||||
*/
|
||||
public function find(string $diviVersion, string $pluginVersion): ?array
|
||||
{
|
||||
foreach ($this->entries() as $entry) {
|
||||
if ($entry['divi_version'] === $diviVersion && $entry['plugin_version'] === $pluginVersion) {
|
||||
return $entry;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function add(string $diviVersion, string $pluginVersion, int $userId, string $reportId): void
|
||||
{
|
||||
$entries = $this->without($diviVersion, $pluginVersion);
|
||||
$entries[] = [
|
||||
'divi_version' => $diviVersion,
|
||||
'plugin_version' => $pluginVersion,
|
||||
'user_id' => $userId,
|
||||
'released_at' => gmdate('c'),
|
||||
'report_id' => $reportId,
|
||||
];
|
||||
update_option(self::OPTION, $entries, false);
|
||||
}
|
||||
|
||||
public function withdraw(string $diviVersion, string $pluginVersion): bool
|
||||
{
|
||||
if ($this->find($diviVersion, $pluginVersion) === null) {
|
||||
return false;
|
||||
}
|
||||
update_option(self::OPTION, $this->without($diviVersion, $pluginVersion), false);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<array{divi_version: string, plugin_version: string, user_id: int, released_at: string,
|
||||
* report_id: string}>
|
||||
*/
|
||||
private function without(string $diviVersion, string $pluginVersion): array
|
||||
{
|
||||
return array_values(array_filter(
|
||||
$this->entries(),
|
||||
static fn (array $entry): bool => $entry['divi_version'] !== $diviVersion
|
||||
|| $entry['plugin_version'] !== $pluginVersion
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ use LIT\MCP\Abilities\Errors;
|
||||
use LIT\MCP\Integrations\Divi\Pages\DiviPages;
|
||||
use LIT\MCP\Integrations\WordPress\Content\ContentComposer;
|
||||
use LIT\MCP\Integrations\WordPress\Render\PageRenderer;
|
||||
use LIT\MCP\Log;
|
||||
use Throwable;
|
||||
use WP_Error;
|
||||
use WP_Post;
|
||||
@@ -101,7 +102,9 @@ class WriteProbe
|
||||
try {
|
||||
$problems = $this->exercise($id);
|
||||
} catch (Throwable $e) {
|
||||
$problems = ['Schreibtest abgebrochen: ' . $e->getMessage()];
|
||||
// The report reaches MCP clients through the lock reason; the exception text stays in the log.
|
||||
Log::failure('divi write test', $e);
|
||||
$problems = ['Schreibtest abgebrochen (Details im PHP-Error-Log).'];
|
||||
} finally {
|
||||
if (! self::delete($id)) {
|
||||
$problems[] = sprintf(
|
||||
|
||||
Reference in New Issue
Block a user