- lit/divi-node-get now also returns the post-level rev (Rev::ofPost, same
optimistic lock lit-content-update needs) next to tree_rev, and reports a
node's type as the short name ("button"), matching lit-divi-outline instead
of the namespaced "divi/button". description() notes that $variable(...)$
values shown are re-validated when written, never taken over unchecked.
- lit/divi-outline validates its optional `id` input against the same
path-ID pattern lit-divi-node-get uses (a malformed id is now a validation
error, not a "not found"), and its outputSchema's per-node `required` list
now includes info/presets/children, which are always present.
- NodeSummary::label() (the admin label shown in outlines and node-get)
whitespace-normalizes and bounds its result via Text::shorten(), like every
other summary string, instead of returning a raw, unbounded value.
- docs/superpowers/notes/.../etappe-2a-abnahme.md corrected: only a global
module is "nur lesbar" - a specialty section is a normal, editable
divi/section block; the outline just marks it "[specialty]".
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
lit/divi-catalog's path_kinds previously named one kind per whole attribute
path, built from Divi's metadata component name alone. For a composite path
(Divi groups several named sub-fields, "subName", under one path – e.g.
buttonOne.innerContent holds "text" and "linkUrl") that was wrong: the whole
path got one guessed kind (or none), even though the sub-fields can differ
(fullwidth-header's buttonOne/buttonTwo.innerContent -> "text", though it
also carries a "linkUrl"; image.innerContent -> "image", though the value
also carries alt/id/titleText the metadata never names on its own).
AttrPathIndex::kinds() now resolves a composite path to a map
subName => kind instead, preferring Divi's features.dynamicContent.type over
the (often shared/generic) field component for a named sub-field – this is
what tells a button's plain text sub-field apart from its linkUrl sub-field,
both of which use the same generic "divi/text" component. A plain (non-
composite) path is unaffected. Any subName the metadata never names (or that
disagrees between candidates) is therefore simply absent from the map, and
ModuleCatalog::pathKind() defaults it, like every unresolved path, to
"unknown" - the strictest routing for 2b's sanitizer, never a guess.
ModuleCatalog::pathKind() gains an optional $subName parameter to read a
composite path's map. docs/guides/divi-attrs.md documents the new shape and
that lit-divi-catalog without `module` lists all modules.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Final fix wave for stage 2a, load-bearing for 2b's writers:
- Block::setAttrs()/Container::insertChild()/removeChild() refuse on an opaque
block (undecodable attrs or a global module) instead of silently corrupting it.
- Container::append() is now @internal (Parser only) and refuses on a void
block, so a child can no longer be silently dropped from the serialized output.
- Block::create() validates the block name against namespace/block-name.
- Json::decodeObject() returns null (-> opaque) for a top-level object whose
keys are a plain list ("0","1",...) rather than returning [] and letting the
Serializer re-encode it as a corrupting JSON array.
- AttrMap::set() rejects empty path segments and keeps an existing stdClass
value an object when rewrapping it (no silent object->list flip); AttrMap::get()
is updated in step so it can still read back through such an object.
- Block::copy() deep-clones stdClass attribute values instead of sharing them.
- Json::encode()'s existing JsonException-on-invalid-UTF-8 behaviour is documented.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Review fix round 1 for lit/page-render-summary: extract shared Text::shorten()
(reused by NodeSummary), replace data: URIs with a short marker, prefer
data-src/data-lazy-src over placeholder src, dedup nested p/li/blockquote
text and ignore script/style/noscript content, restore every setup_postdata
global (not just $post), drain output buffers robustly around the_content,
and disclose that rendering can trigger oEmbed/network requests.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01At3V6SjwbMe6eMzVGW3Hs7
Review fix round 1 on the lit/divi-catalog task:
- AttrPathIndex::collectKinds() no longer records a kind from a
component.type === "group" node (a composite of several sub-kinds,
e.g. button.innerContent = text + link + target), and kinds() now
drops any attrName with more than one distinct candidate kind
instead of last-write-wins (e.g. imageIcon.innerContent had onoff +
icon + image sub-fields sharing one attrName). Both previously
produced a wrong-but-present kind, which is more dangerous for 2b's
sanitization routing than "unknown". Verified against the committed
blurb/button/fullwidth-header fixture entries and live against the
full 100-module catalog.
- AttrPathIndex::fromDefaults() now builds on AttrMap::flatten()
instead of a parallel walker.
- ModuleCatalog::shortName() replaces the duplicated
substr($name, strlen('divi/')) in DiviCatalogAbility.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01At3V6SjwbMe6eMzVGW3Hs7
Adds per-path attribute kind (text/richtext/code/url/image/color/enum/
onoff/number/list/icon), derived from Divi's field component metadata
where an attribute path names one unambiguously (module.paths() itself
is unchanged; kinds() is a new, separate lookup exposed as path_kinds
on lit/divi-catalog's module detail). This is not in the task-4 brief;
it is added because 2b's sanitization routing (ruling R3) needs a
metadata-derived field kind per attribute path. Verified live against
the full 100-module catalog (564/4358 paths resolve a kind; the rest
default to "unknown" and fall back to 2b's key-name routing).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01At3V6SjwbMe6eMzVGW3Hs7
Task 14 (rulings P17/P18, Q10):
- lit/menu-get returns menu_rev (Rev::ofMenu over id, parent, position,
title, url, object, object_id); lit/menu-set-items requires it, refuses
a stale one with rev_mismatch (hint: lit/menu-get) and returns the new
menu_rev. Errors::revMismatch takes an optional message/hint.
- GuideRepository: filter lit_mcp_guides adds/overrides topics; invalid
topic names and non-string content are ignored, files stay the default.
- lit/post-trash names front page, posts page and linking menus in the
confirmation and as warnings; lit/menu-assign-location needs confirm
to remove an assigned menu and names it; lit/post-schedule warns that
a private entry becomes public; Divi hints no longer point to
non-existing lit-divi-* tools.
- PostGuard::missedSchedule: 60 s publish window, zero GMT date falls
back to get_gmt_from_date(post_date); post-schedule refuses dates
within that window.
- write_failed after a clean rollback that keeps a revision/backup says
"Die Aenderung wurde nicht gespeichert (... bleibt erhalten)" via the
new state parameter (Errors::writeFailed / WriteFailed rolledBackState).
- wp-basis-smoke: menu_rev flow, unassign confirm, trash/menu and
private-schedule warnings.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01At3V6SjwbMe6eMzVGW3Hs7
Deactivation now clears the endpoint-check transient and the cached rewrite
rules (forcing WordPress to regenerate them without this plugin's routes on
the next request) and activation asks mcp-oauth to re-flush its own rules.
uninstall.php removes all own options/transient/cron/table across every site
of a network, delegating to the new self-contained LIT\MCP\Uninstaller class
so it works without the plugin's autoloader and is unit-testable on its own.
mcp-oauth's data (JWT secret, rewrite marker, refresh-token meta, application
passwords) is left untouched, as is every post/attachment/revision.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01At3V6SjwbMe6eMzVGW3Hs7
ChangeLog gains query()/abilities()/clear() (filters: ability, user,
result, date range - all optional and additive). LogTab/LogListTable
render the "Protokoll" admin tab: WP_List_Table with pagination, links
to the post/attachment/nav_menu edit screen by object_type, a revision
link shown only when the user may edit the revision's parent post, and
a nonce+manage_options-gated "Protokoll leeren" with a JS confirmation
and highlighted 'partial' rows.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01At3V6SjwbMe6eMzVGW3Hs7
Fix round 1 (task 10 review):
- FieldValidator's "unknown field" validation error (reaches the
client via validation_failed's fields) and PostPresenter's
DIVI_HINT (surfaced in the result's hints array) still named
lit-content-types/lit-content-update as plain lit-<name>; converted
both to the neutral "lit/<name> (direkt: lit-<name>)" form like the
other hints from task 10. Re-grepped src/ for any other client-facing
error/field/hints text naming a direct-by-default tool as lit-<name>
(descriptions/schemas excluded per Q11c) - none left.
- Added Limits::bounds() (field => [min, max]) and made LimitsTab use
it for the number inputs' min/max instead of a second, hardcoded
copy of the same bounds.
- LimitsTab now shows a description note when the configured upload_mb
exceeds wp_max_upload_size() (PHP's upload_max_filesize/post_max_size),
since WordPress would refuse a larger upload before this limit ever
applies.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01At3V6SjwbMe6eMzVGW3Hs7
Adds Einstellungen → LIT MCP → Tools (direct/indirect/disabled per
ability, only ExposurePolicy-allowed modes offered) and → Limits
(upload size, log retention, snapshot count), wired into Plugin.php.
Ruling Q11b (carried from task 6): overview.md notes that direct
visibility is configurable; hints in src/ that name a tool which is
direct by default but can be switched to indirect (content-get,
content-query, content-types, post-publish, media-search) now use the
neutral "lit/<name> (direkt: lit-<name>)" form so they stay correct
after an admin override. lit/guide (always direct) and the
media-preview/-download DIRECT_ONLY tools are unaffected.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01At3V6SjwbMe6eMzVGW3Hs7
Introduces ExposurePolicy (option lit_mcp_exposure) deciding per ability
whether it is a direct MCP tool, only reachable via mcp-adapter's
execute-ability meta-tool, or disabled entirely (not registered with the
Abilities API at all). Ability::isDirect() remains the default when no
override applies. lit/guide can never be disabled or made indirect;
lit/media-preview and lit/media-download (image/embedded-resource results)
can never be made indirect, only direct or disabled.
AbilityRegistrar and ServerRegistrar take an optional ExposurePolicy and
use it to filter registered abilities / compute the direct tool list.
IntegrationRegistry::directAbilityNames() is removed (superseded by
ExposurePolicy::directNames()). InstructionsBuilder now also takes the
policy and lists the currently direct tools by name (lit-<name>), noting
the rest are reachable via mcp-adapter-execute-ability as lit/<name>.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01At3V6SjwbMe6eMzVGW3Hs7
MediaUsageFinder treats a failed query (null result or last_error) as a possible
usage and an incomplete scan, so media-delete refuses without force. A refused
wp_delete_attachment() is write_failed only when the attachment is intact,
otherwise partial_write. media-update: re-read hint on write_failed, normalized
ids in failures, documented lock resolution and kses on restore.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01At3V6SjwbMe6eMzVGW3Hs7
lit/media-update validates the whole batch first (edit_post, lengths, optional
modified lock) and restores already written items when a write fails midway.
lit/media-delete requires confirm, refuses used media without force and
reports refused deletes as write_failed.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01At3V6SjwbMe6eMzVGW3Hs7
Empty effective whitelist is rejected instead of falling back to all types; the
whitelist is matched per extension against core's keys (mp3|m4a|m4b); a file moved
into uploads is deleted when the attachment cannot be created; the import request
resolves IPv4 only; empty temp name guard for URL imports.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01At3V6SjwbMe6eMzVGW3Hs7
MediaImporter streams URL imports via wp_safe_remote_get with limit_response_size,
checks Content-Length, intersects the MIME whitelist with get_allowed_mime_types and
validates real content; base64 is size-checked before decoding. Server-side failures
are write_failed; a failed alt text deletes the new attachment again (or partial_write).
MediaUploads is the shared permission/result helper of both upload abilities.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01At3V6SjwbMe6eMzVGW3Hs7
lit/media-preview downscales an image attachment via wp_get_image_editor
and returns it as ImageContent; lit/media-download returns a file (full
or a named size) as an embedded resource, rejecting files above the
upload limit. Both are direct tools (ruling Q6: embedded resources, like
ImageContent, only become content blocks for tools called directly).
Adds MediaFiles::isWithinUploads() so neither tool ever reads a file
outside wp_upload_dir()'s basedir.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01At3V6SjwbMe6eMzVGW3Hs7
I1: query the attachments themselves author-restricted when the user
lacks edit_others_posts (PostGuard::ownPostsOnly, extracted from
ContentQueryAbility so content-query reuses the exact same rule)
instead of relying solely on a post-query editable() filter, which
could leak the existence of other users' attachments via an empty
page plus next_cursor. editable() stays as a backstop.
Also: M1 document that only status=inherit attachments are listed;
M2 tie-break ordering by ID (content-query and media-search) for
stable pagination; M3 no next_cursor once the last candidate was
examined; M4 reject an explicit non-image mime with missing_alt;
M5 additional coverage (posts_per_page, cursor offset pass-through,
invalid cursor, post_status/date/mime args); mime maxLength 100.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01At3V6SjwbMe6eMzVGW3Hs7
Ruling Q8 caps the unused scan at 20 candidates per call (not the
brief's 500/50), and each result (search and unused alike) is
filtered to attachments the current user may edit; overview.md lists
media-search as a direct tool (ruling Q2).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01At3V6SjwbMe6eMzVGW3Hs7
Adds widgets and header/background theme mods, integer galleryIds,
wider pre-filter, regex failures as possible usages, newest-first
candidates, and usages_complete/usages_truncated in media-get.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01At3V6SjwbMe6eMzVGW3Hs7
Usage search distinguishes certain (ID referenced) and possible (file URL
only) usages, hides titles the user cannot read, and keeps queries bounded.
MediaGuard checks per-attachment capabilities and refuses trashed media
unless the caller opts in.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01At3V6SjwbMe6eMzVGW3Hs7
Indirect abilities are written lit/<name> (via mcp-adapter-execute-ability) in
guides, descriptions and hints, direct tools lit-<name>; 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) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01At3V6SjwbMe6eMzVGW3Hs7
- isStored() compares the stored option with sanitize_option() of the new value
(core esc_html()s blogname/blogdescription): setting "Müller & Söhne" again
no longer looks like a failed write; lit/wp-settings-get and previous return
those two decoded, as the user would type them.
- The change log runs as FollowUp (warning on failure); a failed write throws
WriteFailed (write_failed / partial_write with keys), partial writes are
logged with result partial.
- permalink_structure needs a real %tag% (WordPress' own /%[^/%]+%/ rule).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01At3V6SjwbMe6eMzVGW3Hs7
Fields from the meta/sample sources need edit_post_meta (update, checked in
PostWriter::prepare) or add_post_meta (create, checked right after the insert;
a refusal deletes the new draft again) for their key; a refusal is a
fields.<key> validation error.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01At3V6SjwbMe6eMzVGW3Hs7
publish/unpublish/schedule/trash run their WordPress call through StatusWrite:
a throwing hook becomes WriteFailed (rollback incomplete, with post_id) and is
logged with result partial instead of surfacing as internal_error.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01At3V6SjwbMe6eMzVGW3Hs7
after_save hook, re-read, change log and ToolResult (rev, log_id, warnings)
were repeated in nine post abilities; they now share one helper. Behaviour
and warning texts are unchanged.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01At3V6SjwbMe6eMzVGW3Hs7
- PostGuard::editable() refuses trashed entries unless the caller opts in
(content-get, revisions-list, post-trash); publish/schedule/unpublish/update/
restore/duplicate/replace now refuse them.
- PostGuard::statusStable(): content-update, revision-restore and
replace-with-copy refuse a future post whose date has passed (any save would
publish it); the content-update rollback and PostCloner row writes re-check.
- PostCloner::ownMeta() treats a failed SELECT as an error; replace() reads the
copy before its first write.
- replace-with-copy needs create_posts (backup clone) and reports backup_id in
write_failed/partial_write.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01At3V6SjwbMe6eMzVGW3Hs7
recordPartial() replaces the partial-log FollowUp blocks in content-create,
content-update and post-replace-with-copy; record() truncates ability,
object_type and object_id to their column sizes.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01At3V6SjwbMe6eMzVGW3Hs7