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:
Jan-Niclas Loosen
2026-11-06 12:51:03 +01:00
co-authored by Claude Opus 5.5
parent c85131603b
commit 7d6a97a54e
48 changed files with 1469 additions and 647 deletions
+111
View File
@@ -0,0 +1,111 @@
<?php
declare(strict_types=1);
namespace LIT\MCP\Compatibility;
defined('ABSPATH') || exit;
/**
* Versions of one profile released on this site after a green admin check (spec §5.3, source "database"), option
* lit_mcp_<profile>_releases. 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
{
/** @param string $versionKey Row key of the version (Divi: 'divi_version', so stored rows stay as they are). */
public function __construct(private string $profileId, private string $versionKey = 'version')
{
}
public static function forProfile(Profile $profile): self
{
return new self($profile->id, $profile->reportVersionKey);
}
/**
* @return list<array<string, string|int>> Rows with the keys <versionKey>, plugin_version, user_id, released_at,
* report_id.
*/
public function entries(): array
{
$raw = get_option($this->option(), []);
$entries = [];
foreach (is_array($raw) ? $raw : [] as $row) {
if (
is_array($row)
&& is_string($row[$this->versionKey] ?? 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[] = [
$this->versionKey => $row[$this->versionKey],
'plugin_version' => $row['plugin_version'],
'user_id' => $row['user_id'],
'released_at' => $row['released_at'],
'report_id' => $row['report_id'],
];
}
}
return $entries;
}
/**
* @return array<string, string|int>|null
*/
public function find(string $version, string $pluginVersion): ?array
{
foreach ($this->entries() as $entry) {
if ($entry[$this->versionKey] === $version && $entry['plugin_version'] === $pluginVersion) {
return $entry;
}
}
return null;
}
public function add(string $version, string $pluginVersion, int $userId, string $reportId): void
{
$entries = $this->without($version, $pluginVersion);
$entries[] = [
$this->versionKey => $version,
'plugin_version' => $pluginVersion,
'user_id' => $userId,
'released_at' => gmdate('c'),
'report_id' => $reportId,
];
update_option($this->option(), $entries, false);
}
public function withdraw(string $version, string $pluginVersion): bool
{
if ($this->find($version, $pluginVersion) === null) {
return false;
}
update_option($this->option(), $this->without($version, $pluginVersion), false);
return true;
}
/**
* @return list<array<string, string|int>> Rows with the keys <versionKey>, plugin_version, user_id, released_at,
* report_id.
*/
private function without(string $version, string $pluginVersion): array
{
return array_values(array_filter(
$this->entries(),
fn (array $entry): bool => $entry[$this->versionKey] !== $version
|| $entry['plugin_version'] !== $pluginVersion
));
}
private function option(): string
{
return 'lit_mcp_' . $this->profileId . '_releases';
}
}