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
+13 -20
View File
@@ -7,6 +7,7 @@ namespace LIT\MCP\Integrations\Media;
use LIT\MCP\Abilities\Ability;
use LIT\MCP\Abilities\Errors;
use LIT\MCP\Abilities\FollowUp;
use LIT\MCP\Abilities\Rollback;
use LIT\MCP\Abilities\ToolResult;
use LIT\MCP\Abilities\WriteFailed;
use LIT\MCP\ChangeLog\ChangeLog;
@@ -266,24 +267,20 @@ final class MediaUpdateAbility implements Ability
$unrestored = [];
$failures = [];
foreach (array_reverse($written) as $entry) {
try {
$this->writer->restore($entry['snapshot']);
} catch (Throwable $e) {
$failed = Rollback::attempt(fn () => $this->writer->restore($entry['snapshot']));
if ($failed !== []) {
$unrestored[] = $entry['attachment'];
$failures[] = $e->getMessage();
$failures = array_merge($failures, $failed);
}
}
$detail = 'Media update failed: ' . $cause->getMessage();
if ($unrestored === []) {
throw new WriteFailed(
throw WriteFailed::rolledBack(
self::REASON,
true,
null,
null,
$cause,
$detail,
rolledBackHint: 'Das Zurücksetzen hat modified der Medien geändert: vor einem neuen Versuch die Medien '
hint: 'Das Zurücksetzen hat modified der Medien geändert: vor einem neuen Versuch die Medien '
. 'neu lesen (lit/media-get oder lit/media-search, direkt: lit-media-search) und den Stapel mit '
. 'dem aktuellen modified senden; tritt der Fehler wieder auf, den Administrator informieren '
. '(Details im PHP-Error-Log).'
@@ -301,17 +298,14 @@ final class MediaUpdateAbility implements Ability
);
}
throw new WriteFailed(
throw WriteFailed::partial(
self::REASON,
false,
null,
null,
$cause,
$detail . '; rollback incomplete: ' . implode('; ', $failures),
sprintf('Möglicherweise geändert: Medien %s (siehe changed_ids).', implode(', ', $ids)),
'Den Aufruf nicht einfach wiederholen. Die unter changed_ids genannten Medien mit lit/media-get prüfen, '
. 'die Metadaten ggf. korrigieren und dem Nutzer Bescheid geben.',
['changed_ids' => $ids]
state: sprintf('Möglicherweise geändert: Medien %s (siehe changed_ids).', implode(', ', $ids)),
hint: 'Den Aufruf nicht einfach wiederholen. Die unter changed_ids genannten Medien mit lit/media-get '
. 'prüfen, die Metadaten ggf. korrigieren und dem Nutzer Bescheid geben.',
details: ['changed_ids' => $ids]
);
}
@@ -374,12 +368,11 @@ final class MediaUpdateAbility implements Ability
*/
private static function failure(int $index, array $item, WP_Error $error): array
{
$payload = json_decode($error->get_error_message(), true);
return [
'index' => $index,
'id' => PostGuard::id($item['id'] ?? null),
'code' => (string) preg_replace('/^lit_/', '', (string) $error->get_error_code()),
'error' => is_array($payload) ? (string) ($payload['message'] ?? '') : $error->get_error_message(),
'code' => Errors::codeOf($error),
'error' => Errors::clientMessage($error),
];
}