Pure functions from vault ZIP to import plan — no DB, no DI:
- parseVaultZip: fflate unzip with the plugin-package protections
(zip-slip rejection, incremental unpacked ceiling 256 MiB,
parameterized for tests); dot-directories like .obsidian/ skipped;
deterministic ordering.
- extractFrontmatter: leading --- block, tags:/tag: in scalar, inline-
array, and block-list forms; strip mode drops the block, preserve
re-emits it as a fenced yaml code block.
- extractInlineTags: fence- and inline-code-aware #tag / #nested/tag
extraction and removal (headings and pure numbers untouched).
- rewriteLinks: [[Name]], [[Name|Display]], [[Name#Heading]] (fragment
stripped), [[folder/Name]] (path match beats basename) → the FINAL
slug with the human name as display; unresolvable → slugified
phantom; ![[img]] and relative  → vault-asset: placeholders
the uploader resolves (#117); non-image embeds → italic filename +
page attachment; SVG deliberately stays an attachment (never inline,
security.md); note embeds degrade to plain wikilinks.
- planFolders: folder chains merged at the deepest levels to fit
MAX_PAGE_DEPTH below the mount page (merged titles read c/d).
- planSlugs: -n suffixing against existing ∪ batch; duplicate
basenames resolve to the lexicographically first vault path.
- planVaultImport ties it together into containers + notes + the
referenced-asset set.
Fixture vault under fixtures/import/obsidian-vault/ (umlauts,
duplicate basenames, nested tags, deep folders, embeds, code traps);
13 unit tests colocated.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>