fix(plugin): join plugin-opened undo scopes in XRecord writes

The internal per-call push_undo fractured plugin-composed operations:
history has no nested scopes, so each push finished the plugin's
still-pending scope and opened its own — an operation of several
writes became several undo steps and the plugin's label was lost
(measured: 3 creates + update + delete in one scope = 5 steps).

Follow the entity-write contract instead: a mutation inside a
plugin-opened push_undo scope joins it, so the whole operation is a
single undo step under the plugin's label; a scope-less mutation opens
and finishes its own scope, keeping one undo step per call; refusals
record no undo step in either regime. Document the contract on the
trait methods and the host docs; the composition experiment becomes
the permanent xrecord_writes_join_a_plugin_initiated_undo_scope test.
This commit is contained in:
Sebastian committed 2026-10-11 20:58:41 +02:00
1 parent ca2f0c9857
commit 297a4d3074
4 files changed
+166 -17

No files matched your search

+6 -1
View File
@@ -352,8 +352,13 @@ replaces the entries atomically (handle, owner and name stay, the record's verba
saves emit the new content); delete returns the removed record; list reports only the keys that address
XRecords, in dictionary order. Undo: objects-map mutations are captured by the pending history's full structure
snapshot, so each mutating call is one undo step whose undo restores the objects map byte-exactly, the
auto-created dictionaries included. Evidence: `xrecord_crud_round_trips_refusals_and_list`,
auto-created dictionaries included. Undo composition follows the entity-write contract: a mutation inside a
plugin-opened `push_undo` scope joins that scope (a multi-write operation — entities and XRecords — is a single
undo step under the plugin's label); only a scope-less mutation opens its own step. Evidence:
`xrecord_crud_round_trips_refusals_and_list`,
`xrecord_mutations_are_single_undo_steps_that_restore_objects` (four steps, refusal paths record none),
`xrecord_writes_join_a_plugin_initiated_undo_scope` (3 creates + update + delete + a refusal + an entity write
in one scope = one step, undone and redone whole),
`xrecords_survive_dxf_and_dwg_round_trips` (typed entries reload from both writers), the IPC round-trips
`xrecord_request_and_response_roundtrip` and `xrecord_requests_await_their_typed_responses`. The API is
host-level and stands on its own; a Python mapping (`doc.xrecords`-style) is future work.
+7 -4
View File
@@ -95,10 +95,13 @@ dictionaries are created on create only, so the first call seeds a plugin's
namespace. The payload is the codec's typed `XRecordEntry` pairs — the host puts
no opinion on the content. Names and path fragments are symbol-validated, and
dictionary addressing follows the codec's case-insensitive lookup. Create never
overwrites and update never creates; each refusal is decided before the undo step
is recorded, so a failure changes nothing, and each change is one undo step whose
undo restores the objects map byte-exactly, the auto-created dictionaries
included. `list_xrecords` reports only the keys that address XRecords, in the
overwrites and update never creates; each refusal is decided before the undo
scope is touched, so a failure changes nothing, and each change joins a
plugin-opened `push_undo` scope — the entity-write contract — so an operation
composed of several writes (entities and XRecords) is a single undo step; with
no scope open a change is one undo step of its own, whose undo restores the
objects map byte-exactly, the auto-created dictionaries included.
`list_xrecords` reports only the keys that address XRecords, in the
dictionary's own order, so audits need no prior knowledge. Not yet mapped to
Python.