Context
Sub-B2 of #13221 (NL mutation undo — Slice-1) — the second half of create/remove capture (Sub-B1 = create, #13261/#13264 merged). gpt cross-family-vetted the Sub-B design (guardrail 2: position-preservation). Completes the create/remove set so the merged undo tool (#13259) reverts a removal cleanly.
Seam + Design (gpt-vetted)
remove_component forwards server-side as call_method component.destroy(true); the app-side InstanceService.callMethod captures the reverse — but before the destroy (the config/parent/index must be snapshotted before the component is gone):
- Server-stamped marker:
ComponentService.removeComponent stamps undoKind: 'remove_component' on its call_method dispatch (NOT in CallMethodRequest's schema — the generic call_method forwards only {id, method, args}, stripping any injected marker).
- App-side pre-destroy capture: on marker + canonical
destroy(true) + writer identity + non-replay, snapshot parentId + the child's index (parent.indexOf) + a JSON-safe config (toJSON); reverse = call_method insert(index, config) on the parent — position-preserving (guardrail 2: add() appends → tree-order drift, so re-create via insert(index, …), not append).
toJSON bound: a documented JSON-safe serializable-config snapshot — no full-live-state-fidelity claim (TransactionService's data-not-code + payload caps bound it).
Acceptance Criteria
Out of Scope
- A live-bridge / two-agent e2e (the in-process round-trip is the proof here).
Related
Parent: #13221 (Slice-1). Sub-B1: #13261/#13264 (merged). Undo tool: #13259. remove_component: #9847. Pillar-2: #13012. Slice-tracking: #9848.
Authored by @neo-opus-vega (Claude Opus 4.8, Claude Code). Origin Session ID: 4cc428e3-cf36-4324-8646-1b96cb23fa4a.
Context
Sub-B2 of #13221 (NL mutation undo — Slice-1) — the second half of create/remove capture (Sub-B1 = create, #13261/#13264 merged). gpt cross-family-vetted the Sub-B design (guardrail 2: position-preservation). Completes the create/remove set so the merged
undotool (#13259) reverts a removal cleanly.Seam + Design (gpt-vetted)
remove_componentforwards server-side ascall_methodcomponent.destroy(true); the app-sideInstanceService.callMethodcaptures the reverse — but before the destroy (the config/parent/index must be snapshotted before the component is gone):ComponentService.removeComponentstampsundoKind: 'remove_component'on itscall_methoddispatch (NOT inCallMethodRequest's schema — the genericcall_methodforwards only{id, method, args}, stripping any injected marker).destroy(true)+ writer identity + non-replay, snapshotparentId+ the child's index (parent.indexOf) + a JSON-safe config (toJSON); reverse =call_methodinsert(index, config)on the parent — position-preserving (guardrail 2:add()appends → tree-order drift, so re-create viainsert(index, …), not append).toJSONbound: a documented JSON-safe serializable-config snapshot — no full-live-state-fidelity claim (TransactionService's data-not-code + payload caps bound it).Acceptance Criteria
remove_componentserver-stampsundoKind(not in the publicCallMethodRequestschema).callMethodcaptures remove's reverse (=insert(index, config)on the parent) before the destroy, when marked + canonical (destroy(true)) + agent-attributed; fail-closed on mismatch; suppressed onundoReplay.undo→ the child is re-inserted at its original index with its config; transaction consumed.call_methoddestroy(no marker) is NOT captured.ComponentService.specserver-side dispatch test asserts theremove_componentmarker — and is run (the Sub-B1 missed-server-spec lesson).Out of Scope
Related
Parent: #13221 (Slice-1). Sub-B1: #13261/#13264 (merged). Undo tool: #13259.
remove_component: #9847. Pillar-2: #13012. Slice-tracking: #9848.Authored by @neo-opus-vega (Claude Opus 4.8, Claude Code). Origin Session ID: 4cc428e3-cf36-4324-8646-1b96cb23fa4a.