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

+16 -5
View File
@@ -1100,7 +1100,13 @@ pub trait HostApi {
/// case-insensitive like dictionary lookups) happens **before** any change,
/// so a failure leaves the drawing untouched. The record is registered in
/// its dictionary and owned by it; the returned handle stays stable across
/// updates. The change is one undo step.
/// updates.
///
/// Undo composition — the entity-write contract: a mutation performed
/// inside a plugin-opened [`HostApi::push_undo`] scope joins that scope,
/// so an operation composed of several writes (entities and XRecords) is
/// a single undo step under the plugin's own label. With no scope open
/// the change is one undo step of its own. Refusals record no undo step.
fn create_xrecord(
&mut self,
_dictionary_path: &[&str],
@@ -1131,8 +1137,11 @@ pub trait HostApi {
/// create anything silently. Identity (handle, owning dictionary) stays;
/// the entries and their DWG handle-reference stream are replaced and the
/// record's verbatim raw payloads are dropped, so subsequent saves emit the
/// new content. A refusal leaves the drawing untouched; a successful change
/// is one undo step whose undo restores the previous entries byte-exactly.
/// new content. A refusal leaves the drawing untouched. Undo composition —
/// the entity-write contract: a mutation inside a plugin-opened
/// [`HostApi::push_undo`] scope joins that scope; with no scope open it is
/// one undo step of its own, whose undo restores the previous entries
/// byte-exactly. Refusals record no undo step.
fn update_xrecord(
&mut self,
_dictionary_path: &[&str],
@@ -1144,8 +1153,10 @@ pub trait HostApi {
/// Delete the XRecord named `name` and its dictionary entry (API v8,
/// additive). Returns the removed record, so audits keep the prior
/// content. A refusal leaves the drawing untouched; a successful delete is
/// one undo step.
/// content. A refusal leaves the drawing untouched. Undo composition — the
/// entity-write contract: a mutation inside a plugin-opened
/// [`HostApi::push_undo`] scope joins that scope; with no scope open it is
/// one undo step of its own. Refusals record no undo step.
fn delete_xrecord(
&mut self,
_dictionary_path: &[&str],