{"record":{"id":"660596d008d6222c","repo":"siyuan-note/siyuan","slug":"block-s-type-is-locked-expected-s-got-s","errorCode":null,"errorMessage":"block [%s] type is locked: expected %s, got %s","messagePattern":"block \\[(.+?)\\] type is locked: expected (.+?), got (.+?)","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"kernel/model/block_update.go","lineNumber":316,"sourceCode":"}\n\nfunc firstContentBlock(parent *ast.Node) *ast.Node {\n\tif nil == parent {\n\t\treturn nil\n\t}\n\tfor child := parent.FirstChild; nil != child; child = child.Next {\n\t\tif child.IsBlock() && ast.NodeKramdownBlockIAL != child.Type {\n\t\t\treturn child\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc validateBlockUpdateType(oldNode, updatedNode *ast.Node, lockType bool) error {\n\tif !lockType || oldNode.Type == updatedNode.Type || isEmptyParagraphBlock(oldNode) {\n\t\treturn nil\n\t}\n\treturn fmt.Errorf(\"block [%s] type is locked: expected %s, got %s\",\n\t\toldNode.ID, oldNode.Type.String(), updatedNode.Type.String())\n}\n\nfunc isEmptyParagraphBlock(node *ast.Node) bool {\n\tif nil == node || ast.NodeParagraph != node.Type {\n\t\treturn false\n\t}\n\tfor child := node.FirstChild; nil != child; child = child.Next {\n\t\tswitch child.Type {\n\t\tcase ast.NodeText:\n\t\t\ttext := strings.ReplaceAll(string(child.Tokens), \"\\u200b\", \"\")\n\t\t\tif \"\" != strings.TrimSpace(text) {\n\t\t\t\treturn false\n\t\t\t}\n\t\tcase ast.NodeSoftBreak, ast.NodeBr:\n\t\tdefault:\n\t\t\treturn false\n\t\t}","sourceCodeStart":298,"sourceCodeEnd":334,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/251596fc0de2f9528c00c224252fd073a99973f4/kernel/model/block_update.go#L298-L334","documentation":"Returned by validateBlockUpdateType. When lockType is true, the updated node's Type must equal the old node's Type (with an exemption for empty paragraph blocks via isEmptyParagraphBlock). Any other type transition is rejected to prevent silent block-type conversion during an update. The message names the old ID, the expected (old) type, and the actual (new) type.","triggerScenarios":"Updating a paragraph block with content that parses as a heading/list/blockquote; updating a heading with body text that Lute rewrites to a paragraph; submitting DOM whose root content block type differs from the target.","commonSituations":"User changes a paragraph into a heading in the editor and the integration sends it as an update rather than a replace; markdown input whose leading # changes the parsed type.","solutions":["When the user's intent is to change block type, use delete+insert (or a dedicated transform API) instead of BlockUpdate.","Keep the submitted DOM's root content block type identical to the existing block when lockType is on.","If the API exposes a lockType toggle and type change is intended, pass lockType=false."],"exampleFix":"// before\n// updating a paragraph with markdown \"# Heading\" -> type changes Paragraph -> Heading\n\n// after\n// delete the paragraph, then insert the heading as a separate operation\n// or submit markdown that keeps the paragraph shape and use a separate transform path","handlingStrategy":"validation","validationCode":"// determine parsed type of the new DOM and compare to old type\nnewTree := lute.BlockDOM2Tree(input.Data)\nif firstContentBlock(newTree.Root).Type != oldNodeType && !isEmptyParagraph(oldNode) {\n    return errors.New(\"type would change; use delete+insert instead\")\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Use delete+insert for intentional block-type changes.","Keep the submitted DOM root content block type identical to the target when lockType is on.","Surface the locked-type rule to the user in the UI when they attempt a type-changing update."],"tags":["block-update","type-lock","validation"],"backgroundTag":null,"analyzedSha":"251596fc0de2f9528c00c224252fd073a99973f4","analyzedAt":"2026-08-12T21:18:37.123Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}