{"record":{"id":"87b2323f4eb96261","repo":"siyuan-note/siyuan","slug":"template-document-tree-plan-parent-operations-are","errorCode":null,"errorMessage":"template document tree plan parent operations are not reversible","messagePattern":"template document tree plan parent operations are not reversible","errorType":"error_code","errorClass":null,"httpStatus":null,"severity":"error","filePath":"kernel/model/template_doc_tree.go","lineNumber":561,"sourceCode":"\t\t}\n\t\tundoOperations[operation.Action+\"\\x00\"+operation.ID]++\n\t}\n\n\thasContentMutation := false\n\tfor _, operation := range transaction.DoOperations {\n\t\tif nil == operation || \"\" == operation.ID {\n\t\t\treturn errors.New(\"template document tree plan contains an invalid parent operation\")\n\t\t}\n\t\tinverseAction, supported := inverseActions[operation.Action]\n\t\tif !supported || \"\" != operation.RootID {\n\t\t\treturn errors.New(\"template document tree plan contains an unsupported parent operation\")\n\t\t}\n\t\tif \"insert\" == operation.Action || \"delete\" == operation.Action || \"update\" == operation.Action {\n\t\t\thasContentMutation = true\n\t\t}\n\t\tkey := inverseAction + \"\\x00\" + operation.ID\n\t\tif 1 > undoOperations[key] {\n\t\t\treturn errors.New(\"template document tree plan parent operations are not reversible\")\n\t\t}\n\t\tundoOperations[key]--\n\t}\n\tif !hasContentMutation {\n\t\treturn errors.New(\"template document tree plan requires a parent content operation\")\n\t}\n\tfor _, count := range undoOperations {\n\t\tif 0 != count {\n\t\t\treturn errors.New(\"template document tree plan parent operations are not reversible\")\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc transactionTargetsTemplateRoot(transaction *Transaction, rootID, boxID string) bool {\n\tmatched := false\n\tfor operationSetIndex, operations := range [][]*Operation{transaction.DoOperations, transaction.UndoOperations} {\n\t\tfor _, operation := range operations {","sourceCodeStart":543,"sourceCodeEnd":579,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/8641553a1f07374001902d3ce773285db1292b2d/kernel/model/template_doc_tree.go#L543-L579","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["For every DoOperation, emit an UndoOperation with the inverse action (insert<->delete, update<->update, foldHeading<->unfoldHeading, setAttrs<->setAttrs) and the same ID","Ensure the Do and Undo lists are exact mirrors with no extra or missing entries so counters balance to zero","Include at least one content mutation (insert, delete, or update) — fold/attr-only pairs are insufficient","Generate both lists programmatically from the same operation set instead of writing them by hand"],"exampleFix":"// before\ndoOps := []*Operation{{Action: \"insert\", ID: blkID, ParentID: p}}\nundoOps := []*Operation{} // no inverse\n// after\ndoOps := []*Operation{{Action: \"insert\", ID: blkID, ParentID: p}}\nundoOps := []*Operation{{Action: \"delete\", ID: blkID}} // inverse with same ID","handlingStrategy":"validation","validationCode":"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 } }","typeGuard":null,"tryCatchPattern":"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 }","preventionTips":["Generate UndoOperations as the exact mirror of DoOperations from one source of truth","Pair every do op with its inverse action and identical ID","Include at least one insert/delete/update pair in the parent transaction","Add a client-side balance check (counts must net to zero) before submission"],"tags":["go","transaction","validation","undo-redo","reversibility"],"backgroundTag":"invalid-state-transition","analyzedSha":"8641553a1f07374001902d3ce773285db1292b2d","analyzedAt":"2026-09-11T16:08:28.414Z","contentChangedAt":"2026-09-11T16:08:28.414Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}