{"record":{"id":"43a964ddc5837c2f","repo":"siyuan-note/siyuan","slug":"template-document-tree-plan-requires-reversible-pa","errorCode":null,"errorMessage":"template document tree plan requires reversible parent operations","messagePattern":"template document tree plan requires reversible parent operations","errorType":"error_code","errorClass":null,"httpStatus":null,"severity":"error","filePath":"kernel/model/template_doc_tree.go","lineNumber":467,"sourceCode":"func AttachTemplateDocTreePlans(transactions []*Transaction) (attached bool, err error) {\n\tvar target *Transaction\n\tfor _, transaction := range transactions {\n\t\tif nil == transaction || \"\" == transaction.TemplateDocTreePlanID {\n\t\t\tcontinue\n\t\t}\n\t\tif nil != target || 1 != len(transactions) {\n\t\t\treturn false, errors.New(\"a document tree plan must be applied in a single transaction\")\n\t\t}\n\t\ttarget = transaction\n\t}\n\tif nil == target {\n\t\treturn false, nil\n\t}\n\tif target.isReplay {\n\t\treturn false, errors.New(\"template document tree plans cannot be attached to replay transactions\")\n\t}\n\tif 0 == len(target.DoOperations) || 0 == len(target.UndoOperations) {\n\t\treturn false, errors.New(\"template document tree plan requires reversible parent operations\")\n\t}\n\tif err = validateTemplateDocTreeParentOperations(target); nil != err {\n\t\treturn false, err\n\t}\n\tfor _, operation := range append(append([]*Operation{}, target.DoOperations...), target.UndoOperations...) {\n\t\tif nil != operation && (\"restoreCreatedDoc\" == operation.Action || \"removeCreatedDoc\" == operation.Action) {\n\t\t\treturn false, errors.New(\"template document tree transaction contains a reserved operation\")\n\t\t}\n\t}\n\n\tplanID := target.TemplateDocTreePlanID\n\ttarget.TemplateDocTreePlanID = \"\"\n\tvalue, loaded := templateDocTreePlans.LoadAndDelete(planID)\n\tif !loaded {\n\t\treturn false, errors.New(\"template document tree plan is missing or has expired\")\n\t}\n\tplan, ok := value.(*templateDocTreePlan)\n\tif !ok || plan.id != planID || time.Now().After(plan.expiresAt) {","sourceCodeStart":449,"sourceCodeEnd":485,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/8641553a1f07374001902d3ce773285db1292b2d/kernel/model/template_doc_tree.go#L449-L485","documentation":"A document-tree plan transaction must carry both DoOperations and UndoOperations so the generated documents can be rolled back; an empty list on either side triggers this error. The plan mechanism compensates by undoing the parent operation if any part of the tree creation fails, so reversibility is mandatory.","triggerScenarios":"Submitting a transaction with TemplateDocTreePlanID set but with no undo operations (or no do operations) in performTransactions — typically a hand-constructed transaction missing the compensating operation list.","commonSituations":"Plugin or script authors building the plan transaction manually and forgetting UndoOperations; serialization dropping empty operation arrays; a code path that clears undo ops after partial failure and re-submits the transaction.","solutions":["Populate UndoOperations with the inverse of every DoOperation when constructing the plan transaction","Build the transaction through the official template rendering flow (renderTemplateSource) instead of hand-assembling operations","Do not strip or clear UndoOperations before submission","Verify both operation arrays are non-empty before calling the transactions API"],"exampleFix":"// before: missing undo side\ntx := &Transaction{TemplateDocTreePlanID: planID, DoOperations: doOps}\n// after\ntx := &Transaction{TemplateDocTreePlanID: planID, DoOperations: doOps, UndoOperations: undoOps}","handlingStrategy":"validation","validationCode":"// Go: verify both operation sides before submitting a plan transaction\nreversible := len(tx.DoOperations) > 0 && len(tx.UndoOperations) > 0\nif !reversible {\n    return errors.New(\"plan transaction needs non-empty Do and Undo operations\")\n}","typeGuard":null,"tryCatchPattern":"if _, err := AttachTemplateDocTreePlans([]*Transaction{tx}); err != nil && strings.Contains(err.Error(), \"reversible\") {\n    return rebuildPlanTransactionWithUndo(tx)\n}","preventionTips":["Generate plan transactions through renderTemplateSource, never by hand","Always pair every do operation with its inverse undo operation","Assert both operation arrays are non-empty before calling the API","Do not strip UndoOperations when serializing or copying transactions"],"tags":["transaction","undo","template-doc-tree","reversibility","siyuan-kernel"],"backgroundTag":"empty-required-field","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"}