{"record":{"id":"d056ee7eaf9489b9","repo":"siyuan-note/siyuan","slug":"346","errorCode":"346","errorMessage":"Markdown [%s] changed after analysis: %w","messagePattern":"Markdown \\[(.+?)\\] changed after analysis: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"kernel/model/import_obsidian.go","lineNumber":1867,"sourceCode":"\t}\n\tif len(vault.Assets) != len(fresh.Assets) {\n\t\treturn newObsidianUserError(349, \"\", errors.New(\"the attachment file list changed after analysis\"))\n\t}\n\tfor key := range vault.Assets {\n\t\tif fresh.Assets[key] == nil {\n\t\t\treturn newObsidianUserError(349, \"\", errors.New(\"the attachment file list changed after analysis\"))\n\t\t}\n\t}\n\tfor _, doc := range vault.Docs {\n\t\tif doc.Synthetic {\n\t\t\tcontinue\n\t\t}\n\t\tif err := ctx.Err(); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif err := validateObsidianSourceMetadata(doc.Source); err != nil {\n\t\t\treturn newObsidianUserError(346, doc.Source.RelPath,\n\t\t\t\tfmt.Errorf(\"Markdown [%s] changed after analysis: %w\", doc.Source.RelPath, err))\n\t\t}\n\t}\n\tfor _, asset := range vault.ImportAssets {\n\t\tif err := ctx.Err(); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif err := validateObsidianSourceMetadata(asset.Source); err != nil {\n\t\t\treturn newObsidianUserError(346, asset.Source.RelPath,\n\t\t\t\tfmt.Errorf(\"attachment [%s] changed after analysis: %w\", asset.Source.RelPath, err))\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc validateObsidianSourceMetadata(file *obsidianSourceFile) error {\n\tinfo, err := os.Stat(file.AbsPath)\n\tif err != nil {\n\t\treturn err","sourceCodeStart":1849,"sourceCodeEnd":1885,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/9f775e8a12daef8255556097396f9b2739078892/kernel/model/import_obsidian.go#L1849-L1885","documentation":"Just before converting each document, the importer re-validates the source file's metadata (size/mtime via validateObsidianSourceMetadata) against what was recorded at analysis time. Any drift is wrapped as localized user error 346, 'Markdown [%s] changed after analysis', naming the file — the file's content changed after the import plan was built, so the import is aborted rather than importing a stale or mismatched document.","triggerScenarios":"validateObsidianSourceMetadata(doc.Source) returns an error in the per-document loop — the .md file's size or modification time differs from the analysis snapshot, typically because the file was edited or re-synced after analysis.","commonSituations":"Obsidian open with autosave writing to the file during import; sync client downloading a newer version mid-import; build/backup tool touching mtimes without content changes.","solutions":["Re-run the import so the analysis captures the file's current state","Close Obsidian and pause sync during import to prevent concurrent writes","If only mtimes changed (touch by a tool) with identical content, re-running analysis resolves it; do not attempt to bypass the check"],"exampleFix":null,"handlingStrategy":"retry","validationCode":"// compare current size+mtime of each source file against the values captured at analysis time\nfor _, s := range analyzedSources {\n    if info, err := os.Stat(s.abs); err != nil || info.Size() != s.size || !info.ModTime().Equal(s.mtime) { /* changed; re-analyze */ }\n}","typeGuard":null,"tryCatchPattern":"if code == 346 {\n    // the named file changed; re-run the import so the analysis reflects the current content\n}","preventionTips":["Close Obsidian (autosave can rewrite files mid-import) before importing","Pause sync clients so files are not updated during the import","Avoid tools that touch mtimes (backup/indexers) on the vault during import"],"tags":["go","obsidian-import","race-condition"],"backgroundTag":"source-changed-during-operation","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"}