feat: wp lit-mcp divi-check with diff and reference update

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-30 02:52:17 +02:00
co-authored by Claude Opus 5.5
parent b441febd81
commit ed806a1cc8
5 changed files with 488 additions and 0 deletions
+192
View File
@@ -0,0 +1,192 @@
<?php
declare(strict_types=1);
namespace LIT\MCP\Cli;
use Closure;
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\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\ReleaseResolver;
use LIT\MCP\Integrations\Divi\Release\SourceFingerprint;
use RuntimeException;
defined('ABSPATH') || exit;
/**
* wp lit-mcp divi-check (spec §7): the admin check from the command line, for a developer checking a new Divi
* version. Never releases anything – a green version goes into DiviReleases::TESTED by hand.
*/
final class DiviCheckCommand
{
/** @var Closure(string): void */
private Closure $out;
/**
* @param Closure(): CompatibilityCheck $check
* @param (Closure(string): void)|null $out null = WP_CLI::line().
* @param (Closure(): ?string)|null $themeDir null = DiviEnvironment::themeDir().
* @param string|null $expectedFile null = DiviReference::FILE.
* @param (Closure(): ?string)|null $sourceDir null = DiviReference::sourceDir().
*/
public function __construct(
private Closure $check,
private ?CheckReportStore $reports = null,
?Closure $out = null,
private ?Closure $themeDir = null,
private ?string $expectedFile = null,
private ?Closure $sourceDir = null
) {
$this->out = $out ?? static function (string $line): void {
\WP_CLI::line($line);
};
}
/** @param Closure(): CompatibilityCheck $check */
public static function register(Closure $check): void
{
if (defined('WP_CLI') && WP_CLI) {
\WP_CLI::add_command('lit-mcp divi-check', new self($check));
}
}
/**
* Prüft die installierte Divi-Version wie der Reiter „Divi“: Manifeste, Fingerabdrücke, Modulkatalog, Format
* und Schreibtest auf einer Wegwerfseite. Gibt nichts frei.
*
* ## OPTIONS
*
* [--diff]
* : Zeigt für jede geänderte nachgebaute Divi-Funktion den Unterschied zur Referenz.
*
* [--update-reference]
* : Schreibt Soll-Fingerabdrücke und Referenzquellen neu – nur wenn alle übrigen Schritte grün sind.
*
* ## EXAMPLES
*
* wp --user=admin lit-mcp divi-check --diff
*
* @param list<string> $args
* @param array<string, mixed> $assoc
*/
public function __invoke(array $args, array $assoc): void
{
$code = $this->run($assoc);
if ($code !== 0) {
\WP_CLI::halt($code);
}
}
/** @param array<string, mixed> $assoc */
public function run(array $assoc): int
{
if (! current_user_can('manage_options')) {
($this->out)('Bitte mit --user=<Administrator> ausführen: der Schreibtest legt eine Seite an.');
return 1;
}
$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));
foreach ($report->steps as $step) {
($this->out)(($step->skipped ? '–' : ($step->green() ? '✓' : '✗')) . ' ' . $step->label());
foreach ($step->problems as $problem) {
($this->out)(' ' . $problem);
}
}
if (isset($assoc['diff'])) {
$this->diff();
}
if (isset($assoc['update-reference'])) {
return $this->updateReference($report);
}
return $report->green() ? 0 : 1;
}
private function diff(): void
{
$themeDir = ($this->themeDir ?? DiviEnvironment::themeDir(...))();
$sourceDir = ($this->sourceDir ?? DiviReference::sourceDir(...))();
$expected = DiviReference::expected($this->expectedFile);
foreach (DiviAdapters::mirrors() as $mirror) {
try {
$print = SourceFingerprint::of(
(string) $themeDir . '/' . $mirror->file,
$mirror->function,
$mirror->class
);
} catch (RuntimeException $e) {
($this->out)(sprintf('%s: nicht lesbar (%s)', $mirror->id, $e->getMessage()));
continue;
}
if (($expected[$mirror->id] ?? null) === $print->hash) {
continue;
}
($this->out)(sprintf(
'%s: %s, Zeilen %d–%d',
$mirror->id,
$mirror->file,
$print->startLine,
$print->endLine
));
$reference = $sourceDir === null ? null : DiviReference::readSource($mirror->id, $sourceDir);
if ($reference === null) {
($this->out)(' keine Referenzquelle (nur in einer Entwicklungskopie mit tests/)');
continue;
}
foreach (LineDiff::lines(explode("\n", rtrim($reference, "\n")), explode("\n", $print->source)) as $line) {
($this->out)(' ' . $line);
}
}
}
private function updateReference(CheckReport $report): int
{
foreach ($report->steps as $step) {
if ($step->key !== CheckStep::MIRRORS && ! $step->green()) {
($this->out)('Referenz nicht aktualisiert: nicht nur Fingerabdrücke sind rot (siehe oben).');
return 1;
}
}
$themeDir = ($this->themeDir ?? DiviEnvironment::themeDir(...))();
try {
$prints = DiviReference::current((string) $themeDir);
} catch (RuntimeException $e) {
($this->out)(
'Referenz nicht aktualisiert – eine nachgebaute Divi-Funktion ist nicht lesbar: ' . $e->getMessage()
);
return 1;
}
DiviReference::writeExpected(
array_map(static fn (SourceFingerprint $print): string => $print->hash, $prints),
$this->expectedFile
);
$sourceDir = ($this->sourceDir ?? DiviReference::sourceDir(...))();
foreach ($sourceDir === null ? [] : $prints as $id => $print) {
DiviReference::writeSource($id, $print->source, $sourceDir);
}
delete_transient(ReleaseResolver::CACHE);
ReleaseGate::forget();
($this->out)(sprintf(
'Referenz aktualisiert (%d Fingerabdrücke). Smoke-Skripte laufen lassen, dann committen.',
count($prints)
));
return 0;
}
}
@@ -0,0 +1,85 @@
<?php
declare(strict_types=1);
namespace LIT\MCP\Integrations\Divi\Release;
defined('ABSPATH') || exit;
/** A small line diff (longest common subsequence) for wp lit-mcp divi-check --diff. */
final class LineDiff
{
/**
* @param list<string> $old
* @param list<string> $new
* @return list<string> " kept", "- removed", "+ added"; "…" between changes more than 2 × $context lines apart.
*/
public static function lines(array $old, array $new, int $context = 2): array
{
$operations = self::operations($old, $new);
$keep = [];
foreach ($operations as $i => [$op]) {
if ($op === ' ') {
continue;
}
for ($j = max(0, $i - $context); $j <= min(count($operations) - 1, $i + $context); ++$j) {
$keep[$j] = true;
}
}
$lines = [];
$last = null;
foreach ($operations as $i => [$op, $line]) {
if (! isset($keep[$i])) {
continue;
}
if ($last !== null && $i > $last + 1) {
$lines[] = '…';
}
$lines[] = $op . ' ' . $line;
$last = $i;
}
return $lines;
}
/**
* @param list<string> $a
* @param list<string> $b
* @return list<array{0: string, 1: string}>
*/
private static function operations(array $a, array $b): array
{
$n = count($a);
$m = count($b);
$lcs = array_fill(0, $n + 1, array_fill(0, $m + 1, 0));
for ($i = $n - 1; $i >= 0; --$i) {
for ($j = $m - 1; $j >= 0; --$j) {
$lcs[$i][$j] = $a[$i] === $b[$j] ? $lcs[$i + 1][$j + 1] + 1 : max($lcs[$i + 1][$j], $lcs[$i][$j + 1]);
}
}
$operations = [];
$i = 0;
$j = 0;
while ($i < $n && $j < $m) {
if ($a[$i] === $b[$j]) {
$operations[] = [' ', $a[$i]];
++$i;
++$j;
} elseif ($lcs[$i + 1][$j] >= $lcs[$i][$j + 1]) {
$operations[] = ['-', $a[$i++]];
} else {
$operations[] = ['+', $b[$j++]];
}
}
while ($i < $n) {
$operations[] = ['-', $a[$i++]];
}
while ($j < $m) {
$operations[] = ['+', $b[$j++]];
}
return $operations;
}
}
+2
View File
@@ -21,6 +21,7 @@ use LIT\MCP\Admin\ToolsTab;
use LIT\MCP\ChangeLog\ChangeLog;
use LIT\MCP\ChangeLog\ChangeLogCleanup;
use LIT\MCP\ChangeLog\ChangeLogInstaller;
use LIT\MCP\Cli\DiviCheckCommand;
use LIT\MCP\Guides\InstructionsBuilder;
use LIT\MCP\Integrations\Core\CoreIntegration;
use LIT\MCP\Integrations\Divi\DiviIntegration;
@@ -140,6 +141,7 @@ final class Plugin
// Spec §6: the admin check runs as a REST request (same context as MCP calls).
$diviCheck = static fn (): CompatibilityCheck => CompatibilityCheck::create($divi->contentComposer());
(new DiviCheckRoute($diviCheck))->register();
DiviCheckCommand::register($diviCheck);
$policy = ExposurePolicy::fromOption()->withGroups($registry->groupMap(...));
(new AbilityRegistrar($registry, $policy, new DiviToolGate($registry->groupMap(...), $changeLog)))->register();