From fea75b03d6b8dd9a9d38294076a85f084242df76 Mon Sep 17 00:00:00 2001 From: Jan-Niclas Loosen Date: Sat, 26 Sep 2026 01:28:18 +0200 Subject: [PATCH] docs: name tools and error codes the way the client sees them Indirect abilities are written lit/ (via mcp-adapter-execute-ability) in guides, descriptions and hints, direct tools lit-; guides use the client-visible codes (write_failed, partial_write) and say exactly which tools return a revision_id. ToolNamingTest keeps guides and sources consistent with Ability::isDirect() and checks the overview's direct-tool list. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01At3V6SjwbMe6eMzVGW3Hs7 --- docs/guides/content-types.md | 40 ++++--- docs/guides/overview.md | 9 +- docs/guides/workflows.md | 8 +- src/Abilities/Errors.php | 5 +- .../Menus/MenuAssignLocationAbility.php | 4 +- .../WordPress/Menus/MenuCreateAbility.php | 6 +- .../WordPress/Menus/MenuGetAbility.php | 4 +- .../WordPress/Menus/MenuSetItemsAbility.php | 8 +- .../WordPress/Menus/MenuWriter.php | 7 +- .../Revisions/RevisionRestoreAbility.php | 2 +- .../Revisions/RevisionsListAbility.php | 2 +- .../Settings/WpSettingsGetAbility.php | 2 +- .../Settings/WpSettingsUpdateAbility.php | 7 +- .../WordPress/Status/PostDuplicateAbility.php | 2 +- .../Status/PostReplaceWithCopyAbility.php | 14 +-- tests/Unit/Abilities/ErrorsTest.php | 3 +- tests/Unit/Abilities/WriteFailedTest.php | 4 +- tests/Unit/Guides/ToolNamingTest.php | 111 ++++++++++++++++++ .../WordPress/Menus/MenuWriterTest.php | 3 +- 19 files changed, 186 insertions(+), 55 deletions(-) create mode 100644 tests/Unit/Guides/ToolNamingTest.php diff --git a/docs/guides/content-types.md b/docs/guides/content-types.md index da6fa91..4c38efa 100644 --- a/docs/guides/content-types.md +++ b/docs/guides/content-types.md @@ -1,5 +1,8 @@ # Inhalte, Felder und Status +Werkzeugnamen: `lit-…` sind direkt aufrufbare Tools; `lit/…` sind Abilities, die über +`mcp-adapter-execute-ability` (Name in `ability_name`, Eingaben in `parameters`) aufgerufen werden. + ## Inhaltstypen erkennen - `lit-content-types` ohne Parameter listet alle Inhaltstypen (Seiten, Beiträge, eigene Typen) mit @@ -32,20 +35,27 @@ - Begriffe: `terms: {"category": [4, "Neuer Begriff"]}` ersetzt die Zuordnung dieser Taxonomie (IDs oder Namen; unbekannte Namen werden angelegt, falls erlaubt). - Felder leeren: Wert `null`. Pflichtfelder können nicht geleert werden. -- Beziehungsfelder erwarten Eintrags-IDs, Datei-/Bildfelder Anhang-IDs aus der Mediathek. +- Beziehungsfelder erwarten IDs von Einträgen, die der Nutzer lesen darf; Datei-/Bildfelder Anhang-IDs aus der + Mediathek. +- Einträge im Papierkorb werden nicht geändert (auch nicht veröffentlicht, geplant oder wiederhergestellt); + zuerst in WordPress aus dem Papierkorb holen. +- Ist ein geplanter Eintrag über seinen Veröffentlichungszeitpunkt hinaus (verpasster Termin), würde jedes + Speichern ihn sofort veröffentlichen – `lit-content-update`, `lit/revision-restore` und + `lit/post-replace-with-copy` verweigern das dann. Mit dem Nutzer klären: jetzt veröffentlichen + (`lit-post-publish`) oder neu planen (`lit/post-schedule`). ## Status -- Veröffentlichen, Zurückziehen und Planen sind eigene Werkzeuge (`lit-post-publish`, `lit-post-unpublish`, - `lit-post-schedule` mit `date` = „JJJJ-MM-TT HH:MM“ in der Zeitzone der Website). Nur auf ausdrücklichen +- Veröffentlichen, Zurückziehen und Planen sind eigene Werkzeuge (`lit-post-publish`, `lit/post-unpublish`, + `lit/post-schedule` mit `date` = „JJJJ-MM-TT HH:MM“ in der Zeitzone der Website). Nur auf ausdrücklichen Wunsch des Nutzers verwenden. -- `lit-post-trash` legt in den Papierkorb (nie endgültig) und braucht `confirm: true` nach Rückfrage. +- `lit/post-trash` legt in den Papierkorb (nie endgültig) und braucht `confirm: true` nach Rückfrage. ## Sicher überarbeiten -1. `lit-post-duplicate` → Entwurfskopie; die Antwort enthält die `rev` der Kopie für `lit-content-update`. +1. `lit/post-duplicate` → Entwurfskopie; die Antwort enthält die `rev` der Kopie für `lit-content-update`. 2. Kopie mit `lit-content-update` (oder den Divi-Tools) bearbeiten und dem Nutzer die Vorschau zeigen. -3. `lit-post-replace-with-copy` übernimmt die Kopie ins Original (ID, URL, Status und Menüeinträge bleiben). +3. `lit/post-replace-with-copy` übernimmt die Kopie ins Original (ID, URL, Status und Menüeinträge bleiben). Voraussetzungen: die Kopie muss noch Entwurf oder ausstehend sein, `rev` des Originals aus `lit-content-get` ist Pflicht (sie deckt Titel, Inhalt, Auszug und Status ab – Felder/Meta, die direkt am Original geändert wurden, werden dabei überschrieben), und erst nach Rückfrage mit `confirm: true` @@ -53,14 +63,16 @@ Papierkorb (`backup_id` in der Antwort); danach wandert die Kopie ebenfalls in den Papierkorb. Ist der Papierkorb der Website deaktiviert, wird der Austausch verweigert, statt die Kopie endgültig zu löschen. -Jede Änderung liefert `revision_id` (Stand vorher) und `log_id`. Rückgängig: `lit-revisions-list` → -`lit-revision-restore` mit der `rev` des Eintrags (aus `lit-content-get`) und `confirm: true`. Revisionen -umfassen Titel, Inhalt und Auszug – Felder und Begriffe nicht. +Jede Änderung liefert `log_id`. Eine `revision_id` (Revision mit dem Stand vorher) liefern nur +`lit-content-update`, `lit/post-replace-with-copy` und `lit/revision-restore`; bei allen anderen Tools ist sie +`null`. Rückgängig: `lit/revisions-list` → `lit/revision-restore` mit der `rev` des Eintrags (aus +`lit-content-get`) und `confirm: true`. Revisionen umfassen Titel, Inhalt und Auszug – Felder und Begriffe +nicht (dafür bei `lit/post-replace-with-copy` die Sicherung `backup_id`). ## Wenn ein Schreibvorgang fehlschlägt -Schlägt ein Schreibvorgang nach ersten Änderungen fehl, meldet das Tool `lit_write_failed` (alles wurde -zurückgenommen, nichts wurde gespeichert) oder `lit_partial_write` (die Rücknahme war unvollständig). Beide -Fehler stecken als JSON (`code`, `message`, `hint`, ggf. weitere Felder) in der Fehlermeldung. Bei -`lit_partial_write` vor einem erneuten Versuch immer erst den aktuellen Stand prüfen (`lit-content-get`, -`lit-menu-get` o. Ä.) statt den Aufruf einfach zu wiederholen – dem `hint` folgen. +Fehler stecken als JSON (`code`, `message`, `hint`, ggf. weitere Felder) in der Fehlermeldung. Schlägt ein +Schreibvorgang nach ersten Änderungen fehl, ist `code` = `write_failed` (alles wurde zurückgenommen, nichts +wurde gespeichert) oder `partial_write` (die Rücknahme war unvollständig). Bei `partial_write` vor einem +erneuten Versuch immer erst den aktuellen Stand prüfen (`lit-content-get`, +`lit/menu-get` o. Ä.) statt den Aufruf einfach zu wiederholen – dem `hint` folgen. diff --git a/docs/guides/overview.md b/docs/guides/overview.md index 2f40e7b..71ada6d 100644 --- a/docs/guides/overview.md +++ b/docs/guides/overview.md @@ -20,7 +20,8 @@ des verbundenen WordPress-Benutzers. Direkt sichtbar: `lit-guide`, `lit-site-status`, `lit-content-types`, `lit-content-query`, `lit-content-get`, `lit-content-create`, `lit-content-update`, `lit-post-publish`. -Über `mcp-adapter-discover-abilities` / `mcp-adapter-execute-ability` (Ability-Namen `lit/…`): +Über `mcp-adapter-discover-abilities` / `mcp-adapter-execute-ability` (Ability-Namen `lit/…`; in Guides und +Hinweisen immer so geschrieben): Status (`lit/post-unpublish`, `lit/post-schedule`, `lit/post-trash`), Kopien (`lit/post-duplicate`, `lit/post-replace-with-copy`), Revisionen (`lit/revisions-list`, `lit/revision-restore`), Menüs (`lit/menu-list`, `lit/menu-get`, `lit/menu-create`, `lit/menu-set-items`, `lit/menu-assign-location`), @@ -30,6 +31,8 @@ Weitere Themen: `lit-guide` mit `topic` = `content-types` oder `workflows`. ## Rückmeldungen -- Erfolg: `ok`, `summary`, `changed`, `warnings`, `revision_id` (Stand vorher), `log_id`, URLs. -- Fehler: JSON mit `code`, `message`, `hint` (und ggf. `fields`). Dem `hint` folgen. +- Erfolg: `ok`, `summary`, `changed`, `warnings`, `rev`, `revision_id` (Revision mit dem Stand vorher, nur bei + Tools, die Inhalt überschreiben, sonst `null`), `log_id`, URLs. +- Fehler: JSON mit `code` (z. B. `validation_failed`, `rev_mismatch`, `write_failed`, `partial_write`), + `message`, `hint` (und ggf. `fields`). Dem `hint` folgen. - `confirm_required`: Nutzer fragen, dann mit `confirm: true` wiederholen. diff --git a/docs/guides/workflows.md b/docs/guides/workflows.md index 8b06d5e..25cd116 100644 --- a/docs/guides/workflows.md +++ b/docs/guides/workflows.md @@ -3,18 +3,18 @@ ## Neue Website aufsetzen 1. `lit-site-status` und `lit-content-types` lesen. -2. Grundeinstellungen: `lit-wp-settings-get`, dann `lit-wp-settings-update` (Titel, Untertitel, Sprache, +2. Grundeinstellungen: `lit/wp-settings-get`, dann `lit/wp-settings-update` (Titel, Untertitel, Sprache, Zeitzone, Datumsformat, Permalinks `/%postname%/`). `previous` in der Antwort enthält die alten Werte. 3. Seiten als Entwürfe anlegen (`lit-content-create`), Hierarchie über `parent`. 4. Startseite: Seite veröffentlichen (nach Rückfrage), dann `{"settings": {"page_on_front": , "show_on_front": "page"}}`. -5. Menü: `lit-menu-create` (optional mit `location` aus `lit-menu-list`), Einträge mit `lit-menu-set-items`. +5. Menü: `lit/menu-create` (optional mit `location` aus `lit/menu-list`), Einträge mit `lit/menu-set-items`. 6. Veröffentlichen erst, wenn der Nutzer es ausdrücklich möchte. ## Menüs pflegen -- `lit-menu-get` liefert den Baum. Für größere Umbauten den Baum verändern und vollständig als `items` an - `lit-menu-set-items` senden (bestehende Einträge mit `id`, neue ohne `id`). Wird der Baum unverändert +- `lit/menu-get` liefert den Baum. Für größere Umbauten den Baum verändern und vollständig als `items` an + `lit/menu-set-items` senden (bestehende Einträge mit `id`, neue ohne `id`). Wird der Baum unverändert zurückgeschickt, meldet das Tool „keine Änderung“ und schreibt nichts. - Für kleine Änderungen `operations`: `{op: "add", item, parent_id?, position?}`, `{op: "move", id, parent_id?, position?}`, `{op: "remove", id}` (Kinder eines entfernten Eintrags rücken diff --git a/src/Abilities/Errors.php b/src/Abilities/Errors.php index b98e378..74d5724 100644 --- a/src/Abilities/Errors.php +++ b/src/Abilities/Errors.php @@ -144,8 +144,9 @@ final class Errors trim('Speichern fehlgeschlagen: ' . $reason) . ' Die Änderung konnte nicht vollständig zurückgenommen ' . 'werden. ' . $state, $hint ?? 'Den Aufruf nicht einfach wiederholen. Zuerst den aktuellen Stand prüfen (lit-content-get, bei ' - . 'einem neuen Entwurf lit-content-query) und ggf. über lit-revisions-list den vorherigen Stand ' - . 'wiederherstellen; dem Nutzer Bescheid geben.', + . 'einem neuen Entwurf lit-content-query) und ggf. mit lit/revisions-list und lit/revision-restore ' + . '(über mcp-adapter-execute-ability) den vorherigen Stand wiederherstellen; dem Nutzer Bescheid ' + . 'geben.', 500, $details ); diff --git a/src/Integrations/WordPress/Menus/MenuAssignLocationAbility.php b/src/Integrations/WordPress/Menus/MenuAssignLocationAbility.php index f09c734..1e969e7 100644 --- a/src/Integrations/WordPress/Menus/MenuAssignLocationAbility.php +++ b/src/Integrations/WordPress/Menus/MenuAssignLocationAbility.php @@ -34,7 +34,7 @@ final class MenuAssignLocationAbility implements Ability public function description(): string { - return 'Weist einer Theme-Position (siehe lit-menu-list) ein Menü zu; menu_id 0 entfernt die Zuweisung. ' + return 'Weist einer Theme-Position (siehe lit/menu-list) ein Menü zu; menu_id 0 entfernt die Zuweisung. ' . 'Das vorher zugewiesene Menü steht in previous_menu_id.'; } @@ -86,7 +86,7 @@ final class MenuAssignLocationAbility implements Ability } if ($menuId > 0 && ! is_object(wp_get_nav_menu_object($menuId))) { - return Errors::notFound(sprintf('Menü %d nicht gefunden.', $menuId), 'Menüs über lit-menu-list abrufen.'); + return Errors::notFound(sprintf('Menü %d nicht gefunden.', $menuId), 'Menüs über lit/menu-list abrufen.'); } $previous = MenuLocations::assigned()[$location] ?? 0; diff --git a/src/Integrations/WordPress/Menus/MenuCreateAbility.php b/src/Integrations/WordPress/Menus/MenuCreateAbility.php index 5d713fc..996f8a5 100644 --- a/src/Integrations/WordPress/Menus/MenuCreateAbility.php +++ b/src/Integrations/WordPress/Menus/MenuCreateAbility.php @@ -34,7 +34,7 @@ final class MenuCreateAbility implements Ability public function description(): string { return 'Legt ein leeres Menü an und weist es optional einer Theme-Position zu. Einträge danach mit ' - . 'lit-menu-set-items setzen.'; + . 'lit/menu-set-items setzen.'; } public function inputSchema(): array @@ -43,7 +43,7 @@ final class MenuCreateAbility implements Ability 'type' => 'object', 'properties' => [ 'name' => ['type' => 'string', 'minLength' => 1], - 'location' => ['type' => 'string', 'description' => 'Theme-Position aus lit-menu-list.'], + 'location' => ['type' => 'string', 'description' => 'Theme-Position aus lit/menu-list.'], ], 'required' => ['name'], 'additionalProperties' => false, @@ -88,7 +88,7 @@ final class MenuCreateAbility implements Ability return Errors::invalidInput( 'Menü konnte nicht angelegt werden: ' . $menuId->get_error_message(), ['name' => 'bereits vergeben oder ungültig'], - 'Vorhandene Menüs über lit-menu-list abrufen.' + 'Vorhandene Menüs über lit/menu-list abrufen.' ); } diff --git a/src/Integrations/WordPress/Menus/MenuGetAbility.php b/src/Integrations/WordPress/Menus/MenuGetAbility.php index 8cce38d..3efed74 100644 --- a/src/Integrations/WordPress/Menus/MenuGetAbility.php +++ b/src/Integrations/WordPress/Menus/MenuGetAbility.php @@ -29,7 +29,7 @@ final class MenuGetAbility implements Ability return 'Liefert ein Menü als Baum (id, title, type post/taxonomy/custom, object/object_id, url, ' . 'children). object_id und url sind bei post/taxonomy-Einträgen nur informativ (WordPress leitet ' . 'sie aus dem verlinkten Objekt ab); nur bei custom-Einträgen ist url maßgeblich. Der Baum kann ' - . 'bearbeitet an lit-menu-set-items zurückgegeben werden.'; + . 'bearbeitet an lit/menu-set-items zurückgegeben werden.'; } public function inputSchema(): array @@ -78,7 +78,7 @@ final class MenuGetAbility implements Ability $menuId = (int) ($input['menu_id'] ?? 0); $menu = wp_get_nav_menu_object($menuId); if (! is_object($menu)) { - return Errors::notFound(sprintf('Menü %d nicht gefunden.', $menuId), 'Menüs über lit-menu-list abrufen.'); + return Errors::notFound(sprintf('Menü %d nicht gefunden.', $menuId), 'Menüs über lit/menu-list abrufen.'); } $items = wp_get_nav_menu_items($menuId); diff --git a/src/Integrations/WordPress/Menus/MenuSetItemsAbility.php b/src/Integrations/WordPress/Menus/MenuSetItemsAbility.php index 6d11f12..eb01c5a 100644 --- a/src/Integrations/WordPress/Menus/MenuSetItemsAbility.php +++ b/src/Integrations/WordPress/Menus/MenuSetItemsAbility.php @@ -44,7 +44,7 @@ final class MenuSetItemsAbility implements Ability return 'Setzt die Einträge eines Menüs. Entweder items = vollständiger Zielbaum oder operations = Liste aus ' . '{op: add, item, parent_id?, position?}, {op: move, id, parent_id?, position?}, {op: remove, id} ' . '(Kinder eines entfernten Eintrags rücken an seine Stelle; position 0-basiert, ohne = ans Ende). ' - . 'Der Baum aus lit-menu-get kann bearbeitet unverändert zurückgegeben werden: bestehende Einträge ' + . 'Der Baum aus lit/menu-get kann bearbeitet unverändert zurückgegeben werden: bestehende Einträge ' . 'werden über id erkannt, type/object/object_id und bei post/taxonomy auch url sind nur informativ ' . 'und werden ignoriert; title wird nur bei Änderung zur eigenen Beschriftung, url nur bei custom ' . 'geändert. Neue Einträge ohne id: type post|taxonomy mit object_id oder custom mit url (/pfad, ' @@ -70,7 +70,7 @@ final class MenuSetItemsAbility implements Ability 'items' => [ 'type' => 'array', 'items' => ['type' => 'object'], - 'description' => 'Vollständiger Zielbaum (Knoten wie in lit-menu-get, neue Einträge ohne id).', + 'description' => 'Vollständiger Zielbaum (Knoten wie in lit/menu-get, neue Einträge ohne id).', ], 'operations' => [ 'type' => 'array', @@ -132,7 +132,7 @@ final class MenuSetItemsAbility implements Ability $menuId = (int) ($input['menu_id'] ?? 0); $menu = wp_get_nav_menu_object($menuId); if (! is_object($menu)) { - return Errors::notFound(sprintf('Menü %d nicht gefunden.', $menuId), 'Menüs über lit-menu-list abrufen.'); + return Errors::notFound(sprintf('Menü %d nicht gefunden.', $menuId), 'Menüs über lit/menu-list abrufen.'); } if (array_key_exists('items', $input) === array_key_exists('operations', $input)) { @@ -203,7 +203,7 @@ final class MenuSetItemsAbility implements Ability $after = FollowUp::run( fn () => MenuTree::build(array_values($this->currentItems($menuId))), $this->name() . ' re-read', - 'Das Menü ist gespeichert, der neue Stand konnte aber nicht gelesen werden; mit lit-menu-get abrufen.', + 'Das Menü ist gespeichert, der neue Stand konnte aber nicht gelesen werden; mit lit/menu-get abrufen.', $warnings ); diff --git a/src/Integrations/WordPress/Menus/MenuWriter.php b/src/Integrations/WordPress/Menus/MenuWriter.php index d44073e..c0806b9 100644 --- a/src/Integrations/WordPress/Menus/MenuWriter.php +++ b/src/Integrations/WordPress/Menus/MenuWriter.php @@ -462,9 +462,10 @@ class MenuWriter $cause, $detail, sprintf('Menü %d ist möglicherweise teilweise geändert worden.', $menuId), - 'Den Aufruf nicht einfach wiederholen. Zuerst den aktuellen Stand mit lit-menu-get prüfen und ihn ggf. ' - . 'mit lit-menu-set-items anhand von previous_items (Stand vorher) wiederherstellen; entfernte ' - . 'Einträge müssen dabei neu angelegt werden. Dem Nutzer Bescheid geben.', + 'Den Aufruf nicht einfach wiederholen. Zuerst den aktuellen Stand mit lit/menu-get prüfen und ihn ggf. ' + . 'mit lit/menu-set-items (beide über mcp-adapter-execute-ability) anhand von previous_items (Stand ' + . 'vorher) wiederherstellen; entfernte Einträge müssen dabei neu angelegt werden. Dem Nutzer Bescheid ' + . 'geben.', ['menu_id' => $menuId, 'previous_items' => MenuTree::build(array_values($current))] ); } diff --git a/src/Integrations/WordPress/Revisions/RevisionRestoreAbility.php b/src/Integrations/WordPress/Revisions/RevisionRestoreAbility.php index 32014a9..c5987a0 100644 --- a/src/Integrations/WordPress/Revisions/RevisionRestoreAbility.php +++ b/src/Integrations/WordPress/Revisions/RevisionRestoreAbility.php @@ -146,7 +146,7 @@ final class RevisionRestoreAbility implements Ability $revisionId = PostGuard::id($input['revision_id'] ?? null); $revision = $revisionId === null ? null : get_post($revisionId); if (! $revision instanceof WP_Post || $revision->post_type !== 'revision') { - return Errors::notFound('Revision nicht gefunden.', 'Revisionen über lit-revisions-list abrufen.'); + return Errors::notFound('Revision nicht gefunden.', 'Revisionen über lit/revisions-list abrufen.'); } // field 'revision_id': post_parent 0 (an orphaned revision) is the only id this diff --git a/src/Integrations/WordPress/Revisions/RevisionsListAbility.php b/src/Integrations/WordPress/Revisions/RevisionsListAbility.php index 4bdd893..cf374ec 100644 --- a/src/Integrations/WordPress/Revisions/RevisionsListAbility.php +++ b/src/Integrations/WordPress/Revisions/RevisionsListAbility.php @@ -41,7 +41,7 @@ final class RevisionsListAbility implements Ability { return 'Listet die gespeicherten Revisionen eines Eintrags (neueste zuerst) mit Datum, Autor und ' . 'Textauszug. Revisionen von Divi-Inhalten sind mit divi: true markiert; ihr excerpt ist dann ' - . 'Shortcode-/Block-Markup, kein lesbarer Text. Wiederherstellen mit lit-revision-restore.'; + . 'Shortcode-/Block-Markup, kein lesbarer Text. Wiederherstellen mit lit/revision-restore.'; } public function inputSchema(): array diff --git a/src/Integrations/WordPress/Settings/WpSettingsGetAbility.php b/src/Integrations/WordPress/Settings/WpSettingsGetAbility.php index 71dd857..822c0f7 100644 --- a/src/Integrations/WordPress/Settings/WpSettingsGetAbility.php +++ b/src/Integrations/WordPress/Settings/WpSettingsGetAbility.php @@ -27,7 +27,7 @@ final class WpSettingsGetAbility implements Ability public function description(): string { return 'Liest die freigegebenen WordPress-Einstellungen (Allgemein, Lesen, Diskussion, Permalinks, ' - . 'Website-Icon) mit Typ und Beschreibung. Ändern mit lit-wp-settings-update.'; + . 'Website-Icon) mit Typ und Beschreibung. Ändern mit lit/wp-settings-update.'; } public function inputSchema(): array diff --git a/src/Integrations/WordPress/Settings/WpSettingsUpdateAbility.php b/src/Integrations/WordPress/Settings/WpSettingsUpdateAbility.php index 3f998c3..6ae6c92 100644 --- a/src/Integrations/WordPress/Settings/WpSettingsUpdateAbility.php +++ b/src/Integrations/WordPress/Settings/WpSettingsUpdateAbility.php @@ -44,7 +44,7 @@ final class WpSettingsUpdateAbility implements Ability public function description(): string { - return 'Ändert freigegebene WordPress-Einstellungen (Schlüssel siehe lit-wp-settings-get). Alle Werte ' + return 'Ändert freigegebene WordPress-Einstellungen (Schlüssel siehe lit/wp-settings-get). Alle Werte ' . 'werden vorher geprüft – bei einem Fehler wird nichts gespeichert. previous enthält die alten Werte. ' . 'Gesperrt: admin_email, users_can_register, default_role, siteurl, home.'; } @@ -98,7 +98,7 @@ final class WpSettingsUpdateAbility implements Ability return Errors::invalidInput( 'Einstellungen ungültig; nichts wurde gespeichert.', $errors, - 'Werte gemäß fields korrigieren; aktuelle Werte über lit-wp-settings-get.' + 'Werte gemäß fields korrigieren; aktuelle Werte über lit/wp-settings-get.' ); } @@ -255,7 +255,8 @@ final class WpSettingsUpdateAbility implements Ability $cause, $cause->getMessage() . '; not restored: ' . implode(', ', $stuck), sprintf('Möglicherweise auf dem neuen, nicht vollständigen Stand: %s.', implode(', ', $stuck)), - 'Aktuelle Werte über lit/wp-settings-get prüfen und ggf. erneut auf die gewünschten Werte setzen.', + 'Aktuelle Werte über lit/wp-settings-get (mcp-adapter-execute-ability) prüfen und ggf. erneut auf die ' + . 'gewünschten Werte setzen.', ['keys' => $stuck] ); } diff --git a/src/Integrations/WordPress/Status/PostDuplicateAbility.php b/src/Integrations/WordPress/Status/PostDuplicateAbility.php index 2a8406f..a1a2ff5 100644 --- a/src/Integrations/WordPress/Status/PostDuplicateAbility.php +++ b/src/Integrations/WordPress/Status/PostDuplicateAbility.php @@ -41,7 +41,7 @@ final class PostDuplicateAbility implements Ability { return 'Legt eine Entwurfskopie eines Eintrags an (Inhalt, Felder/Meta, Begriffe, Beitragsbild, Template, ' . 'Divi-Einstellungen). Typischer Ablauf: Kopie bearbeiten, prüfen, dann mit ' - . 'lit-post-replace-with-copy ins Original übernehmen – so bleibt die veröffentlichte Seite bis dahin ' + . 'lit/post-replace-with-copy ins Original übernehmen – so bleibt die veröffentlichte Seite bis dahin ' . 'unverändert. Die Antwort enthält die rev der Kopie für lit-content-update.'; } diff --git a/src/Integrations/WordPress/Status/PostReplaceWithCopyAbility.php b/src/Integrations/WordPress/Status/PostReplaceWithCopyAbility.php index 31675db..268a8fb 100644 --- a/src/Integrations/WordPress/Status/PostReplaceWithCopyAbility.php +++ b/src/Integrations/WordPress/Status/PostReplaceWithCopyAbility.php @@ -60,7 +60,7 @@ final class PostReplaceWithCopyAbility implements Ability public function description(): string { - return 'Übernimmt Titel, Inhalt, Felder/Meta und Begriffe einer mit lit-post-duplicate erstellten Kopie ' + return 'Übernimmt Titel, Inhalt, Felder/Meta und Begriffe einer mit lit/post-duplicate erstellten Kopie ' . 'ins Original. ID, URL, Status und Menüeinträge des Originals bleiben; vorher entsteht eine ' . 'Revision und eine vollständige Sicherung des Originals (im Papierkorb, backup_id), die Kopie kommt ' . 'danach in den Papierkorb. Die Kopie muss Entwurf oder ausstehend sein. rev des Originals aus ' @@ -165,8 +165,8 @@ final class PostReplaceWithCopyAbility implements Ability if ($copy->post_type !== $original->post_type || $copyOf !== $originalId || $copyId === $originalId) { return Errors::invalidInput( 'Die Kopie gehört nicht zu diesem Original.', - ['copy_id' => sprintf('keine mit lit-post-duplicate erstellte Kopie von %d', $originalId)], - 'Kopie mit lit-post-duplicate aus diesem Original erstellen.' + ['copy_id' => sprintf('keine mit lit/post-duplicate erstellte Kopie von %d', $originalId)], + 'Kopie mit lit/post-duplicate aus diesem Original erstellen.' ); } @@ -174,7 +174,7 @@ final class PostReplaceWithCopyAbility implements Ability return Errors::invalidInput( sprintf('Die Kopie hat den Status "%s"; übernommen wird nur ein Entwurf.', $copy->post_status), ['copy_id' => 'Kopie muss Entwurf oder ausstehend sein'], - 'Mit lit-post-duplicate eine neue Kopie erstellen (bzw. die Kopie in WordPress wiederherstellen).' + 'Mit lit/post-duplicate eine neue Kopie erstellen (bzw. die Kopie in WordPress wiederherstellen).' ); } @@ -290,9 +290,9 @@ final class PostReplaceWithCopyAbility implements Ability $backupId ), 'Den Aufruf nicht einfach wiederholen. Zuerst den Stand mit lit-content-get prüfen; Titel, Inhalt ' - . 'und Auszug lassen sich mit lit/revision-restore (revision_id) zurückholen, Felder/Meta und ' - . 'Begriffe stehen in der Sicherung backup_id (in WordPress aus dem Papierkorb holen). Dem ' - . 'Nutzer Bescheid geben.', + . 'und Auszug lassen sich mit lit/revision-restore (revision_id, über ' + . 'mcp-adapter-execute-ability) zurückholen, Felder/Meta und Begriffe stehen in der Sicherung ' + . 'backup_id (in WordPress aus dem Papierkorb holen). Dem Nutzer Bescheid geben.', $details ); } diff --git a/tests/Unit/Abilities/ErrorsTest.php b/tests/Unit/Abilities/ErrorsTest.php index 20fbd43..8a63e63 100644 --- a/tests/Unit/Abilities/ErrorsTest.php +++ b/tests/Unit/Abilities/ErrorsTest.php @@ -76,7 +76,8 @@ final class ErrorsTest extends TestCase self::assertStringContainsString('Eintrag 5', $payload['message']); self::assertStringContainsString('Revision 811', $payload['message']); self::assertStringContainsString('lit-content-get', $payload['hint']); - self::assertStringContainsString('lit-revisions-list', $payload['hint']); + self::assertStringContainsString('lit/revisions-list', $payload['hint']); + self::assertStringContainsString('mcp-adapter-execute-ability', $payload['hint']); self::assertStringContainsString('nicht einfach wiederholen', $payload['hint']); self::assertSame(5, $payload['post_id']); self::assertSame(811, $payload['revision_id']); diff --git a/tests/Unit/Abilities/WriteFailedTest.php b/tests/Unit/Abilities/WriteFailedTest.php index d0728ab..a42a209 100644 --- a/tests/Unit/Abilities/WriteFailedTest.php +++ b/tests/Unit/Abilities/WriteFailedTest.php @@ -36,14 +36,14 @@ final class WriteFailedTest extends TestCase 'Menü nicht gespeichert.', false, state: 'Das Menü ist teilweise geändert.', - hint: 'Mit lit-menu-get prüfen.', + hint: 'Mit lit/menu-get prüfen.', details: ['menu_id' => 3] ))->toError(); self::assertSame('lit_partial_write', $error->get_error_code()); self::assertStringContainsString('Das Menü ist teilweise geändert.', $error->get_error_message()); self::assertStringNotContainsString('Entwurf', $error->get_error_message()); - self::assertSame('Mit lit-menu-get prüfen.', $error->get_error_data()['hint']); + self::assertSame('Mit lit/menu-get prüfen.', $error->get_error_data()['hint']); self::assertSame(3, $error->get_error_data()['menu_id']); } diff --git a/tests/Unit/Guides/ToolNamingTest.php b/tests/Unit/Guides/ToolNamingTest.php new file mode 100644 index 0000000..e836747 --- /dev/null +++ b/tests/Unit/Guides/ToolNamingTest.php @@ -0,0 +1,111 @@ +, indirect ones as lit/ (reached via mcp-adapter-execute-ability), + * and error codes without the internal lit_ prefix of the WP_Error code. + */ +final class ToolNamingTest extends TestCase +{ + /** @return array Ability name without "lit/" => isDirect(). */ + private static function abilities(): array + { + $root = dirname(__DIR__, 3); + $abilities = []; + $files = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($root . '/src')); + foreach ($files as $file) { + if (! str_ends_with((string) $file, 'Ability.php')) { + continue; + } + + $class = 'LIT\\MCP\\' . str_replace(['/', '.php'], ['\\', ''], substr((string) $file, strlen($root . '/src/'))); + $reflection = new ReflectionClass($class); + if ($reflection->isInterface() || ! $reflection->implementsInterface(Ability::class)) { + continue; + } + + /** @var Ability $ability */ + $ability = $reflection->newInstanceWithoutConstructor(); + $abilities[substr($ability->name(), strlen('lit/'))] = $ability->isDirect(); + } + + ksort($abilities); + + return $abilities; + } + + /** @return array Path => content of the guides and all PHP sources. */ + private static function texts(): array + { + $root = dirname(__DIR__, 3); + $texts = []; + foreach (glob($root . '/docs/guides/*.md') ?: [] as $file) { + $texts[$file] = (string) file_get_contents($file); + } + + $files = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($root . '/src')); + foreach ($files as $file) { + if (str_ends_with((string) $file, '.php')) { + $texts[(string) $file] = (string) file_get_contents((string) $file); + } + } + + return $texts; + } + + public function testIndirectToolsAreNeverNamedLikeDirectOnes(): void + { + $abilities = self::abilities(); + self::assertArrayHasKey('menu-set-items', $abilities); + + foreach (self::texts() as $path => $text) { + preg_match_all('/\blit-([a-z]+(?:-[a-z]+)*)/', $text, $matches); + foreach ($matches[1] as $name) { + self::assertFalse( + ($abilities[$name] ?? true) === false, + sprintf('%s names the indirect tool lit/%s as lit-%s', basename($path), $name, $name) + ); + } + + preg_match_all('#\blit/([a-z]+(?:-[a-z]+)*)#', $text, $matches); + foreach ($matches[1] as $name) { + self::assertFalse( + ($abilities[$name] ?? false) === true && str_ends_with($path, '.md'), + sprintf('%s names the direct tool lit-%s as lit/%s', basename($path), $name, $name) + ); + } + } + } + + public function testGuidesUseTheErrorCodesTheClientSees(): void + { + foreach (self::texts() as $path => $text) { + if (str_ends_with($path, '.md')) { + self::assertDoesNotMatchRegularExpression('/`lit_(?!mcp)[a-z_]+`/', $text, basename($path)); + } + } + } + + public function testOverviewListsExactlyTheDirectTools(): void + { + $overview = (string) file_get_contents(dirname(__DIR__, 3) . '/docs/guides/overview.md'); + preg_match('/Direkt sichtbar:(.*?)\n\n/s', $overview, $section); + preg_match_all('/`lit-([a-z-]+)`/', $section[1] ?? '', $listed); + + $direct = array_keys(array_filter(self::abilities())); + $listedNames = $listed[1]; + sort($listedNames); + + self::assertSame($direct, $listedNames); + } +} diff --git a/tests/Unit/Integrations/WordPress/Menus/MenuWriterTest.php b/tests/Unit/Integrations/WordPress/Menus/MenuWriterTest.php index 207d8d6..b36fcb8 100644 --- a/tests/Unit/Integrations/WordPress/Menus/MenuWriterTest.php +++ b/tests/Unit/Integrations/WordPress/Menus/MenuWriterTest.php @@ -360,7 +360,8 @@ final class MenuWriterTest extends TestCase self::assertSame('lit_partial_write', $error->get_error_code()); self::assertSame(3, $error->get_error_data()['menu_id']); self::assertSame('Home', $error->get_error_data()['previous_items'][0]['title']); - self::assertStringContainsString('lit-menu-get', $error->get_error_data()['hint']); + self::assertStringContainsString('lit/menu-get', $error->get_error_data()['hint']); + self::assertStringContainsString('mcp-adapter-execute-ability', $error->get_error_data()['hint']); } } }