feat: manifest checks, Divi compatibility from manifests, reference fingerprints
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Integrations\Divi\Release;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
/**
|
||||
* The installed Divi theme: version (the parent's for a child theme) and directory; null when Divi is not the
|
||||
* template.
|
||||
*/
|
||||
final class DiviEnvironment
|
||||
{
|
||||
public const TEMPLATE = 'Divi';
|
||||
|
||||
public static function version(): ?string
|
||||
{
|
||||
$theme = wp_get_theme();
|
||||
if ($theme->get_template() !== self::TEMPLATE) {
|
||||
return null;
|
||||
}
|
||||
$parent = $theme->parent();
|
||||
|
||||
return (string) ($parent ? $parent->get('Version') : $theme->get('Version'));
|
||||
}
|
||||
|
||||
public static function themeDir(): ?string
|
||||
{
|
||||
return wp_get_theme()->get_template() === self::TEMPLATE ? (string) get_template_directory() : null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Integrations\Divi\Release;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
/**
|
||||
* The expected fingerprints of the mirrors (fingerprints.json, shipped with the plugin) and their reference sources
|
||||
* (tests/Fixtures/divi/reference/<id>.txt, development only – not in the plugin package). Written by
|
||||
* wp lit-mcp divi-check --update-reference.
|
||||
*/
|
||||
final class DiviReference
|
||||
{
|
||||
public const FILE = __DIR__ . '/fingerprints.json';
|
||||
|
||||
/** @return array<string, string> Mirror ID => sha256; [] when the file is missing or broken. */
|
||||
public static function expected(?string $file = null): array
|
||||
{
|
||||
$file ??= self::FILE;
|
||||
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- local plugin file.
|
||||
$raw = is_file($file) && is_readable($file) ? file_get_contents($file) : false;
|
||||
$data = is_string($raw) ? json_decode($raw, true) : null;
|
||||
if (! is_array($data)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$expected = [];
|
||||
foreach ($data as $id => $hash) {
|
||||
if (is_string($id) && is_string($hash)) {
|
||||
$expected[$id] = $hash;
|
||||
}
|
||||
}
|
||||
ksort($expected);
|
||||
|
||||
return $expected;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, string> $hashes Mirror ID => sha256.
|
||||
* @throws RuntimeException
|
||||
*/
|
||||
public static function writeExpected(array $hashes, ?string $file = null): void
|
||||
{
|
||||
ksort($hashes);
|
||||
// phpcs:ignore WordPress.WP.AlternativeFunctions.json_encode_json_encode -- pure PHP, no WordPress.
|
||||
$json = json_encode($hashes, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
|
||||
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents -- dev fixture.
|
||||
if (! is_string($json) || file_put_contents($file ?? self::FILE, $json . "\n") === false) {
|
||||
throw new RuntimeException('fingerprints.json not written');
|
||||
}
|
||||
}
|
||||
|
||||
/** tests/Fixtures/divi/reference of a development checkout; null in an installed plugin. */
|
||||
public static function sourceDir(): ?string
|
||||
{
|
||||
$dir = dirname(__DIR__, 4) . '/tests/Fixtures/divi/reference';
|
||||
|
||||
return is_dir($dir) ? $dir : null;
|
||||
}
|
||||
|
||||
public static function readSource(string $id, string $dir): ?string
|
||||
{
|
||||
$file = $dir . '/' . $id . '.txt';
|
||||
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- local dev fixture.
|
||||
$source = is_file($file) ? file_get_contents($file) : false;
|
||||
|
||||
return is_string($source) ? $source : null;
|
||||
}
|
||||
|
||||
/** @throws RuntimeException */
|
||||
public static function writeSource(string $id, string $source, string $dir): void
|
||||
{
|
||||
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents -- dev fixture.
|
||||
if (file_put_contents($dir . '/' . $id . '.txt', $source . "\n") === false) {
|
||||
throw new RuntimeException(sprintf('reference source %s not written', $id));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fingerprints of every mirror in the installed Divi.
|
||||
*
|
||||
* @return array<string, SourceFingerprint>
|
||||
* @throws RuntimeException When a mirrored function cannot be read.
|
||||
*/
|
||||
public static function current(string $themeDir): array
|
||||
{
|
||||
$prints = [];
|
||||
foreach (DiviAdapters::mirrors() as $mirror) {
|
||||
$file = $themeDir . '/' . $mirror->file;
|
||||
$prints[$mirror->id] = SourceFingerprint::of($file, $mirror->function, $mirror->class);
|
||||
}
|
||||
|
||||
return $prints;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Integrations\Divi\Release;
|
||||
|
||||
use Closure;
|
||||
use LIT\MCP\Integrations\Divi\Schema\DiviModules;
|
||||
use ReflectionFunction;
|
||||
use ReflectionMethod;
|
||||
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,
|
||||
* taxonomies, block types) depend on the request context (stage3 finding 6) and only show in reports.
|
||||
*/
|
||||
final 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 => 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().
|
||||
*/
|
||||
public function __construct(
|
||||
?array $manifests = null,
|
||||
private ?array $expected = null,
|
||||
?Closure $current = null,
|
||||
?Closure $knowsBlock = null
|
||||
) {
|
||||
$this->manifests = $manifests ?? DiviAdapters::manifests();
|
||||
$this->current = $current ?? self::installed(...);
|
||||
$this->knowsBlock = $knowsBlock ?? DiviModules::knowsBlock(...);
|
||||
}
|
||||
|
||||
/** @return array<string, Closure(): bool> */
|
||||
public function codeChecks(): array
|
||||
{
|
||||
$checks = [];
|
||||
foreach ($this->manifests as $manifest) {
|
||||
foreach ($manifest->calls as $call) {
|
||||
$checks[$manifest->area . ': ' . $call->label()] = static fn (): bool => self::accepts($call);
|
||||
}
|
||||
foreach ($manifest->constants as $constant) {
|
||||
$checks[$manifest->area . ': Konstante ' . $constant] = static fn (): bool => defined($constant);
|
||||
}
|
||||
}
|
||||
|
||||
return $checks;
|
||||
}
|
||||
|
||||
/** @return array<string, Closure(): bool> */
|
||||
public function mirrorChecks(): array
|
||||
{
|
||||
$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;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return $checks;
|
||||
}
|
||||
|
||||
/** @return array<string, Closure(): bool> */
|
||||
public function registrationChecks(): array
|
||||
{
|
||||
$checks = [];
|
||||
foreach ($this->manifests as $manifest) {
|
||||
foreach ($manifest->postTypes as $type) {
|
||||
$checks[$manifest->area . ': Post-Typ ' . $type] = static fn (): bool => post_type_exists($type);
|
||||
}
|
||||
foreach ($manifest->taxonomies as $taxonomy) {
|
||||
$checks[$manifest->area . ': Taxonomie ' . $taxonomy] =
|
||||
static fn (): bool => taxonomy_exists($taxonomy);
|
||||
}
|
||||
foreach ($manifest->blockTypes as $block) {
|
||||
$checks[$manifest->area . ': Block-Typ ' . $block] = fn (): bool => ($this->knowsBlock)($block);
|
||||
}
|
||||
}
|
||||
|
||||
return $checks;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, callable(): bool> $checks
|
||||
* @return list<string> Labels of the checks that failed or threw.
|
||||
*/
|
||||
public static function failed(array $checks): array
|
||||
{
|
||||
$failed = [];
|
||||
foreach ($checks as $label => $check) {
|
||||
try {
|
||||
$ok = (bool) $check();
|
||||
} catch (Throwable) {
|
||||
$ok = false;
|
||||
}
|
||||
if (! $ok) {
|
||||
$failed[] = (string) $label;
|
||||
}
|
||||
}
|
||||
|
||||
return $failed;
|
||||
}
|
||||
|
||||
/** 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));
|
||||
}
|
||||
|
||||
private static function installed(Mirror $mirror): ?string
|
||||
{
|
||||
$dir = DiviEnvironment::themeDir();
|
||||
|
||||
return $dir === null
|
||||
? null
|
||||
: SourceFingerprint::of($dir . '/' . $mirror->file, $mirror->function, $mirror->class)->hash;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"library-submit-layout": "594b3843d4c42e1a91e5eda06f108c79583a0036e46c92dd749f4f541e362d02",
|
||||
"tb-api-save": "7ba452f4d73c9b676281ca6ddcfddd669502965db868bf478ef7804937e5bfc4",
|
||||
"tb-load-setting-options": "2ea0a6925a1da5522489a417104b384b2bd77a16b25f4122b9503873a4c8a24e",
|
||||
"tb-store-template": "3bf4ac6028ed99ed8ac3523c659392a5fce7818ef8a80ea226e01f5404eb441d",
|
||||
"vb-sync-flags": "f8e85421197b0f823219dad6081af7e47f9b4e388c45e162e9188f892766fde9"
|
||||
}
|
||||
Reference in New Issue
Block a user