refactor: ReleaseGate::invalidate() als einzige Stelle zum Zurücksetzen des Freigabe-Zustands; CompatibilityMatrix::locked() und ReleaseGate::lockOf() ersetzen die drei Kopien der Sperrprüfung

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Jan-Niclas Loosen
2026-10-18 22:57:35 +02:00
co-authored by Claude Opus 5.5
parent 7f9a087b8b
commit 63389f35a8
12 changed files with 129 additions and 68 deletions
+2 -2
View File
@@ -160,7 +160,7 @@ final class DiviTab implements AdminTab
}
($this->releases ?? new ReleaseStore())->add($version, Plugin::VERSION, get_current_user_id(), $report->id);
ReleaseGate::forget();
ReleaseGate::invalidate();
$this->log?->record(
'lit/divi-release',
'divi',
@@ -183,7 +183,7 @@ final class DiviTab implements AdminTab
return;
}
ReleaseGate::forget();
ReleaseGate::invalidate();
$this->log?->record(
'lit/divi-release',
'divi',
@@ -15,7 +15,6 @@ 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;
@@ -173,8 +172,7 @@ final class DiviCheckCommand
return ($this->matrix)();
}
delete_transient(ReleaseResolver::CACHE);
ReleaseGate::forget();
ReleaseGate::invalidate(mirrors: true);
return ReleaseGate::status()->matrix;
}
@@ -279,8 +277,7 @@ final class DiviCheckCommand
return 1;
}
delete_transient(ReleaseResolver::CACHE);
ReleaseGate::forget();
ReleaseGate::invalidate(mirrors: true);
($this->out)(sprintf(
'Referenz aktualisiert (%d Fingerabdrücke, %d Signaturen). Smoke-Skripte laufen lassen, dann committen.',
count($prints),
+2 -2
View File
@@ -55,8 +55,8 @@ 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\ReleaseResolver;
use LIT\MCP\Integrations\Divi\Release\FeatureOverview;
use LIT\MCP\Integrations\Divi\Release\ReleaseGate;
use LIT\MCP\Integrations\Divi\Schema\DiviCatalogAbility;
use LIT\MCP\Integrations\Divi\Schema\DiviModules;
use LIT\MCP\Integrations\Divi\ThemeBuilder\DiviThemeBuilder;
@@ -212,7 +212,7 @@ final class DiviIntegration implements Integration, Bootable
{
// Spec §5.4: a theme update checks the Divi fingerprints again at once (the state key would, too).
add_action('upgrader_process_complete', static function (): void {
delete_transient(ReleaseResolver::CACHE);
ReleaseGate::invalidate(mirrors: true);
});
// Spec §6: the admin check runs as a REST request (same context as MCP calls). Its short form has no
@@ -11,7 +11,7 @@ use LIT\MCP\Integrations\Divi\BlockTree\Outline;
use LIT\MCP\Integrations\Divi\BlockTree\Serializer;
use LIT\MCP\Integrations\Divi\Design\DesignSystemReader;
use LIT\MCP\Integrations\Divi\Release\DiviFeature;
use LIT\MCP\Integrations\Divi\Release\DiviFeatureGate;
use LIT\MCP\Integrations\Divi\Release\ReleaseGate;
use LIT\MCP\Integrations\Divi\Schema\DiviModules;
use LIT\MCP\Integrations\WordPress\Content\ContentComposer;
use LIT\MCP\Integrations\WordPress\Schema\ContentSchema;
@@ -81,7 +81,7 @@ final class DiviContentComposer implements ContentComposer
public function refusal(): ?WP_Error
{
return DiviFeatureGate::lockFor(self::FEATURES)?->toError();
return ReleaseGate::lockOf(self::FEATURES)?->toError();
}
public function check(string $postType, array $sections): ?WP_Error
@@ -76,6 +76,27 @@ final class CompatibilityMatrix
return true;
}
/**
* The statuses of those of $features that are not usable – each once, in the order of $features. The one answer
* to "what locks this call" for the gate (DiviFeatureGate), the adapters (ReleaseGate::assertFeatures()) and the
* refusal (ReleaseStatus::refusalFor()).
*
* @param list<DiviFeature> $features
* @return list<FeatureStatus>
*/
public function locked(array $features): array
{
$locked = [];
foreach ($features as $feature) {
$status = $this->of($feature);
if (! $status->usable()) {
$locked[$feature->value] = $status;
}
}
return array_values($locked);
}
/** @return list<FeatureStatus> */
public function unusable(): array
{
@@ -63,7 +63,7 @@ final class DiviCheckRoute
try {
$report = ($this->check)()->run(get_current_user_id());
($this->reports ?? new CheckReportStore())->save($report);
ReleaseGate::forget();
ReleaseGate::invalidate();
$notice = 'divi_checked';
} catch (Throwable $e) {
Log::failure('divi check', $e);
@@ -25,27 +25,9 @@ final class DiviFeatureGate implements AbilityGuard
{
}
/**
* The lock for the usable-ness of $features, or null when every one is usable. Public: the Divi short form of
* lit/content-create asks the same question for its own capabilities (DiviContentComposer::refusal()).
*
* @param list<DiviFeature> $features
*/
public static function lockFor(array $features): ?DiviLocked
{
$unusable = [];
foreach ($features as $feature) {
if (! ReleaseGate::feature($feature)->usable()) {
$unusable[$feature->value] = $feature;
}
}
return $unusable === [] ? null : new DiviLocked(ReleaseGate::status(), array_values($unusable));
}
public function refusal(Ability $ability, array $input): ?WP_Error
{
$lock = self::lockFor(FeatureSets::declaredBy($ability, $input));
$lock = ReleaseGate::lockOf(FeatureSets::declaredBy($ability, $input));
if ($lock === null) {
return null;
}
+34 -12
View File
@@ -51,15 +51,25 @@ final class ReleaseGate
self::$probe = null;
}
/** The next status() computes again (after a check, a release or a withdrawal). */
/** The next status() of this request computes again; the cached mirror results stay (see invalidate()). */
public static function forget(): void
{
self::$status = null;
}
public static function feature(DiviFeature $feature): FeatureStatus
/**
* The single way to make the release state current again: forgets the status of this request and, with
* $mirrors, also deletes the cached mirror results (transient ReleaseResolver::CACHE) so the next status()
* fingerprints Divi's code again. Without $mirrors: after a release or a withdrawal and after the admin check
* (only the source of the state changed). With $mirrors: after a theme update, a CLI check or a new reference
* (the code or the reference may have changed).
*/
public static function invalidate(bool $mirrors = false): void
{
return self::status()->matrix->of($feature);
if ($mirrors) {
delete_transient(ReleaseResolver::CACHE);
}
self::forget();
}
/** @throws DiviLocked When $feature is not usable – before anything is written. */
@@ -80,16 +90,28 @@ final class ReleaseGate
return;
}
$lock = self::lockOf($features);
if ($lock !== null) {
throw $lock;
}
}
/**
* The lock of this request's status for the locked ones of $features, or null when every one is usable. Asked by
* DiviFeatureGate before a tool runs and by the Divi short form of lit/content-create (DiviContentComposer::
* refusal()); no probe exception – tool calls never write the probe page.
*
* @param list<DiviFeature> $features
*/
public static function lockOf(array $features): ?DiviLocked
{
$status = self::status();
$locked = [];
foreach ($features as $feature) {
if (! $status->matrix->of($feature)->usable()) {
$locked[$feature->value] = $feature;
}
}
if ($locked !== []) {
throw new DiviLocked($status, array_values($locked));
}
$locked = $status->matrix->locked($features);
return $locked === [] ? null : new DiviLocked(
$status,
array_map(static fn (FeatureStatus $feature): DiviFeature => $feature->feature, $locked)
);
}
public static function whileProbing(int $postId, callable $write): mixed
@@ -77,18 +77,11 @@ final class ReleaseStatus
*/
public function refusalFor(array $features): WP_Error
{
$locked = [];
foreach ($features as $feature) {
$status = $this->matrix->of($feature);
if (! $status->usable()) {
$locked[$feature->value] = $status;
}
}
$locked = $this->matrix->locked($features);
if ($locked === []) {
throw new LogicException('usable features refuse nothing');
}
$locked = array_values($locked);
$incompatible = array_values(array_filter(
$locked,
static fn (FeatureStatus $status): bool => $status->state === ReleaseState::Incompatible