siyuan-note/siyuan · error

template document tree plan parent operations are not revers

Error message

template document tree plan parent operations are not reversible

What it means

Reversibility check: for each DoOperation, an UndoOperation whose action is the inverse (key = inverseAction + NUL + ID) must exist; afterwards no leftover undo operations may remain (the trailing loop reports the same message). This error means the Do and Undo lists are not exact inverses of each other — an operation has no matching inverse, or inverses are missing/duplicated so counts never balance to zero.

Source

Thrown at kernel/model/template_doc_tree.go:561

		}
		undoOperations[operation.Action+"\x00"+operation.ID]++
	}

	hasContentMutation := false
	for _, operation := range transaction.DoOperations {
		if nil == operation || "" == operation.ID {
			return errors.New("template document tree plan contains an invalid parent operation")
		}
		inverseAction, supported := inverseActions[operation.Action]
		if !supported || "" != operation.RootID {
			return errors.New("template document tree plan contains an unsupported parent operation")
		}
		if "insert" == operation.Action || "delete" == operation.Action || "update" == operation.Action {
			hasContentMutation = true
		}
		key := inverseAction + "\x00" + operation.ID
		if 1 > undoOperations[key] {
			return errors.New("template document tree plan parent operations are not reversible")
		}
		undoOperations[key]--
	}
	if !hasContentMutation {
		return errors.New("template document tree plan requires a parent content operation")
	}
	for _, count := range undoOperations {
		if 0 != count {
			return errors.New("template document tree plan parent operations are not reversible")
		}
	}
	return nil
}

func transactionTargetsTemplateRoot(transaction *Transaction, rootID, boxID string) bool {
	matched := false
	for operationSetIndex, operations := range [][]*Operation{transaction.DoOperations, transaction.UndoOperations} {
		for _, operation := range operations {

View on GitHub (pinned to 8641553a1f)

Solutions

  1. For every DoOperation, emit an UndoOperation with the inverse action (insert<->delete, update<->update, foldHeading<->unfoldHeading, setAttrs<->setAttrs) and the same ID
  2. Ensure the Do and Undo lists are exact mirrors with no extra or missing entries so counters balance to zero
  3. Include at least one content mutation (insert, delete, or update) — fold/attr-only pairs are insufficient
  4. Generate both lists programmatically from the same operation set instead of writing them by hand

Example fix

// before
doOps := []*Operation{{Action: "insert", ID: blkID, ParentID: p}}
undoOps := []*Operation{} // no inverse
// after
doOps := []*Operation{{Action: "insert", ID: blkID, ParentID: p}}
undoOps := []*Operation{{Action: "delete", ID: blkID}} // inverse with same ID
Defensive patterns

Strategy: validation

Validate before calling

inverse := map[string]string{"insert":"delete","delete":"insert","update":"update","foldHeading":"unfoldHeading","unfoldHeading":"foldHeading","setAttrs":"setAttrs"}; need := map[string]int{}; for _, op := range tx.DoOperations { need[inverse[op.Action]+"\x00"+op.ID]++ }; got := map[string]int{}; for _, op := range tx.UndoOperations { got[op.Action+"\x00"+op.ID]++ }; for k, n := range need { if got[k] < n { reject: not reversible } }

Try / catch

if err != nil && strings.Contains(err.Error(), "parent operations are not reversible") { rebuild the undo list programmatically as the exact inverse of the do list and resubmit }

Prevention

When it happens

Trigger: DoOperations with fewer UndoOperations than needed; mismatched action pairs (e.g. insert forward with setAttrs undo); duplicate or extra undo operations left after all do ops are matched (leftover count != 0 hits the second identical error at line 570); the transaction lacks an insert/delete/update pair so hasContentMutation also triggers the related 'requires a parent content operation' error.

Common situations: Manually constructed transactions where the undo list was truncated or reordered incorrectly; clients that generate undo ops only for some actions; tests with asymmetric fixtures.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


AI-assisted analysis of siyuan-note/siyuan@8641553a1f (2026-09-11). Data as JSON: /api/errors/87b2323f4eb96261. Report an issue: GitHub.