Skip to content

refactor(preview): merge dual preview primitives into unified PendingActionStore lane#1171

Merged
chubes4 merged 4 commits intomainfrom
merge-preview-primitives
Apr 23, 2026
Merged

refactor(preview): merge dual preview primitives into unified PendingActionStore lane#1171
chubes4 merged 4 commits intomainfrom
merge-preview-primitives

Conversation

@chubes4
Copy link
Copy Markdown
Member

@chubes4 chubes4 commented Apr 23, 2026

Summary

Collapses Data Machine's two preview/approve primitives into a single
extensible lane. Everything — content edits, socials publishes, and any
future preview-opting tool — now stages through PendingActionStore and
dispatches via the datamachine_pending_action_handlers filter.

Removed:

  • PendingDiffStore (class)
  • ResolveDiffAbility (class, datamachine/resolve-diff ability, POST /datamachine/v1/diff/resolve REST route, datamachine_diff_resolved action)
  • CanonicalDiffPreview::store_pending() and ::response() helpers
  • The dm_pending_diff_ transient prefix and the diff_ id prefix

Kept, now with unified routing:

  • CanonicalDiffPreview::build() — pure payload builder, now emits actionId (was diffId) so the payload lines up with the staged envelope's top-level action_id.
  • The three content abilities (edit_post_blocks, replace_post_blocks, insert_content) still produce the exact same canonical diff shape — just nested inside preview_data of the PendingAction envelope.

Added:

  • inc/Abilities/Content/ContentActionHandlers.php — registers the three kinds on datamachine_pending_action_handlers with shared edit_post cap gating.
  • PendingActionHelper::stage() now accepts an optional caller-supplied action_id so abilities can embed the id inside preview_data before staging.
  • tests/Unit/Abilities/Content/ContentActionHandlersTest.php — full stage → accept/reject round trip for each kind, plus can_resolve gating.
  • docs/development/hooks/core-filters.md now has a Preview & Approval Filters section covering the four hooks (datamachine_pending_action_handlers, datamachine_pending_action_staged, datamachine_pending_action_resolved, datamachine_tool_action_policy) with a "which preview primitive should I use?" callout.

Why

The two lanes did almost the same thing. PendingDiffStore was a transient
wrapper that only knew about three hardcoded ability types via a closed
switch($type) in ResolveDiffAbility::apply() — not extensible.
PendingActionStore already has the right filter seam
(datamachine_pending_action_handlers). The merger routes the three
content kinds through that filter and retires the switch. One class, one
store, one REST endpoint, one hook — applies to everything going forward.

Breaking changes

This is a hard cutover with no backwards-compatibility shim.

Removed Replacement
\DataMachine\Abilities\Content\PendingDiffStore \DataMachine\Engine\AI\Actions\PendingActionStore
\DataMachine\Abilities\Content\ResolveDiffAbility \DataMachine\Engine\AI\Actions\ResolvePendingActionAbility
datamachine/resolve-diff ability datamachine/resolve-pending-action
POST /datamachine/v1/diff/resolve POST /datamachine/v1/actions/resolve
do_action( 'datamachine_diff_resolved', ... ) do_action( 'datamachine_pending_action_resolved', $decision, $action_id, $kind, $payload, $result )
diff_id / diffId param & field action_id / actionId
dm_pending_diff_* transients dm_pa_* transients

Any in-flight pending diff transient at the moment of upgrade becomes
unresolvable (1-hour TTL). Callers re-run the preview.

Downstream known consumer: Extra-Chill/data-machine-editor. A follow-up
PR on that repo migrates its REST bridge, Gutenberg block attributes, and
TypeScript surface to the new lane.

