refactor: WriteFailed named constructors, Rollback runner, Errors read-back helpers (H6)

- WriteFailed::rolledBack()/partial() replace the 11-parameter constructor (now
  private); the hint/state of either error are plain $hint/$state.
- Rollback::attempt(...steps) runs every undo step and collects failure messages;
  used by ContentCreate::discard, ContentUpdate::rollBack, MediaUpdate::rollBack and
  DiviSaver::restore.
- Errors::clientMessage/codeOf/fieldsOf replace DiviReplaceTextAbility::reason,
  the JSON digging in MediaUpdateAbility::failure and ContentCreateAbility::fields.
- Errors docblock documents the exception/WP_Error split.
- ContentCreateAbility::discard takes the client reason as a parameter (both callers
  still pass REASON_EXTRAS; changing the Divi text needs a ruling).

Error payloads unchanged.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-26 17:40:32 +02:00
co-authored by Claude Opus 5.5
parent fb6eabf80c
commit 8e5ba9eac4
23 changed files with 358 additions and 207 deletions
@@ -186,7 +186,7 @@ final class DiviReplaceTextAbility implements Ability
$divi = $this->loader->load($postId, true);
$problem = $divi instanceof WP_Error ? $divi : $this->loader->writable($divi);
if ($problem instanceof WP_Error) {
$skipped[] = ['post_id' => $postId, 'reason' => self::reason($problem)];
$skipped[] = ['post_id' => $postId, 'reason' => Errors::clientMessage($problem)];
continue;
}
@@ -320,7 +320,7 @@ final class DiviReplaceTextAbility implements Ability
} catch (WriteFailed $e) {
self::log($postId, $e);
return ['post_id' => $postId, 'ok' => false, 'error' => self::reason($e->toError())];
return ['post_id' => $postId, 'ok' => false, 'error' => Errors::clientMessage($e->toError())];
} catch (Throwable $e) {
self::log($postId, $e);
@@ -343,7 +343,7 @@ final class DiviReplaceTextAbility implements Ability
$postId = $input['post_id'];
$page = $this->writer->page($input);
if ($page instanceof WP_Error) {
return ['post_id' => $postId, 'ok' => false, 'error' => self::reason($page)];
return ['post_id' => $postId, 'ok' => false, 'error' => Errors::clientMessage($page)];
}
$post = $page->post;
@@ -475,14 +475,6 @@ final class DiviReplaceTextAbility implements Ability
: 'Knoten nicht lesbar (Attribute kein JSON-Objekt) – im Divi-Builder bearbeiten';
}
/** The client message of an error (its JSON "message"), without the JSON around it. */
private static function reason(WP_Error $error): string
{
$data = json_decode($error->get_error_message(), true);
return is_array($data) && is_string($data['message'] ?? null) ? $data['message'] : $error->get_error_message();
}
private static function log(int $postId, Throwable $e): void
{
Log::failure(sprintf('lit/divi-replace-text on post %d', $postId), $e);
+15 -16
View File
@@ -6,6 +6,7 @@ namespace LIT\MCP\Integrations\Divi\Pages;
use LIT\MCP\Abilities\Errors;
use LIT\MCP\Abilities\FollowUp;
use LIT\MCP\Abilities\Rollback;
use LIT\MCP\Abilities\WriteFailed;
use LIT\MCP\Integrations\WordPress\PostGuard;
use LIT\MCP\Integrations\WordPress\RevisionPoint;
@@ -78,14 +79,12 @@ class DiviSaver
if ($result instanceof WP_Error || $result === 0) {
$message = $result instanceof WP_Error ? $result->get_error_message() : 'returned 0';
throw new WriteFailed(
throw WriteFailed::rolledBack(
self::REASON,
true,
$postId,
$revisionId,
null,
sprintf('wp_update_post(%d) refused: %s', $postId, $message),
rolledBackState: self::keeping($revisionId)
detail: sprintf('wp_update_post(%d) refused: %s', $postId, $message),
state: self::keeping($revisionId),
postId: $postId,
revisionId: $revisionId
);
}
@@ -172,7 +171,7 @@ class DiviSaver
private function restore(int $postId, string $before, ?int $revisionId, Throwable $cause): never
{
$detail = sprintf('Saving Divi content of post %d failed: %s', $postId, $cause->getMessage());
try {
$failures = Rollback::attempt(function () use ($postId, $before): void {
$current = get_post($postId);
if ($current instanceof WP_Post && PostGuard::missedSchedule($current)) {
throw new RuntimeException('restoring would publish a missed schedule');
@@ -185,19 +184,19 @@ class DiviSaver
if ($restored instanceof WP_Error || $restored === 0) {
throw new RuntimeException('previous content could not be written back');
}
} catch (Throwable $e) {
$detail .= '; rollback: ' . $e->getMessage();
throw new WriteFailed(self::REASON, false, $postId, $revisionId, $cause, $detail);
});
if ($failures !== []) {
$detail .= '; rollback: ' . $failures[0];
throw WriteFailed::partial(self::REASON, $cause, $detail, $postId, $revisionId);
}
throw new WriteFailed(
throw WriteFailed::rolledBack(
self::REASON,
true,
$postId,
$revisionId,
$cause,
$detail,
rolledBackState: self::keeping($revisionId)
state: self::keeping($revisionId),
postId: $postId,
revisionId: $revisionId
);
}