{"record":{"id":"3f9e4714ea0c2961","repo":"siyuan-note/siyuan","slug":"duplicate-source-id-s","errorCode":null,"errorMessage":"duplicate source ID [%s]","messagePattern":"duplicate source ID \\[(.+?)\\]","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"kernel/model/file.go","lineNumber":2754,"sourceCode":"\tIncSync()\n\tpushFiletreeSortChanged(sortIDs)\n\treturn\n}\n\nfunc validateReorderArgs(sourceIDs []string, targetID, position string) error {\n\tif 1 > len(sourceIDs) {\n\t\treturn errors.New(\"source IDs must not be empty\")\n\t}\n\tif \"before\" != position && \"after\" != position {\n\t\treturn fmt.Errorf(\"invalid reorder position [%s]\", position)\n\t}\n\tseen := map[string]struct{}{}\n\tfor _, sourceID := range sourceIDs {\n\t\tif sourceID == targetID {\n\t\t\treturn fmt.Errorf(\"target ID [%s] must not be included in source IDs\", targetID)\n\t\t}\n\t\tif _, ok := seen[sourceID]; ok {\n\t\t\treturn fmt.Errorf(\"duplicate source ID [%s]\", sourceID)\n\t\t}\n\t\tseen[sourceID] = struct{}{}\n\t}\n\treturn nil\n}\n\nfunc isSortableDocument(tree *treenode.BlockTree) bool {\n\treturn nil != tree && tree.ID == tree.RootID && \"d\" == tree.Type && !IsBoxDoc(tree.BoxID, tree.RootID)\n}\n\nfunc loadSiblingCustomOrder(boxID, parentPath string, fullSortIDs map[string]int) (ret []string, err error) {\n\tabsParentPath := filepath.Join(util.DataDir, boxID, parentPath)\n\tfiles, err := os.ReadDir(absParentPath)\n\tif nil != err {\n\t\treturn nil, fmt.Errorf(\"read dir [%s] failed: %w\", absParentPath, err)\n\t}\n\tfor _, file := range files {\n\t\tif file.IsDir() || !strings.HasSuffix(file.Name(), \".sy\") {","sourceCodeStart":2736,"sourceCodeEnd":2772,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/9f775e8a12daef8255556097396f9b2739078892/kernel/model/file.go#L2736-L2772","documentation":"Each source document may appear only once in a reorder request. validateReorderArgs tracks seen IDs and fails with this error if the same ID occurs twice. Duplicates would corrupt the computed ordering, so they are rejected before any mutation.","triggerScenarios":"sourceIDs built by concatenating selections that overlap (e.g. two multi-select ranges sharing a document); retry logic appending to an existing list instead of replacing it.","commonSituations":"Plugins merging drag selections across tree nodes without dedup; idempotency bugs where the same batch is accumulated twice; manually authored API payloads with a repeated ID.","solutions":["Deduplicate sourceIDs (e.g. via a Set) before sending the request","Fix the selection-merging logic that produces overlapping ranges","Replace rather than append when retrying a failed reorder with the same IDs"],"exampleFix":"// before\nreorderDocs(rawIDs, targetID, \"after\") // may contain duplicates\n// after\nconst sources = [...new Set(rawIDs)].filter(id => id !== targetID)\nreorderDocs(sources, targetID, \"after\")","handlingStrategy":"validation","validationCode":"const sources = [...new Set(sourceIDs)]\nif (sources.length !== sourceIDs.length) { /* deduplicated; proceed with sources */ }","typeGuard":null,"tryCatchPattern":"try { await api.reorderDocs(src, targetID, pos) } catch (e) { if (String(e).includes(\"duplicate source ID\")) { src = [...new Set(src)]; return retryOnce() } throw e }","preventionTips":["Deduplicate any list assembled from multiple selection ranges","Replace (not append) source lists on retry","Use Set-based collection while accumulating IDs from UI events"],"tags":["go","kernel","validation","duplicate"],"backgroundTag":"invalid-argument-value","analyzedSha":"9f775e8a12daef8255556097396f9b2739078892","analyzedAt":"2026-09-19T03:17:15.984Z","contentChangedAt":"2026-09-19T03:17:15.984Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}