Testing

  • tests/Unit/Abilities/Content/ContentActionHandlersTest.php — 7 test methods covering:
    • All three kinds register on the filter with callable apply + can_resolve.
    • edit_post_blocks preview → stage envelope → accept → post content updated; transient deleted.
    • edit_post_blocks preview → reject → post untouched; transient deleted.
    • replace_post_blocks and insert_content preview → stage → accept → applied.
    • can_resolve denies users without edit_post; apply never runs; transient preserved.
    • Canonical preview payload exposes actionId (not diffId) and matches top-level action_id.
    • datamachine_pending_action_resolved fires exactly once per resolution.
  • All touched PHP files pass php -l.

AI assistance

  • AI assistance: Yes
  • Tool(s): Claude Code (Opus 4.7)
  • Used for: Drafted the ContentActionHandlers filter registrar, the rewrites to each content ability's preview branch, the PendingActionHelper::stage() action_id passthrough, the docblock/docs updates, and the test suite. Chris reviewed the architectural direction (single-lane unification with no compat shim), the final commit breakdown, and the PR scope boundary (core only; DM-editor migration lands in a separate follow-up PR).

chubes4 added 4 commits April 23, 2026 18:04
…action_handlers

Adds ContentActionHandlers, which wires edit_post_blocks,
replace_post_blocks, and insert_content onto the unified
datamachine_pending_action_handlers filter. Each handler replays the
stored apply_input through the relevant ability's execute() with
preview stripped, and gates resolution with a shared edit_post cap
check.

Also extends PendingActionHelper::stage() to accept a caller-supplied
action_id so abilities can embed the id inside their preview_data
payload before staging.
…fId → actionId

The three content abilities (edit_post_blocks, replace_post_blocks,
insert_content) now stage pending previews through
PendingActionHelper::stage() instead of the dedicated PendingDiffStore.
The same canonical diff payload is built via CanonicalDiffPreview::build()
and nested inside the staged envelope as preview_data, so the Gutenberg
diff block and any other consumer still reads one stable shape.

CanonicalDiffPreview is narrowed to a pure payload builder: the
storage-helper method store_pending() and the response wrapper response()
are gone, and the canonical field is renamed from diffId to actionId to
match the unified id space produced by PendingActionStore::generate_id().

Ability preview responses now expose `action_id` (top-level, from the
staged envelope) and a renamable `is_preview` boolean flag; the old
`preview` boolean key is repurposed to hold the preview_data payload
returned by PendingActionHelper::stage().
…ine_diff_resolved

Full-stack removal of the parallel diff-only preview lane. Everything
that previously routed through PendingDiffStore / ResolveDiffAbility /
datamachine/resolve-diff / POST /datamachine/v1/diff/resolve /
datamachine_diff_resolved now routes through the unified PendingAction
lane added in the previous two commits.

- Deletes inc/Abilities/Content/PendingDiffStore.php.
- Deletes inc/Abilities/Content/ResolveDiffAbility.php (the ability,
  the REST route, and the datamachine_diff_resolved action all go with
  it).
- Drops the corresponding require_once + instantiation lines from the
  plugin bootstrap.
- Adds require_once for inc/Abilities/Content/ContentActionHandlers.php
  so the filter registrations fire at plugin-load time.
- Drops the historical reference to PendingDiffStore from
  PendingActionStore's class docblock (no more successor language — the
  store is the primitive).

No backwards-compatibility shim. In-flight transients under the old
dm_pending_diff_ prefix (1-hour TTL) become unresolvable on upgrade;
the worst case is a user re-running the preview.
Adds a Preview & Approval Filters section to core-filters.md covering
datamachine_pending_action_handlers, datamachine_pending_action_staged,
datamachine_pending_action_resolved, and datamachine_tool_action_policy.
Includes a 'which preview primitive should I use?' callout pointing
integrators at the one lane that now exists.

Also adds tests/Unit/Abilities/Content/ContentActionHandlersTest.php,
a WP_UnitTestCase suite that exercises the full stage → resolve round
trip for each content kind, verifies the actionId canonical field,
confirms rejection leaves content untouched, and checks the
can_resolve gate denies unauthorized users.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant