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
+34
View File
@@ -12,6 +12,14 @@ use WP_Error;
* mcp-adapter 0.5 turns a WP_Error into a tool result with isError: true but
* forwards only the message text. The message therefore carries the whole
* structured error as JSON: {code, message, hint, fields?, usages?, …} (spec §8.2).
* clientMessage(), codeOf() and fieldsOf() read such an error back (batch results that list
* per-item errors, merged validation errors).
*
* How failures travel: pure helpers (e.g. the Divi Compose classes) throw
* InvalidArgumentException, which the caller collects into `fields`; abilities return a
* WP_Error from this factory; writes that may have left something behind throw WriteFailed
* (rolledBack()/partial()), which the AbilityRegistrar maps via toError(); internal steps
* throw RuntimeException, which is logged and becomes internal_error.
*/
final class Errors
{
@@ -190,6 +198,32 @@ final class Errors
);
}
/** The client message of an error (its JSON "message"), without the JSON around it. */
public static function clientMessage(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();
}
/** The error code as the client sees it (without the "lit_" prefix of the WP_Error code). */
public static function codeOf(WP_Error $error): string
{
return (string) preg_replace('/^lit_/', '', (string) $error->get_error_code());
}
/**
* The `fields` of a validation error (input path => problem); [] when it has none.
*
* @return array<string, string>
*/
public static function fieldsOf(WP_Error $error): array
{
$data = $error->get_error_data();
return is_array($data) && is_array($data['fields'] ?? null) ? $data['fields'] : [];
}
public static function internal(): WP_Error
{
return self::make(